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