1package theme
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/infrastructure/config"
8 "github.com/bnema/dumber/internal/logging"
9 "github.com/jwijenbergh/puregotk/v4/gdk"
10 "github.com/jwijenbergh/puregotk/v4/gtk"
11)
12
13// Manager handles theme state and CSS application.
14type Manager struct {
15 scheme string // "light", "dark", "system"
16 prefersDark bool // Resolved dark mode preference
17 lightPalette Palette // Light theme colors
18 darkPalette Palette // Dark theme colors
19 uiScale float64 // UI scaling factor (1.0 = 100%)
20 fonts FontConfig
21 cssProvider *gtk.CssProvider
22}
23
24// NewManager creates a new theme manager from configuration.
25func NewManager(ctx context.Context, cfg *config.Config) *Manager {
26 log := logging.FromContext(ctx)
27
28 // Determine color scheme preference
29 scheme := "system"
30 if cfg != nil && cfg.Appearance.ColorScheme != "" {
31 scheme = cfg.Appearance.ColorScheme
32 }
33
34 // Resolve whether we should use dark mode
35 prefersDark := ResolveColorScheme(scheme)
36
37 // Build palettes from config or defaults
38 var lightPalette, darkPalette Palette
39 if cfg != nil {
40 lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false)
41 darkPalette = PaletteFromConfig(&cfg.Appearance.DarkPalette, true)
42 } else {
43 lightPalette = DefaultLightPalette()
44 darkPalette = DefaultDarkPalette()
45 }
46
47 // Get UI scale factor (default to 1.0 if not set)
48 uiScale := 1.0
49 if cfg != nil && cfg.DefaultUIScale > 0 {
50 uiScale = cfg.DefaultUIScale
51 }
52
53 fonts := DefaultFontConfig()
54 if cfg != nil {
55 fonts.SansFont = Coalesce(cfg.Appearance.SansFont, fonts.SansFont)
56 fonts.MonospaceFont = Coalesce(cfg.Appearance.MonospaceFont, fonts.MonospaceFont)
57 }
58
59 log.Debug().
60 Str("scheme", scheme).
61 Bool("prefers_dark", prefersDark).
62 Float64("ui_scale", uiScale).
63 Str("sans_font", fonts.SansFont).
64 Str("monospace_font", fonts.MonospaceFont).
65 Msg("theme manager initialized")
66
67 return &Manager{
68 scheme: scheme,
69 prefersDark: prefersDark,
70 lightPalette: lightPalette,
71 darkPalette: darkPalette,
72 uiScale: uiScale,
73 fonts: fonts,
74 }
75}
76
77// PrefersDark returns true if dark mode is active.
78func (m *Manager) PrefersDark() bool {
79 return m.prefersDark
80}
81
82// GetCurrentPalette returns the active palette based on current scheme.
83func (m *Manager) GetCurrentPalette() Palette {
84 if m.prefersDark {
85 return m.darkPalette
86 }
87 return m.lightPalette
88}
89
90// GetBackgroundRGBA returns the current theme's background color as RGBA float32 values.
91// Used to set WebView background color to eliminate white flash during loading.
92func (m *Manager) GetBackgroundRGBA() (r, g, b, a float32) {
93 palette := m.GetCurrentPalette()
94 return HexToRGBA(palette.Background)
95}
96
97// GetLightPalette returns the light theme palette.
98func (m *Manager) GetLightPalette() Palette {
99 return m.lightPalette
100}
101
102// GetDarkPalette returns the dark theme palette.
103func (m *Manager) GetDarkPalette() Palette {
104 return m.darkPalette
105}
106
107// GetWebUIThemeCSS returns CSS text that defines both light and dark variables.
108// WebUI uses `:root` for light and `.dark` overrides for dark.
109func (m *Manager) GetWebUIThemeCSS() string {
110 lightVars := m.lightPalette.ToWebCSSVars()
111 darkVars := m.darkPalette.ToWebCSSVars()
112 return fmt.Sprintf(":root{\n%s}\n\n.dark{\n%s}\n", lightVars, darkVars)
113}
114
115// ApplyToDisplay loads the theme CSS into the display.
116func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) {
117 log := logging.FromContext(ctx)
118
119 if display == nil {
120 log.Warn().Msg("cannot apply theme: display is nil")
121 return
122 }
123
124 // Generate CSS with current palette, UI scale and fonts
125 palette := m.GetCurrentPalette()
126 css := GenerateCSSWithScaleAndFonts(palette, m.uiScale, m.fonts)
127
128 // Create CSS provider if needed
129 if m.cssProvider == nil {
130 m.cssProvider = gtk.NewCssProvider()
131 }
132
133 if m.cssProvider == nil {
134 log.Error().Msg("failed to create CSS provider")
135 return
136 }
137
138 // Load CSS
139 m.cssProvider.LoadFromString(css)
140 gtk.StyleContextAddProviderForDisplay(
141 display,
142 m.cssProvider,
143 uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION),
144 )
145
146 log.Debug().
147 Bool("dark_mode", m.prefersDark).
148 Msg("theme CSS applied to display")
149}
150
151// SetColorScheme changes the active color scheme at runtime.
152func (m *Manager) SetColorScheme(ctx context.Context, scheme string, display *gdk.Display) {
153 log := logging.FromContext(ctx)
154
155 m.scheme = scheme
156 m.prefersDark = ResolveColorScheme(scheme)
157
158 log.Info().
159 Str("scheme", scheme).
160 Bool("prefers_dark", m.prefersDark).
161 Msg("color scheme changed")
162
163 // Re-apply CSS if display is available
164 if display != nil {
165 m.ApplyToDisplay(ctx, display)
166 }
167}
168
169// UpdateFromConfig updates the theme manager state from a new config.
170func (m *Manager) UpdateFromConfig(ctx context.Context, cfg *config.Config, display *gdk.Display) {
171 log := logging.FromContext(ctx)
172
173 if cfg == nil {
174 return
175 }
176
177 // Update scheme and resolve prefersDark
178 scheme := "system"
179 if cfg.Appearance.ColorScheme != "" {
180 scheme = cfg.Appearance.ColorScheme
181 }
182 m.scheme = scheme
183 m.prefersDark = ResolveColorScheme(scheme)
184
185 // Update palettes
186 m.lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false)
187 m.darkPalette = PaletteFromConfig(&cfg.Appearance.DarkPalette, true)
188
189 // Update UI scale
190 if cfg.DefaultUIScale > 0 {
191 m.uiScale = cfg.DefaultUIScale
192 }
193
194 defaults := DefaultFontConfig()
195 m.fonts = FontConfig{
196 SansFont: Coalesce(cfg.Appearance.SansFont, defaults.SansFont),
197 MonospaceFont: Coalesce(cfg.Appearance.MonospaceFont, defaults.MonospaceFont),
198 }
199
200 log.Info().
201 Str("scheme", m.scheme).
202 Bool("prefers_dark", m.prefersDark).
203 Float64("ui_scale", m.uiScale).
204 Str("sans_font", m.fonts.SansFont).
205 Str("monospace_font", m.fonts.MonospaceFont).
206 Msg("theme manager updated from config")
207
208 // Re-apply CSS if display is available
209 if display != nil {
210 m.ApplyToDisplay(ctx, display)
211 }
212}