fix: attempt to eliminate white flash in dark mode (WIP, see #52)
- commit
- ddb81a0be26f31da50e5e03f0e3dac1b32921427
- author
- brice · · 2025-12-28 09:17 UTC
- parent
- 3748947
- signature
- unsigned
6 files · +99 -21
M internal/bootstrap/webkit_stack.go +3 -0
| @@ -95,6 +95,9 @@ func BuildWebKitStack( | |||
| 95 | 95 | settings := webkit.NewSettingsManager(ctx, cfg) | |
| 96 | 96 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | |
| 97 | 97 | ||
| 98 | + | // Set background color on injector for early CSS injection (prevents white flash) | |
| 99 | + | injector.SetBackgroundColor(themeManager.GetCurrentPalette().Background) | |
| 100 | + | ||
| 98 | 101 | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |
| 99 | 102 | themeCSSText := themeManager.GetWebUIThemeCSS() | |
| 100 | 103 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil { | |
| @@ -95,6 +95,9 @@ func BuildWebKitStack( | |||||
| 95 | settings := webkit.NewSettingsManager(ctx, cfg) | 95 | settings := webkit.NewSettingsManager(ctx, cfg) | ||
| 96 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | 96 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | ||
| 97 | 97 | ||||
| 98 | + | // Set background color on injector for early CSS injection (prevents white flash) | |||
| 99 | + | injector.SetBackgroundColor(themeManager.GetCurrentPalette().Background) | |||
| 100 | + | ||||
| 98 | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | 101 | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | ||
| 99 | themeCSSText := themeManager.GetWebUIThemeCSS() | 102 | themeCSSText := themeManager.GetWebUIThemeCSS() | ||
| 100 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil { | 103 | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil { | ||
M internal/infrastructure/webkit/factory.go +14 -8
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 9 | 9 | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | // WebViewFactory creates WebView instances for the application. | |
| @@ -138,19 +139,24 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||
| 138 | 139 | func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) { | |
| 139 | 140 | log := logging.FromContext(ctx) | |
| 140 | 141 | ||
| 141 | - | wv, err := NewWebView(ctx, f.wkCtx, f.settings) | |
| 142 | - | if err != nil { | |
| 143 | - | return nil, err | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | // Set background color to match theme (eliminates white flash) | |
| 142 | + | // Build background color for immediate application during WebView creation | |
| 147 | 143 | f.bgMu.RLock() | |
| 148 | 144 | r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA | |
| 149 | 145 | f.bgMu.RUnlock() | |
| 150 | - | if a > 0 { | |
| 151 | - | wv.SetBackgroundColor(r, g, b, a) | |
| 146 | + | ||
| 147 | + | var bgColor *gdk.RGBA | |
| 148 | + | if a > 0 { // Only set if a valid color was configured | |
| 149 | + | bgColor = &gdk.RGBA{Red: r, Green: g, Blue: b, Alpha: a} | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | wv, err := NewWebView(ctx, f.wkCtx, f.settings, bgColor) | |
| 153 | + | if err != nil { | |
| 154 | + | return nil, err | |
| 152 | 155 | } | |
| 153 | 156 | ||
| 157 | + | // Add CSS class for theme background styling (prevents white flash) | |
| 158 | + | wv.inner.AddCssClass("webview-themed") | |
| 159 | + | ||
| 154 | 160 | // Keep hidden until content is painted | |
| 155 | 161 | wv.inner.SetVisible(false) | |
| 156 | 162 | ||
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 9 | "github.com/bnema/dumber/internal/logging" | 9 | "github.com/bnema/dumber/internal/logging" | ||
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| 12 | // WebViewFactory creates WebView instances for the application. | 13 | // WebViewFactory creates WebView instances for the application. | ||
| @@ -138,19 +139,24 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||||
| 138 | func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) { | 139 | func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) { | ||
| 139 | log := logging.FromContext(ctx) | 140 | log := logging.FromContext(ctx) | ||
| 140 | 141 | ||||
| 141 | - | wv, err := NewWebView(ctx, f.wkCtx, f.settings) | 142 | + | // Build background color for immediate application during WebView creation |
| 142 | - | if err != nil { | |||
| 143 | - | return nil, err | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | // Set background color to match theme (eliminates white flash) | |||
| 147 | f.bgMu.RLock() | 143 | f.bgMu.RLock() | ||
| 148 | r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA | 144 | r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA | ||
| 149 | f.bgMu.RUnlock() | 145 | f.bgMu.RUnlock() | ||
| 150 | - | if a > 0 { | 146 | + | |
| 151 | - | wv.SetBackgroundColor(r, g, b, a) | 147 | + | var bgColor *gdk.RGBA |
| 148 | + | if a > 0 { // Only set if a valid color was configured | |||
| 149 | + | bgColor = &gdk.RGBA{Red: r, Green: g, Blue: b, Alpha: a} | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | wv, err := NewWebView(ctx, f.wkCtx, f.settings, bgColor) | |||
| 153 | + | if err != nil { | |||
| 154 | + | return nil, err | |||
| 152 | } | 155 | } | ||
| 153 | 156 | ||||
| 157 | + | // Add CSS class for theme background styling (prevents white flash) | |||
| 158 | + | wv.inner.AddCssClass("webview-themed") | |||
| 159 | + | ||||
| 154 | // Keep hidden until content is painted | 160 | // Keep hidden until content is painted | ||
| 155 | wv.inner.SetVisible(false) | 161 | wv.inner.SetVisible(false) | ||
| 156 | 162 | ||||
M internal/infrastructure/webkit/injector.go +56 -5
| @@ -25,6 +25,34 @@ const themeCSSScript = `(function() { | |||
| 25 | 25 | (document.head || document.documentElement).appendChild(style); | |
| 26 | 26 | })();` | |
| 27 | 27 | ||
| 28 | + | // earlyBackgroundScript injects a temporary background color to prevent white flash. | |
| 29 | + | // It's injected at document start and removed after the page loads. | |
| 30 | + | // The %s placeholder is replaced with the hex color (e.g., "#0a0a0b"). | |
| 31 | + | const earlyBackgroundScript = `(function() { | |
| 32 | + | var style = document.createElement('style'); | |
| 33 | + | style.id = '__dumber_early_bg'; | |
| 34 | + | style.textContent = 'html,body{background-color:%s!important}'; | |
| 35 | + | document.documentElement.appendChild(style); | |
| 36 | + | ||
| 37 | + | // Remove the style after page content loads to avoid overriding page styles | |
| 38 | + | function removeEarlyBg() { | |
| 39 | + | var s = document.getElementById('__dumber_early_bg'); | |
| 40 | + | if (s) s.remove(); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Try multiple events to ensure removal | |
| 44 | + | if (document.readyState === 'complete') { | |
| 45 | + | setTimeout(removeEarlyBg, 100); | |
| 46 | + | } else { | |
| 47 | + | window.addEventListener('DOMContentLoaded', function() { | |
| 48 | + | setTimeout(removeEarlyBg, 100); | |
| 49 | + | }, {once: true}); | |
| 50 | + | window.addEventListener('load', function() { | |
| 51 | + | setTimeout(removeEarlyBg, 100); | |
| 52 | + | }, {once: true}); | |
| 53 | + | } | |
| 54 | + | })();` | |
| 55 | + | ||
| 28 | 56 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | |
| 29 | 57 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | |
| 30 | 58 | // This script: | |
| @@ -132,6 +160,7 @@ type ContentInjector struct { | |||
| 132 | 160 | prefersDark bool | |
| 133 | 161 | themeCSSVars string // CSS custom property declarations for WebUI | |
| 134 | 162 | findCSS string // CSS for find-in-page highlight styling | |
| 163 | + | bgColor string // Background color hex for early injection (prevents white flash) | |
| 135 | 164 | } | |
| 136 | 165 | ||
| 137 | 166 | // NewContentInjector creates a new injector instance. | |
| @@ -171,6 +200,13 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) { | |||
| 171 | 200 | ci.prefersDark = prefersDark | |
| 172 | 201 | } | |
| 173 | 202 | ||
| 203 | + | // SetBackgroundColor sets the background color for early CSS injection. | |
| 204 | + | // This color is applied to html/body at document start to prevent white flash. | |
| 205 | + | // The hex color should include the # prefix (e.g., "#0a0a0b"). | |
| 206 | + | func (ci *ContentInjector) SetBackgroundColor(hex string) { | |
| 207 | + | ci.bgColor = hex | |
| 208 | + | } | |
| 209 | + | ||
| 174 | 210 | // InjectScripts adds the minimal dark mode detection scripts to the given content manager. | |
| 175 | 211 | // Only injects: | |
| 176 | 212 | // - window.__dumber_gtk_prefers_dark flag | |
| @@ -193,7 +229,22 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 193 | 229 | log.Debug().Str("script", label).Msg("injected user script") | |
| 194 | 230 | } | |
| 195 | 231 | ||
| 196 | - | // 1. Inject GTK dark mode preference (must be before dark mode handler) | |
| 232 | + | // 1. Inject early background color to prevent white flash (must be first!) | |
| 233 | + | if ci.bgColor != "" { | |
| 234 | + | earlyBgScript := fmt.Sprintf(earlyBackgroundScript, ci.bgColor) | |
| 235 | + | addScript( | |
| 236 | + | webkit.NewUserScript( | |
| 237 | + | earlyBgScript, | |
| 238 | + | webkit.UserContentInjectTopFrameValue, | |
| 239 | + | webkit.UserScriptInjectAtDocumentStartValue, | |
| 240 | + | nil, | |
| 241 | + | nil, | |
| 242 | + | ), | |
| 243 | + | "early-background", | |
| 244 | + | ) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // 2. Inject GTK dark mode preference (must be before dark mode handler) | |
| 197 | 248 | darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark) | |
| 198 | 249 | addScript( | |
| 199 | 250 | webkit.NewUserScript( | |
| @@ -206,7 +257,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 206 | 257 | "gtk-dark-mode-pref", | |
| 207 | 258 | ) | |
| 208 | 259 | ||
| 209 | - | // 2. Inject WebView ID for debugging | |
| 260 | + | // 3. Inject WebView ID for debugging | |
| 210 | 261 | if webviewID != 0 { | |
| 211 | 262 | idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID)) | |
| 212 | 263 | addScript( | |
| @@ -221,7 +272,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 221 | 272 | ) | |
| 222 | 273 | } | |
| 223 | 274 | ||
| 224 | - | // 3. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark) | |
| 275 | + | // 4. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark) | |
| 225 | 276 | addScript( | |
| 226 | 277 | webkit.NewUserScript( | |
| 227 | 278 | darkModeScript, | |
| @@ -233,7 +284,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 233 | 284 | "dark-mode-handler", | |
| 234 | 285 | ) | |
| 235 | 286 | ||
| 236 | - | // 4. Inject theme CSS for internal pages (dumb://* only) | |
| 287 | + | // 5. Inject theme CSS for internal pages (dumb://* only) | |
| 237 | 288 | if ci.themeCSSVars != "" { | |
| 238 | 289 | // Escape for JS string literal | |
| 239 | 290 | escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\") | |
| @@ -253,7 +304,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 253 | 304 | log.Debug().Msg("theme CSS vars injection configured") | |
| 254 | 305 | } | |
| 255 | 306 | ||
| 256 | - | // 5. Inject find highlight CSS for all pages | |
| 307 | + | // 6. Inject find highlight CSS for all pages | |
| 257 | 308 | if ci.findCSS != "" { | |
| 258 | 309 | stylesheet := webkit.NewUserStyleSheet( | |
| 259 | 310 | ci.findCSS, | |
| @@ -25,6 +25,34 @@ const themeCSSScript = `(function() { | |||||
| 25 | (document.head || document.documentElement).appendChild(style); | 25 | (document.head || document.documentElement).appendChild(style); | ||
| 26 | })();` | 26 | })();` | ||
| 27 | 27 | ||||
| 28 | + | // earlyBackgroundScript injects a temporary background color to prevent white flash. | |||
| 29 | + | // It's injected at document start and removed after the page loads. | |||
| 30 | + | // The %s placeholder is replaced with the hex color (e.g., "#0a0a0b"). | |||
| 31 | + | const earlyBackgroundScript = `(function() { | |||
| 32 | + | var style = document.createElement('style'); | |||
| 33 | + | style.id = '__dumber_early_bg'; | |||
| 34 | + | style.textContent = 'html,body{background-color:%s!important}'; | |||
| 35 | + | document.documentElement.appendChild(style); | |||
| 36 | + | ||||
| 37 | + | // Remove the style after page content loads to avoid overriding page styles | |||
| 38 | + | function removeEarlyBg() { | |||
| 39 | + | var s = document.getElementById('__dumber_early_bg'); | |||
| 40 | + | if (s) s.remove(); | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Try multiple events to ensure removal | |||
| 44 | + | if (document.readyState === 'complete') { | |||
| 45 | + | setTimeout(removeEarlyBg, 100); | |||
| 46 | + | } else { | |||
| 47 | + | window.addEventListener('DOMContentLoaded', function() { | |||
| 48 | + | setTimeout(removeEarlyBg, 100); | |||
| 49 | + | }, {once: true}); | |||
| 50 | + | window.addEventListener('load', function() { | |||
| 51 | + | setTimeout(removeEarlyBg, 100); | |||
| 52 | + | }, {once: true}); | |||
| 53 | + | } | |||
| 54 | + | })();` | |||
| 55 | + | ||||
| 28 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | 56 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | ||
| 29 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | 57 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | ||
| 30 | // This script: | 58 | // This script: | ||
| @@ -132,6 +160,7 @@ type ContentInjector struct { | |||||
| 132 | prefersDark bool | 160 | prefersDark bool | ||
| 133 | themeCSSVars string // CSS custom property declarations for WebUI | 161 | themeCSSVars string // CSS custom property declarations for WebUI | ||
| 134 | findCSS string // CSS for find-in-page highlight styling | 162 | findCSS string // CSS for find-in-page highlight styling | ||
| 163 | + | bgColor string // Background color hex for early injection (prevents white flash) | |||
| 135 | } | 164 | } | ||
| 136 | 165 | ||||
| 137 | // NewContentInjector creates a new injector instance. | 166 | // NewContentInjector creates a new injector instance. | ||
| @@ -171,6 +200,13 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) { | |||||
| 171 | ci.prefersDark = prefersDark | 200 | ci.prefersDark = prefersDark | ||
| 172 | } | 201 | } | ||
| 173 | 202 | ||||
| 203 | + | // SetBackgroundColor sets the background color for early CSS injection. | |||
| 204 | + | // This color is applied to html/body at document start to prevent white flash. | |||
| 205 | + | // The hex color should include the # prefix (e.g., "#0a0a0b"). | |||
| 206 | + | func (ci *ContentInjector) SetBackgroundColor(hex string) { | |||
| 207 | + | ci.bgColor = hex | |||
| 208 | + | } | |||
| 209 | + | ||||
| 174 | // InjectScripts adds the minimal dark mode detection scripts to the given content manager. | 210 | // InjectScripts adds the minimal dark mode detection scripts to the given content manager. | ||
| 175 | // Only injects: | 211 | // Only injects: | ||
| 176 | // - window.__dumber_gtk_prefers_dark flag | 212 | // - window.__dumber_gtk_prefers_dark flag | ||
| @@ -193,7 +229,22 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 193 | log.Debug().Str("script", label).Msg("injected user script") | 229 | log.Debug().Str("script", label).Msg("injected user script") | ||
| 194 | } | 230 | } | ||
| 195 | 231 | ||||
| 196 | - | // 1. Inject GTK dark mode preference (must be before dark mode handler) | 232 | + | // 1. Inject early background color to prevent white flash (must be first!) |
| 233 | + | if ci.bgColor != "" { | |||
| 234 | + | earlyBgScript := fmt.Sprintf(earlyBackgroundScript, ci.bgColor) | |||
| 235 | + | addScript( | |||
| 236 | + | webkit.NewUserScript( | |||
| 237 | + | earlyBgScript, | |||
| 238 | + | webkit.UserContentInjectTopFrameValue, | |||
| 239 | + | webkit.UserScriptInjectAtDocumentStartValue, | |||
| 240 | + | nil, | |||
| 241 | + | nil, | |||
| 242 | + | ), | |||
| 243 | + | "early-background", | |||
| 244 | + | ) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // 2. Inject GTK dark mode preference (must be before dark mode handler) | |||
| 197 | darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark) | 248 | darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark) | ||
| 198 | addScript( | 249 | addScript( | ||
| 199 | webkit.NewUserScript( | 250 | webkit.NewUserScript( | ||
| @@ -206,7 +257,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 206 | "gtk-dark-mode-pref", | 257 | "gtk-dark-mode-pref", | ||
| 207 | ) | 258 | ) | ||
| 208 | 259 | ||||
| 209 | - | // 2. Inject WebView ID for debugging | 260 | + | // 3. Inject WebView ID for debugging |
| 210 | if webviewID != 0 { | 261 | if webviewID != 0 { | ||
| 211 | idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID)) | 262 | idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID)) | ||
| 212 | addScript( | 263 | addScript( | ||
| @@ -221,7 +272,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 221 | ) | 272 | ) | ||
| 222 | } | 273 | } | ||
| 223 | 274 | ||||
| 224 | - | // 3. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark) | 275 | + | // 4. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark) |
| 225 | addScript( | 276 | addScript( | ||
| 226 | webkit.NewUserScript( | 277 | webkit.NewUserScript( | ||
| 227 | darkModeScript, | 278 | darkModeScript, | ||
| @@ -233,7 +284,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 233 | "dark-mode-handler", | 284 | "dark-mode-handler", | ||
| 234 | ) | 285 | ) | ||
| 235 | 286 | ||||
| 236 | - | // 4. Inject theme CSS for internal pages (dumb://* only) | 287 | + | // 5. Inject theme CSS for internal pages (dumb://* only) |
| 237 | if ci.themeCSSVars != "" { | 288 | if ci.themeCSSVars != "" { | ||
| 238 | // Escape for JS string literal | 289 | // Escape for JS string literal | ||
| 239 | escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\") | 290 | escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\") | ||
| @@ -253,7 +304,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 253 | log.Debug().Msg("theme CSS vars injection configured") | 304 | log.Debug().Msg("theme CSS vars injection configured") | ||
| 254 | } | 305 | } | ||
| 255 | 306 | ||||
| 256 | - | // 5. Inject find highlight CSS for all pages | 307 | + | // 6. Inject find highlight CSS for all pages |
| 257 | if ci.findCSS != "" { | 308 | if ci.findCSS != "" { | ||
| 258 | stylesheet := webkit.NewUserStyleSheet( | 309 | stylesheet := webkit.NewUserStyleSheet( | ||
| 259 | ci.findCSS, | 310 | ci.findCSS, | ||
M internal/infrastructure/webkit/pool.go +13 -7
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 11 | 11 | "github.com/bnema/puregotk-webkit/webkit" | |
| 12 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 12 | 13 | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| @@ -147,19 +148,24 @@ func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) { | |||
| 147 | 148 | func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) { | |
| 148 | 149 | log := logging.FromContext(ctx) | |
| 149 | 150 | ||
| 150 | - | wv, err := NewWebView(ctx, p.wkCtx, p.settings) | |
| 151 | - | if err != nil { | |
| 152 | - | return nil, err | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | // Set background color to match theme (eliminates white flash) | |
| 151 | + | // Build background color for immediate application during WebView creation | |
| 156 | 152 | p.bgMu.RLock() | |
| 157 | 153 | r, g, b, a := p.bgR, p.bgG, p.bgB, p.bgA | |
| 158 | 154 | p.bgMu.RUnlock() | |
| 155 | + | ||
| 156 | + | var bgColor *gdk.RGBA | |
| 159 | 157 | if a > 0 { // Only set if a valid color was configured | |
| 160 | - | wv.SetBackgroundColor(r, g, b, a) | |
| 158 | + | bgColor = &gdk.RGBA{Red: r, Green: g, Blue: b, Alpha: a} | |
| 161 | 159 | } | |
| 162 | 160 | ||
| 161 | + | wv, err := NewWebView(ctx, p.wkCtx, p.settings, bgColor) | |
| 162 | + | if err != nil { | |
| 163 | + | return nil, err | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // Add CSS class for theme background styling (prevents white flash) | |
| 167 | + | wv.inner.AddCssClass("webview-themed") | |
| 168 | + | ||
| 163 | 169 | // Keep WebView hidden until content is painted (see content.go:onLoadCommitted) | |
| 164 | 170 | wv.inner.SetVisible(false) | |
| 165 | 171 | ||
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| 11 | "github.com/bnema/puregotk-webkit/webkit" | 11 | "github.com/bnema/puregotk-webkit/webkit" | ||
| 12 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 12 | "github.com/jwijenbergh/puregotk/v4/glib" | 13 | "github.com/jwijenbergh/puregotk/v4/glib" | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| @@ -147,19 +148,24 @@ func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) { | |||||
| 147 | func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) { | 148 | func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) { | ||
| 148 | log := logging.FromContext(ctx) | 149 | log := logging.FromContext(ctx) | ||
| 149 | 150 | ||||
| 150 | - | wv, err := NewWebView(ctx, p.wkCtx, p.settings) | 151 | + | // Build background color for immediate application during WebView creation |
| 151 | - | if err != nil { | |||
| 152 | - | return nil, err | |||
| 153 | - | } | |||
| 154 | - | ||||
| 155 | - | // Set background color to match theme (eliminates white flash) | |||
| 156 | p.bgMu.RLock() | 152 | p.bgMu.RLock() | ||
| 157 | r, g, b, a := p.bgR, p.bgG, p.bgB, p.bgA | 153 | r, g, b, a := p.bgR, p.bgG, p.bgB, p.bgA | ||
| 158 | p.bgMu.RUnlock() | 154 | p.bgMu.RUnlock() | ||
| 155 | + | ||||
| 156 | + | var bgColor *gdk.RGBA | |||
| 159 | if a > 0 { // Only set if a valid color was configured | 157 | if a > 0 { // Only set if a valid color was configured | ||
| 160 | - | wv.SetBackgroundColor(r, g, b, a) | 158 | + | bgColor = &gdk.RGBA{Red: r, Green: g, Blue: b, Alpha: a} |
| 161 | } | 159 | } | ||
| 162 | 160 | ||||
| 161 | + | wv, err := NewWebView(ctx, p.wkCtx, p.settings, bgColor) | |||
| 162 | + | if err != nil { | |||
| 163 | + | return nil, err | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // Add CSS class for theme background styling (prevents white flash) | |||
| 167 | + | wv.inner.AddCssClass("webview-themed") | |||
| 168 | + | ||||
| 163 | // Keep WebView hidden until content is painted (see content.go:onLoadCommitted) | 169 | // Keep WebView hidden until content is painted (see content.go:onLoadCommitted) | ||
| 164 | wv.inner.SetVisible(false) | 170 | wv.inner.SetVisible(false) | ||
| 165 | 171 | ||||
M internal/infrastructure/webkit/webview.go +8 -1
| @@ -240,7 +240,8 @@ func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |||
| 240 | 240 | ||
| 241 | 241 | // NewWebView creates a new WebView with the given context and settings. | |
| 242 | 242 | // Uses the persistent NetworkSession from wkCtx for cookie/data persistence. | |
| 243 | - | func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsManager) (*WebView, error) { | |
| 243 | + | // bgColor is optional - if provided, sets background immediately to prevent white flash. | |
| 244 | + | func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsManager, bgColor *gdk.RGBA) (*WebView, error) { | |
| 244 | 245 | log := logging.FromContext(ctx) | |
| 245 | 246 | ||
| 246 | 247 | if wkCtx == nil || !wkCtx.IsInitialized() { | |
| @@ -253,6 +254,12 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan | |||
| 253 | 254 | return nil, fmt.Errorf("failed to create webkit webview with network session") | |
| 254 | 255 | } | |
| 255 | 256 | ||
| 257 | + | // Set background color IMMEDIATELY after creation, before any other operations. | |
| 258 | + | // This prevents white flash by ensuring WebKit's renderer starts with correct color. | |
| 259 | + | if bgColor != nil { | |
| 260 | + | inner.SetBackgroundColor(bgColor) | |
| 261 | + | } | |
| 262 | + | ||
| 256 | 263 | wv := &WebView{ | |
| 257 | 264 | inner: inner, | |
| 258 | 265 | ucm: inner.GetUserContentManager(), | |
| @@ -240,7 +240,8 @@ func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |||||
| 240 | 240 | ||||
| 241 | // NewWebView creates a new WebView with the given context and settings. | 241 | // NewWebView creates a new WebView with the given context and settings. | ||
| 242 | // Uses the persistent NetworkSession from wkCtx for cookie/data persistence. | 242 | // Uses the persistent NetworkSession from wkCtx for cookie/data persistence. | ||
| 243 | - | func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsManager) (*WebView, error) { | 243 | + | // bgColor is optional - if provided, sets background immediately to prevent white flash. |
| 244 | + | func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsManager, bgColor *gdk.RGBA) (*WebView, error) { | |||
| 244 | log := logging.FromContext(ctx) | 245 | log := logging.FromContext(ctx) | ||
| 245 | 246 | ||||
| 246 | if wkCtx == nil || !wkCtx.IsInitialized() { | 247 | if wkCtx == nil || !wkCtx.IsInitialized() { | ||
| @@ -253,6 +254,12 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan | |||||
| 253 | return nil, fmt.Errorf("failed to create webkit webview with network session") | 254 | return nil, fmt.Errorf("failed to create webkit webview with network session") | ||
| 254 | } | 255 | } | ||
| 255 | 256 | ||||
| 257 | + | // Set background color IMMEDIATELY after creation, before any other operations. | |||
| 258 | + | // This prevents white flash by ensuring WebKit's renderer starts with correct color. | |||
| 259 | + | if bgColor != nil { | |||
| 260 | + | inner.SetBackgroundColor(bgColor) | |||
| 261 | + | } | |||
| 262 | + | ||||
| 256 | wv := &WebView{ | 263 | wv := &WebView{ | ||
| 257 | inner: inner, | 264 | inner: inner, | ||
| 258 | ucm: inner.GetUserContentManager(), | 265 | ucm: inner.GetUserContentManager(), | ||
M internal/ui/theme/css.go +5 -0
| @@ -475,6 +475,11 @@ func generatePaneCSS(p Palette) string { | |||
| 475 | 475 | background-color: var(--bg); | |
| 476 | 476 | } | |
| 477 | 477 | ||
| 478 | + | /* WebView widget - theme background prevents white flash during page load */ | |
| 479 | + | .webview-themed { | |
| 480 | + | background-color: var(--bg); | |
| 481 | + | } | |
| 482 | + | ||
| 478 | 483 | /* Loading skeleton - shown until WebView paints */ | |
| 479 | 484 | .loading-skeleton { | |
| 480 | 485 | background-color: var(--bg); | |
| @@ -475,6 +475,11 @@ func generatePaneCSS(p Palette) string { | |||||
| 475 | background-color: var(--bg); | 475 | background-color: var(--bg); | ||
| 476 | } | 476 | } | ||
| 477 | 477 | ||||
| 478 | + | /* WebView widget - theme background prevents white flash during page load */ | |||
| 479 | + | .webview-themed { | |||
| 480 | + | background-color: var(--bg); | |||
| 481 | + | } | |||
| 482 | + | ||||
| 478 | /* Loading skeleton - shown until WebView paints */ | 483 | /* Loading skeleton - shown until WebView paints */ | ||
| 479 | .loading-skeleton { | 484 | .loading-skeleton { | ||
| 480 | background-color: var(--bg); | 485 | background-color: var(--bg); | ||