detached ddb81a0

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(
9595 settings := webkit.NewSettingsManager(ctx, cfg)
9696 injector := webkit.NewContentInjector(themeManager.PrefersDark())
9797
98+ // Set background color on injector for early CSS injection (prevents white flash)
99+ injector.SetBackgroundColor(themeManager.GetCurrentPalette().Background)
100+
98101 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
99102 themeCSSText := themeManager.GetWebUIThemeCSS()
100103 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 (
77
88 "github.com/bnema/dumber/internal/application/port"
99 "github.com/bnema/dumber/internal/logging"
10+ "github.com/jwijenbergh/puregotk/v4/gdk"
1011 )
1112
1213 // WebViewFactory creates WebView instances for the application.
@@ -138,19 +139,24 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
138139 func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) {
139140 log := logging.FromContext(ctx)
140141
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
147143 f.bgMu.RLock()
148144 r, g, b, a := f.bgR, f.bgG, f.bgB, f.bgA
149145 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
152155 }
153156
157+ // Add CSS class for theme background styling (prevents white flash)
158+ wv.inner.AddCssClass("webview-themed")
159+
154160 // Keep hidden until content is painted
155161 wv.inner.SetVisible(false)
156162
@@ -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.bgA144 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 painted160 // 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() {
2525 (document.head || document.documentElement).appendChild(style);
2626 })();`
2727
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+
2856 // darkModeScript patches window.matchMedia and sets theme class on <html>.
2957 // It must be injected at document start, after __dumber_gtk_prefers_dark is set.
3058 // This script:
@@ -132,6 +160,7 @@ type ContentInjector struct {
132160 prefersDark bool
133161 themeCSSVars string // CSS custom property declarations for WebUI
134162 findCSS string // CSS for find-in-page highlight styling
163+ bgColor string // Background color hex for early injection (prevents white flash)
135164 }
136165
137166 // NewContentInjector creates a new injector instance.
@@ -171,6 +200,13 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) {
171200 ci.prefersDark = prefersDark
172201 }
173202
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+
174210 // InjectScripts adds the minimal dark mode detection scripts to the given content manager.
175211 // Only injects:
176212 // - window.__dumber_gtk_prefers_dark flag
@@ -193,7 +229,22 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
193229 log.Debug().Str("script", label).Msg("injected user script")
194230 }
195231
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)
197248 darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
198249 addScript(
199250 webkit.NewUserScript(
@@ -206,7 +257,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
206257 "gtk-dark-mode-pref",
207258 )
208259
209- // 2. Inject WebView ID for debugging
260+ // 3. Inject WebView ID for debugging
210261 if webviewID != 0 {
211262 idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID))
212263 addScript(
@@ -221,7 +272,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
221272 )
222273 }
223274
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)
225276 addScript(
226277 webkit.NewUserScript(
227278 darkModeScript,
@@ -233,7 +284,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
233284 "dark-mode-handler",
234285 )
235286
236- // 4. Inject theme CSS for internal pages (dumb://* only)
287+ // 5. Inject theme CSS for internal pages (dumb://* only)
237288 if ci.themeCSSVars != "" {
238289 // Escape for JS string literal
239290 escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\")
@@ -253,7 +304,7 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
253304 log.Debug().Msg("theme CSS vars injection configured")
254305 }
255306
256- // 5. Inject find highlight CSS for all pages
307+ // 6. Inject find highlight CSS for all pages
257308 if ci.findCSS != "" {
258309 stylesheet := webkit.NewUserStyleSheet(
259310 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 bool160 prefersDark bool
133 themeCSSVars string // CSS custom property declarations for WebUI161 themeCSSVars string // CSS custom property declarations for WebUI
134 findCSS string // CSS for find-in-page highlight styling162 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 = prefersDark200 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 flag212 // - 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 debugging260+ // 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 literal289 // 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 pages307+ // 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 (
99
1010 "github.com/bnema/dumber/internal/logging"
1111 "github.com/bnema/puregotk-webkit/webkit"
12+ "github.com/jwijenbergh/puregotk/v4/gdk"
1213 "github.com/jwijenbergh/puregotk/v4/glib"
1314 )
1415
@@ -147,19 +148,24 @@ func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) {
147148 func (p *WebViewPool) createWebView(ctx context.Context) (*WebView, error) {
148149 log := logging.FromContext(ctx)
149150
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
156152 p.bgMu.RLock()
157153 r, g, b, a := p.bgR, p.bgG, p.bgB, p.bgA
158154 p.bgMu.RUnlock()
155+
156+ var bgColor *gdk.RGBA
159157 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}
161159 }
162160
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+
163169 // Keep WebView hidden until content is painted (see content.go:onLoadCommitted)
164170 wv.inner.SetVisible(false)
165171
@@ -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.bgA153 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 configured157 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) {
240240
241241 // NewWebView creates a new WebView with the given context and settings.
242242 // 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) {
244245 log := logging.FromContext(ctx)
245246
246247 if wkCtx == nil || !wkCtx.IsInitialized() {
@@ -253,6 +254,12 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan
253254 return nil, fmt.Errorf("failed to create webkit webview with network session")
254255 }
255256
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+
256263 wv := &WebView{
257264 inner: inner,
258265 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 {
475475 background-color: var(--bg);
476476 }
477477
478+/* WebView widget - theme background prevents white flash during page load */
479+.webview-themed {
480+ background-color: var(--bg);
481+}
482+
478483 /* Loading skeleton - shown until WebView paints */
479484 .loading-skeleton {
480485 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);

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close