Merge pull request #358 from bnema/fix/cef-canvas-background
fix(cef): keep browser canvas white, move anti-flash to presentation layer
80 files · +1985 -2004
M go.mod +1 -1
| @@ -6,7 +6,7 @@ require ( | |||
| 6 | 6 | github.com/a-h/templ v0.3.1020 | |
| 7 | 7 | github.com/andybalholm/brotli v1.2.2 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.4 | |
| 9 | - | github.com/bnema/purego-cef v0.13.4 | |
| 9 | + | github.com/bnema/purego-cef v0.14.1 | |
| 10 | 10 | github.com/bnema/purego-cef2gtk v0.8.5 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.5 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.4 | |
| @@ -6,7 +6,7 @@ require ( | |||||
| 6 | github.com/a-h/templ v0.3.1020 | 6 | github.com/a-h/templ v0.3.1020 | ||
| 7 | github.com/andybalholm/brotli v1.2.2 | 7 | github.com/andybalholm/brotli v1.2.2 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.4 | 8 | github.com/bnema/purego v0.11.0-bnema.4 | ||
| 9 | - | github.com/bnema/purego-cef v0.13.4 | 9 | + | github.com/bnema/purego-cef v0.14.1 |
| 10 | github.com/bnema/purego-cef2gtk v0.8.5 | 10 | github.com/bnema/purego-cef2gtk v0.8.5 | ||
| 11 | github.com/bnema/purego-pipewire v0.1.5 | 11 | github.com/bnema/purego-pipewire v0.1.5 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.4 | 12 | github.com/bnema/purego-sqlite v0.1.4 | ||
M go.sum +2 -2 generated file: collapsed
A internal/infrastructure/cef/background_guard.go +106 -0
| @@ -0,0 +1,106 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/puregotk/v4/gdk" | |
| 7 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // opaqueWhiteBackground is Chromium's standard windowless canvas fallback. It is | |
| 11 | + | // baked into every BrowserSettings.BackgroundColor so pages that paint nothing | |
| 12 | + | // get an opaque white canvas (never a theme-dark one). A zero value must not be | |
| 13 | + | // used: in windowless/OSR mode a zero-alpha background enables transparent | |
| 14 | + | // painting, which this engine does not want. | |
| 15 | + | const opaqueWhiteBackground uint32 = 0xFFFFFFFF | |
| 16 | + | ||
| 17 | + | // flashGuardCSSClassPrefix scopes each WebView's transient background provider to | |
| 18 | + | // its own render widget so guards from different tabs never collide. | |
| 19 | + | const flashGuardCSSClassPrefix = "dumber-cef-flash-guard-" | |
| 20 | + | ||
| 21 | + | // colorByteMask isolates a single 8-bit channel from a packed ARGB value. | |
| 22 | + | const colorByteMask = 0xFF | |
| 23 | + | ||
| 24 | + | // applyBackgroundFlashGuard paints the theme background color behind the CEF | |
| 25 | + | // render widget until the first frame is presented. It is the presentation-layer | |
| 26 | + | // replacement for the former dark BrowserSettings.BackgroundColor: Chromium's | |
| 27 | + | // canvas is now always opaque white, so the anti-white-flash lives here instead. | |
| 28 | + | // The bridge widget (a GtkPicture/GtkGLArea) renders its CSS background until an | |
| 29 | + | // opaque frame texture covers it, at which point markFirstFramePainted tears the | |
| 30 | + | // guard down. Must run on the GTK main thread. | |
| 31 | + | func (wv *WebView) applyBackgroundFlashGuard(widget *gtk.Widget) { | |
| 32 | + | if wv == nil || widget == nil { | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | argb := wv.backgroundColor | |
| 36 | + | if argb == 0 { | |
| 37 | + | return // No theme color configured; nothing to guard against. | |
| 38 | + | } | |
| 39 | + | display := gdk.DisplayGetDefault() | |
| 40 | + | if display == nil { | |
| 41 | + | return | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | a := float64((argb>>24)&colorByteMask) / colorScale | |
| 45 | + | r := (argb >> 16) & colorByteMask | |
| 46 | + | g := (argb >> 8) & colorByteMask | |
| 47 | + | b := argb & colorByteMask | |
| 48 | + | class := fmt.Sprintf("%s%d", flashGuardCSSClassPrefix, uint64(wv.id)) | |
| 49 | + | css := fmt.Sprintf(".%s { background-color: rgba(%d, %d, %d, %.4f); }", class, r, g, b, a) | |
| 50 | + | ||
| 51 | + | provider := gtk.NewCssProvider() | |
| 52 | + | if provider == nil { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | provider.LoadFromString(css) | |
| 56 | + | ||
| 57 | + | wv.bgGuardMu.Lock() | |
| 58 | + | defer wv.bgGuardMu.Unlock() | |
| 59 | + | if wv.firstFramePainted.Load() { | |
| 60 | + | return // First frame already arrived before the guard could be installed. | |
| 61 | + | } | |
| 62 | + | widget.AddCssClass(class) | |
| 63 | + | gtk.StyleContextAddProviderForDisplay(display, provider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)) | |
| 64 | + | wv.bgGuardProvider = provider | |
| 65 | + | wv.bgGuardWidget = widget | |
| 66 | + | wv.bgGuardClass = class | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // markFirstFramePainted records that the first CEF frame has been presented and | |
| 70 | + | // schedules teardown of the background flash guard so later frames render | |
| 71 | + | // normally. Safe to call from any thread and idempotent. | |
| 72 | + | func (wv *WebView) markFirstFramePainted() { | |
| 73 | + | if wv == nil || !wv.firstFramePainted.CompareAndSwap(false, true) { | |
| 74 | + | return | |
| 75 | + | } | |
| 76 | + | wv.runOnGTK(func() { | |
| 77 | + | wv.removeBackgroundFlashGuard() | |
| 78 | + | }) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // removeBackgroundFlashGuard detaches the transient theme-background provider and | |
| 82 | + | // clears the render widget's guard class. Must run on the GTK main thread. It is | |
| 83 | + | // safe to call when no guard is installed. | |
| 84 | + | func (wv *WebView) removeBackgroundFlashGuard() { | |
| 85 | + | if wv == nil { | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | wv.bgGuardMu.Lock() | |
| 89 | + | provider := wv.bgGuardProvider | |
| 90 | + | widget := wv.bgGuardWidget | |
| 91 | + | class := wv.bgGuardClass | |
| 92 | + | wv.bgGuardProvider = nil | |
| 93 | + | wv.bgGuardWidget = nil | |
| 94 | + | wv.bgGuardClass = "" | |
| 95 | + | wv.bgGuardMu.Unlock() | |
| 96 | + | ||
| 97 | + | if provider == nil { | |
| 98 | + | return | |
| 99 | + | } | |
| 100 | + | if widget != nil && class != "" { | |
| 101 | + | widget.RemoveCssClass(class) | |
| 102 | + | } | |
| 103 | + | if display := gdk.DisplayGetDefault(); display != nil { | |
| 104 | + | gtk.StyleContextRemoveProviderForDisplay(display, provider) | |
| 105 | + | } | |
| 106 | + | } | |
| @@ -0,0 +1,106 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/puregotk/v4/gdk" | |||
| 7 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // opaqueWhiteBackground is Chromium's standard windowless canvas fallback. It is | |||
| 11 | + | // baked into every BrowserSettings.BackgroundColor so pages that paint nothing | |||
| 12 | + | // get an opaque white canvas (never a theme-dark one). A zero value must not be | |||
| 13 | + | // used: in windowless/OSR mode a zero-alpha background enables transparent | |||
| 14 | + | // painting, which this engine does not want. | |||
| 15 | + | const opaqueWhiteBackground uint32 = 0xFFFFFFFF | |||
| 16 | + | ||||
| 17 | + | // flashGuardCSSClassPrefix scopes each WebView's transient background provider to | |||
| 18 | + | // its own render widget so guards from different tabs never collide. | |||
| 19 | + | const flashGuardCSSClassPrefix = "dumber-cef-flash-guard-" | |||
| 20 | + | ||||
| 21 | + | // colorByteMask isolates a single 8-bit channel from a packed ARGB value. | |||
| 22 | + | const colorByteMask = 0xFF | |||
| 23 | + | ||||
| 24 | + | // applyBackgroundFlashGuard paints the theme background color behind the CEF | |||
| 25 | + | // render widget until the first frame is presented. It is the presentation-layer | |||
| 26 | + | // replacement for the former dark BrowserSettings.BackgroundColor: Chromium's | |||
| 27 | + | // canvas is now always opaque white, so the anti-white-flash lives here instead. | |||
| 28 | + | // The bridge widget (a GtkPicture/GtkGLArea) renders its CSS background until an | |||
| 29 | + | // opaque frame texture covers it, at which point markFirstFramePainted tears the | |||
| 30 | + | // guard down. Must run on the GTK main thread. | |||
| 31 | + | func (wv *WebView) applyBackgroundFlashGuard(widget *gtk.Widget) { | |||
| 32 | + | if wv == nil || widget == nil { | |||
| 33 | + | return | |||
| 34 | + | } | |||
| 35 | + | argb := wv.backgroundColor | |||
| 36 | + | if argb == 0 { | |||
| 37 | + | return // No theme color configured; nothing to guard against. | |||
| 38 | + | } | |||
| 39 | + | display := gdk.DisplayGetDefault() | |||
| 40 | + | if display == nil { | |||
| 41 | + | return | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | a := float64((argb>>24)&colorByteMask) / colorScale | |||
| 45 | + | r := (argb >> 16) & colorByteMask | |||
| 46 | + | g := (argb >> 8) & colorByteMask | |||
| 47 | + | b := argb & colorByteMask | |||
| 48 | + | class := fmt.Sprintf("%s%d", flashGuardCSSClassPrefix, uint64(wv.id)) | |||
| 49 | + | css := fmt.Sprintf(".%s { background-color: rgba(%d, %d, %d, %.4f); }", class, r, g, b, a) | |||
| 50 | + | ||||
| 51 | + | provider := gtk.NewCssProvider() | |||
| 52 | + | if provider == nil { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | provider.LoadFromString(css) | |||
| 56 | + | ||||
| 57 | + | wv.bgGuardMu.Lock() | |||
| 58 | + | defer wv.bgGuardMu.Unlock() | |||
| 59 | + | if wv.firstFramePainted.Load() { | |||
| 60 | + | return // First frame already arrived before the guard could be installed. | |||
| 61 | + | } | |||
| 62 | + | widget.AddCssClass(class) | |||
| 63 | + | gtk.StyleContextAddProviderForDisplay(display, provider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)) | |||
| 64 | + | wv.bgGuardProvider = provider | |||
| 65 | + | wv.bgGuardWidget = widget | |||
| 66 | + | wv.bgGuardClass = class | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // markFirstFramePainted records that the first CEF frame has been presented and | |||
| 70 | + | // schedules teardown of the background flash guard so later frames render | |||
| 71 | + | // normally. Safe to call from any thread and idempotent. | |||
| 72 | + | func (wv *WebView) markFirstFramePainted() { | |||
| 73 | + | if wv == nil || !wv.firstFramePainted.CompareAndSwap(false, true) { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | wv.runOnGTK(func() { | |||
| 77 | + | wv.removeBackgroundFlashGuard() | |||
| 78 | + | }) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // removeBackgroundFlashGuard detaches the transient theme-background provider and | |||
| 82 | + | // clears the render widget's guard class. Must run on the GTK main thread. It is | |||
| 83 | + | // safe to call when no guard is installed. | |||
| 84 | + | func (wv *WebView) removeBackgroundFlashGuard() { | |||
| 85 | + | if wv == nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | wv.bgGuardMu.Lock() | |||
| 89 | + | provider := wv.bgGuardProvider | |||
| 90 | + | widget := wv.bgGuardWidget | |||
| 91 | + | class := wv.bgGuardClass | |||
| 92 | + | wv.bgGuardProvider = nil | |||
| 93 | + | wv.bgGuardWidget = nil | |||
| 94 | + | wv.bgGuardClass = "" | |||
| 95 | + | wv.bgGuardMu.Unlock() | |||
| 96 | + | ||||
| 97 | + | if provider == nil { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | if widget != nil && class != "" { | |||
| 101 | + | widget.RemoveCssClass(class) | |||
| 102 | + | } | |||
| 103 | + | if display := gdk.DisplayGetDefault(); display != nil { | |||
| 104 | + | gtk.StyleContextRemoveProviderForDisplay(display, provider) | |||
| 105 | + | } | |||
| 106 | + | } | |||
M internal/infrastructure/cef/factory.go +12 -8
| @@ -27,7 +27,7 @@ type WebViewFactory struct { | |||
| 27 | 27 | windowlessFrameRate int32 | |
| 28 | 28 | windowlessFrameRateMax int32 | |
| 29 | 29 | inputConfig RuntimeInputConfig | |
| 30 | - | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | |
| 30 | + | bgColor atomic.Uint32 // packed ARGB theme color for the presentation-layer flash guard | |
| 31 | 31 | audioOutputFactory port.AudioOutputFactory | |
| 32 | 32 | } | |
| 33 | 33 | ||
| @@ -69,7 +69,9 @@ func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFacto | |||
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | - | // setDefaultBackgroundColor stores a packed ARGB color for new browser creation. | |
| 72 | + | // setDefaultBackgroundColor stores the packed ARGB theme color used by the | |
| 73 | + | // presentation-layer flash guard on newly-created WebViews. It no longer feeds | |
| 74 | + | // BrowserSettings.BackgroundColor, which is always opaque white. | |
| 73 | 75 | func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) { | |
| 74 | 76 | const s = colorScale | |
| 75 | 77 | argb := uint32(a*s)<<24 | uint32(r*s)<<16 | uint32(g*s)<<8 | uint32(b*s) | |
| @@ -96,9 +98,9 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||
| 96 | 98 | settings := purecef.NewBrowserSettings() | |
| 97 | 99 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | |
| 98 | 100 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | |
| 99 | - | if bg := f.bgColor.Load(); bg != 0 { | |
| 100 | - | settings.BackgroundColor = bg | |
| 101 | - | } | |
| 101 | + | // Always opaque white: Chromium's standard canvas fallback. The theme | |
| 102 | + | // background is applied at the presentation layer as a flash guard instead. | |
| 103 | + | settings.BackgroundColor = opaqueWhiteBackground | |
| 102 | 104 | ||
| 103 | 105 | // Defer browser creation until the bridge view has a non-zero size. | |
| 104 | 106 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | |
| @@ -142,6 +144,8 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) { | |||
| 142 | 144 | wv.nativeWidget = popupRoot | |
| 143 | 145 | } | |
| 144 | 146 | } | |
| 147 | + | // Guard the render widget with the theme background until the first frame. | |
| 148 | + | wv.applyBackgroundFlashGuard(nativeWidget) | |
| 145 | 149 | }) | |
| 146 | 150 | if !result.Completed() { | |
| 147 | 151 | wv.destroyViewBridgeOnGTKAsync() | |
| @@ -383,9 +387,9 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||
| 383 | 387 | settings := purecef.NewBrowserSettings() | |
| 384 | 388 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | |
| 385 | 389 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | |
| 386 | - | if bg := f.bgColor.Load(); bg != 0 { | |
| 387 | - | settings.BackgroundColor = bg | |
| 388 | - | } | |
| 390 | + | // Always opaque white: the theme background is applied as a presentation-layer | |
| 391 | + | // flash guard, not baked into Chromium's canvas. | |
| 392 | + | settings.BackgroundColor = opaqueWhiteBackground | |
| 389 | 393 | ||
| 390 | 394 | if err := f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | |
| 391 | 395 | f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) { | |
| @@ -27,7 +27,7 @@ type WebViewFactory struct { | |||||
| 27 | windowlessFrameRate int32 | 27 | windowlessFrameRate int32 | ||
| 28 | windowlessFrameRateMax int32 | 28 | windowlessFrameRateMax int32 | ||
| 29 | inputConfig RuntimeInputConfig | 29 | inputConfig RuntimeInputConfig | ||
| 30 | - | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | 30 | + | bgColor atomic.Uint32 // packed ARGB theme color for the presentation-layer flash guard |
| 31 | audioOutputFactory port.AudioOutputFactory | 31 | audioOutputFactory port.AudioOutputFactory | ||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| @@ -69,7 +69,9 @@ func newWebViewFactory(engine *Engine, opts webViewFactoryOptions) *WebViewFacto | |||||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | - | // setDefaultBackgroundColor stores a packed ARGB color for new browser creation. | 72 | + | // setDefaultBackgroundColor stores the packed ARGB theme color used by the |
| 73 | + | // presentation-layer flash guard on newly-created WebViews. It no longer feeds | |||
| 74 | + | // BrowserSettings.BackgroundColor, which is always opaque white. | |||
| 73 | func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) { | 75 | func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) { | ||
| 74 | const s = colorScale | 76 | const s = colorScale | ||
| 75 | argb := uint32(a*s)<<24 | uint32(r*s)<<16 | uint32(g*s)<<8 | uint32(b*s) | 77 | argb := uint32(a*s)<<24 | uint32(r*s)<<16 | uint32(g*s)<<8 | uint32(b*s) | ||
| @@ -96,9 +98,9 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||||
| 96 | settings := purecef.NewBrowserSettings() | 98 | settings := purecef.NewBrowserSettings() | ||
| 97 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | 99 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | ||
| 98 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | 100 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | ||
| 99 | - | if bg := f.bgColor.Load(); bg != 0 { | 101 | + | // Always opaque white: Chromium's standard canvas fallback. The theme |
| 100 | - | settings.BackgroundColor = bg | 102 | + | // background is applied at the presentation layer as a flash guard instead. |
| 101 | - | } | 103 | + | settings.BackgroundColor = opaqueWhiteBackground |
| 102 | 104 | ||||
| 103 | // Defer browser creation until the bridge view has a non-zero size. | 105 | // Defer browser creation until the bridge view has a non-zero size. | ||
| 104 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | 106 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | ||
| @@ -142,6 +144,8 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) { | |||||
| 142 | wv.nativeWidget = popupRoot | 144 | wv.nativeWidget = popupRoot | ||
| 143 | } | 145 | } | ||
| 144 | } | 146 | } | ||
| 147 | + | // Guard the render widget with the theme background until the first frame. | |||
| 148 | + | wv.applyBackgroundFlashGuard(nativeWidget) | |||
| 145 | }) | 149 | }) | ||
| 146 | if !result.Completed() { | 150 | if !result.Completed() { | ||
| 147 | wv.destroyViewBridgeOnGTKAsync() | 151 | wv.destroyViewBridgeOnGTKAsync() | ||
| @@ -383,9 +387,9 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie | |||||
| 383 | settings := purecef.NewBrowserSettings() | 387 | settings := purecef.NewBrowserSettings() | ||
| 384 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | 388 | cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate}) | ||
| 385 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | 389 | settings.LocalStorage = 1 // CEF_STATE_ENABLED | ||
| 386 | - | if bg := f.bgColor.Load(); bg != 0 { | 390 | + | // Always opaque white: the theme background is applied as a presentation-layer |
| 387 | - | settings.BackgroundColor = bg | 391 | + | // flash guard, not baked into Chromium's canvas. |
| 388 | - | } | 392 | + | settings.BackgroundColor = opaqueWhiteBackground |
| 389 | 393 | ||||
| 390 | if err := f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | 394 | if err := f.configureInitialBrowserCreation(ctx, popupWV, popupWV.client, &windowInfo, &settings, func(w, h int32) { | ||
| 391 | f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) { | 395 | f.handlePopupShellInitialResize(ctx, popupWV, func(w, h int32) { | ||
M internal/infrastructure/cef/render_handler_adapter.go +9 -0
| @@ -51,6 +51,15 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | |||
| 51 | 51 | ||
| 52 | 52 | var unsupportedPaintOnce sync.Once | |
| 53 | 53 | hooks := startupPresentationHooks(activeStartupTrace()) | |
| 54 | + | // Tear down the theme background flash guard once the first frame is | |
| 55 | + | // presented, without dropping the startup trace mark the bridge already set. | |
| 56 | + | firstAcceleratedPaint := hooks.OnFirstAcceleratedPaint | |
| 57 | + | hooks.OnFirstAcceleratedPaint = func() { | |
| 58 | + | if firstAcceleratedPaint != nil { | |
| 59 | + | firstAcceleratedPaint() | |
| 60 | + | } | |
| 61 | + | wv.markFirstFramePainted() | |
| 62 | + | } | |
| 54 | 63 | hooks.OnUnsupportedPaint = func() { | |
| 55 | 64 | unsupportedPaintOnce.Do(func() { | |
| 56 | 65 | if wv.ctx != nil { | |
| @@ -51,6 +51,15 @@ func newDumberRenderHandler(wv *WebView) purecef.RenderHandler { | |||||
| 51 | 51 | ||||
| 52 | var unsupportedPaintOnce sync.Once | 52 | var unsupportedPaintOnce sync.Once | ||
| 53 | hooks := startupPresentationHooks(activeStartupTrace()) | 53 | hooks := startupPresentationHooks(activeStartupTrace()) | ||
| 54 | + | // Tear down the theme background flash guard once the first frame is | |||
| 55 | + | // presented, without dropping the startup trace mark the bridge already set. | |||
| 56 | + | firstAcceleratedPaint := hooks.OnFirstAcceleratedPaint | |||
| 57 | + | hooks.OnFirstAcceleratedPaint = func() { | |||
| 58 | + | if firstAcceleratedPaint != nil { | |||
| 59 | + | firstAcceleratedPaint() | |||
| 60 | + | } | |||
| 61 | + | wv.markFirstFramePainted() | |||
| 62 | + | } | |||
| 54 | hooks.OnUnsupportedPaint = func() { | 63 | hooks.OnUnsupportedPaint = func() { | ||
| 55 | unsupportedPaintOnce.Do(func() { | 64 | unsupportedPaintOnce.Do(func() { | ||
| 56 | if wv.ctx != nil { | 65 | if wv.ctx != nil { | ||
M internal/infrastructure/cef/webview.go +20 -11
| @@ -239,6 +239,18 @@ type WebView struct { | |||
| 239 | 239 | backgroundColor uint32 | |
| 240 | 240 | inputConfig RuntimeInputConfig | |
| 241 | 241 | ||
| 242 | + | // Background flash guard: a theme-colored CSS provider painted behind the | |
| 243 | + | // render widget until the first CEF frame is presented. It replaces the | |
| 244 | + | // former dark BrowserSettings.BackgroundColor anti-white-flash, whose canvas | |
| 245 | + | // is now always opaque white. State is guarded by bgGuardMu; firstFramePainted | |
| 246 | + | // gates the one-shot teardown and resets naturally when a fresh WebView is | |
| 247 | + | // created after a browser recreation. | |
| 248 | + | firstFramePainted atomic.Bool | |
| 249 | + | bgGuardMu sync.Mutex | |
| 250 | + | bgGuardProvider *gtk.CssProvider | |
| 251 | + | bgGuardWidget *gtk.Widget | |
| 252 | + | bgGuardClass string | |
| 253 | + | ||
| 242 | 254 | // Touchpad input diagnostics are debounced to avoid flooding logs while | |
| 243 | 255 | // debugging continuous scroll streams. | |
| 244 | 256 | touchpadNavigation *touchpadNavigationRecognizer | |
| @@ -992,7 +1004,7 @@ func (wv *WebView) prepareNativePopup( | |||
| 992 | 1004 | prep.parent.trackPendingNativePopup(popupID, wv) | |
| 993 | 1005 | } | |
| 994 | 1006 | ||
| 995 | - | configureNativePopupWindow(windowInfo, settings, prep.frameRate, prep.backgroundColor) | |
| 1007 | + | configureNativePopupWindow(windowInfo, settings, prep.frameRate) | |
| 996 | 1008 | clientSlot.Set(client) | |
| 997 | 1009 | return true | |
| 998 | 1010 | } | |
| @@ -1007,9 +1019,8 @@ func (wv *WebView) nativePopupPreparationSnapshot() (nativePopupPreparation, boo | |||
| 1007 | 1019 | return nativePopupPreparation{}, false | |
| 1008 | 1020 | } | |
| 1009 | 1021 | return nativePopupPreparation{ | |
| 1010 | - | parent: wv.nativePopupParent, | |
| 1011 | - | frameRate: wv.windowlessFrameRate, | |
| 1012 | - | backgroundColor: wv.backgroundColor, | |
| 1022 | + | parent: wv.nativePopupParent, | |
| 1023 | + | frameRate: wv.windowlessFrameRate, | |
| 1013 | 1024 | }, true | |
| 1014 | 1025 | } | |
| 1015 | 1026 | ||
| @@ -1045,7 +1056,6 @@ func configureNativePopupWindow( | |||
| 1045 | 1056 | windowInfo *purecef.WindowInfo, | |
| 1046 | 1057 | settings *purecef.BrowserSettings, | |
| 1047 | 1058 | frameRate int32, | |
| 1048 | - | backgroundColor uint32, | |
| 1049 | 1059 | ) { | |
| 1050 | 1060 | cef2gtk.ConfigureWindowInfo(windowInfo, cef2gtk.WindowInfoOptions{}) | |
| 1051 | 1061 | if externalBeginFrameEnabled() { | |
| @@ -1056,15 +1066,13 @@ func configureNativePopupWindow( | |||
| 1056 | 1066 | } | |
| 1057 | 1067 | cef2gtk.ConfigureBrowserSettings(settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: frameRate}) | |
| 1058 | 1068 | settings.LocalStorage = 1 | |
| 1059 | - | if backgroundColor != 0 { | |
| 1060 | - | settings.BackgroundColor = backgroundColor | |
| 1061 | - | } | |
| 1069 | + | // Always opaque white; the popup shell WebView carries the theme flash guard. | |
| 1070 | + | settings.BackgroundColor = opaqueWhiteBackground | |
| 1062 | 1071 | } | |
| 1063 | 1072 | ||
| 1064 | 1073 | type nativePopupPreparation struct { | |
| 1065 | - | parent *WebView | |
| 1066 | - | frameRate int32 | |
| 1067 | - | backgroundColor uint32 | |
| 1074 | + | parent *WebView | |
| 1075 | + | frameRate int32 | |
| 1068 | 1076 | } | |
| 1069 | 1077 | ||
| 1070 | 1078 | func (wv *WebView) handleNativePopupAborted() { | |
| @@ -1244,6 +1252,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||
| 1244 | 1252 | if wv == nil || wv.viewBridge == nil { | |
| 1245 | 1253 | return | |
| 1246 | 1254 | } | |
| 1255 | + | wv.removeBackgroundFlashGuard() | |
| 1247 | 1256 | if wv.removeSizeObserver != nil { | |
| 1248 | 1257 | wv.removeSizeObserver() | |
| 1249 | 1258 | wv.removeSizeObserver = nil | |
| @@ -239,6 +239,18 @@ type WebView struct { | |||||
| 239 | backgroundColor uint32 | 239 | backgroundColor uint32 | ||
| 240 | inputConfig RuntimeInputConfig | 240 | inputConfig RuntimeInputConfig | ||
| 241 | 241 | ||||
| 242 | + | // Background flash guard: a theme-colored CSS provider painted behind the | |||
| 243 | + | // render widget until the first CEF frame is presented. It replaces the | |||
| 244 | + | // former dark BrowserSettings.BackgroundColor anti-white-flash, whose canvas | |||
| 245 | + | // is now always opaque white. State is guarded by bgGuardMu; firstFramePainted | |||
| 246 | + | // gates the one-shot teardown and resets naturally when a fresh WebView is | |||
| 247 | + | // created after a browser recreation. | |||
| 248 | + | firstFramePainted atomic.Bool | |||
| 249 | + | bgGuardMu sync.Mutex | |||
| 250 | + | bgGuardProvider *gtk.CssProvider | |||
| 251 | + | bgGuardWidget *gtk.Widget | |||
| 252 | + | bgGuardClass string | |||
| 253 | + | ||||
| 242 | // Touchpad input diagnostics are debounced to avoid flooding logs while | 254 | // Touchpad input diagnostics are debounced to avoid flooding logs while | ||
| 243 | // debugging continuous scroll streams. | 255 | // debugging continuous scroll streams. | ||
| 244 | touchpadNavigation *touchpadNavigationRecognizer | 256 | touchpadNavigation *touchpadNavigationRecognizer | ||
| @@ -992,7 +1004,7 @@ func (wv *WebView) prepareNativePopup( | |||||
| 992 | prep.parent.trackPendingNativePopup(popupID, wv) | 1004 | prep.parent.trackPendingNativePopup(popupID, wv) | ||
| 993 | } | 1005 | } | ||
| 994 | 1006 | ||||
| 995 | - | configureNativePopupWindow(windowInfo, settings, prep.frameRate, prep.backgroundColor) | 1007 | + | configureNativePopupWindow(windowInfo, settings, prep.frameRate) |
| 996 | clientSlot.Set(client) | 1008 | clientSlot.Set(client) | ||
| 997 | return true | 1009 | return true | ||
| 998 | } | 1010 | } | ||
| @@ -1007,9 +1019,8 @@ func (wv *WebView) nativePopupPreparationSnapshot() (nativePopupPreparation, boo | |||||
| 1007 | return nativePopupPreparation{}, false | 1019 | return nativePopupPreparation{}, false | ||
| 1008 | } | 1020 | } | ||
| 1009 | return nativePopupPreparation{ | 1021 | return nativePopupPreparation{ | ||
| 1010 | - | parent: wv.nativePopupParent, | 1022 | + | parent: wv.nativePopupParent, |
| 1011 | - | frameRate: wv.windowlessFrameRate, | 1023 | + | frameRate: wv.windowlessFrameRate, |
| 1012 | - | backgroundColor: wv.backgroundColor, | |||
| 1013 | }, true | 1024 | }, true | ||
| 1014 | } | 1025 | } | ||
| 1015 | 1026 | ||||
| @@ -1045,7 +1056,6 @@ func configureNativePopupWindow( | |||||
| 1045 | windowInfo *purecef.WindowInfo, | 1056 | windowInfo *purecef.WindowInfo, | ||
| 1046 | settings *purecef.BrowserSettings, | 1057 | settings *purecef.BrowserSettings, | ||
| 1047 | frameRate int32, | 1058 | frameRate int32, | ||
| 1048 | - | backgroundColor uint32, | |||
| 1049 | ) { | 1059 | ) { | ||
| 1050 | cef2gtk.ConfigureWindowInfo(windowInfo, cef2gtk.WindowInfoOptions{}) | 1060 | cef2gtk.ConfigureWindowInfo(windowInfo, cef2gtk.WindowInfoOptions{}) | ||
| 1051 | if externalBeginFrameEnabled() { | 1061 | if externalBeginFrameEnabled() { | ||
| @@ -1056,15 +1066,13 @@ func configureNativePopupWindow( | |||||
| 1056 | } | 1066 | } | ||
| 1057 | cef2gtk.ConfigureBrowserSettings(settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: frameRate}) | 1067 | cef2gtk.ConfigureBrowserSettings(settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: frameRate}) | ||
| 1058 | settings.LocalStorage = 1 | 1068 | settings.LocalStorage = 1 | ||
| 1059 | - | if backgroundColor != 0 { | 1069 | + | // Always opaque white; the popup shell WebView carries the theme flash guard. |
| 1060 | - | settings.BackgroundColor = backgroundColor | 1070 | + | settings.BackgroundColor = opaqueWhiteBackground |
| 1061 | - | } | |||
| 1062 | } | 1071 | } | ||
| 1063 | 1072 | ||||
| 1064 | type nativePopupPreparation struct { | 1073 | type nativePopupPreparation struct { | ||
| 1065 | - | parent *WebView | 1074 | + | parent *WebView |
| 1066 | - | frameRate int32 | 1075 | + | frameRate int32 |
| 1067 | - | backgroundColor uint32 | |||
| 1068 | } | 1076 | } | ||
| 1069 | 1077 | ||||
| 1070 | func (wv *WebView) handleNativePopupAborted() { | 1078 | func (wv *WebView) handleNativePopupAborted() { | ||
| @@ -1244,6 +1252,7 @@ func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||||
| 1244 | if wv == nil || wv.viewBridge == nil { | 1252 | if wv == nil || wv.viewBridge == nil { | ||
| 1245 | return | 1253 | return | ||
| 1246 | } | 1254 | } | ||
| 1255 | + | wv.removeBackgroundFlashGuard() | |||
| 1247 | if wv.removeSizeObserver != nil { | 1256 | if wv.removeSizeObserver != nil { | ||
| 1248 | wv.removeSizeObserver() | 1257 | wv.removeSizeObserver() | ||
| 1249 | wv.removeSizeObserver = nil | 1258 | wv.removeSizeObserver = nil | ||
M internal/infrastructure/cef/webview_handlers_test.go +2 -2
| @@ -380,14 +380,14 @@ func TestConfigureNativePopupWindow_UsesSharedTextureWindowlessDefaults(t *testi | |||
| 380 | 380 | windowInfo := purecef.NewWindowInfo() | |
| 381 | 381 | settings := purecef.NewBrowserSettings() | |
| 382 | 382 | ||
| 383 | - | configureNativePopupWindow(&windowInfo, &settings, 72, 0xFF112233) | |
| 383 | + | configureNativePopupWindow(&windowInfo, &settings, 72) | |
| 384 | 384 | ||
| 385 | 385 | require.Equal(t, purecef.WindowHandle(0), windowInfo.ParentWindow) | |
| 386 | 386 | require.Equal(t, int32(1), windowInfo.WindowlessRenderingEnabled) | |
| 387 | 387 | require.Equal(t, int32(1), windowInfo.SharedTextureEnabled) | |
| 388 | 388 | require.Equal(t, int32(72), settings.WindowlessFrameRate) | |
| 389 | 389 | require.Equal(t, int32(1), settings.LocalStorage) | |
| 390 | - | require.Equal(t, uint32(0xFF112233), settings.BackgroundColor) | |
| 390 | + | require.Equal(t, opaqueWhiteBackground, settings.BackgroundColor) | |
| 391 | 391 | } | |
| 392 | 392 | ||
| 393 | 393 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | |
| @@ -380,14 +380,14 @@ func TestConfigureNativePopupWindow_UsesSharedTextureWindowlessDefaults(t *testi | |||||
| 380 | windowInfo := purecef.NewWindowInfo() | 380 | windowInfo := purecef.NewWindowInfo() | ||
| 381 | settings := purecef.NewBrowserSettings() | 381 | settings := purecef.NewBrowserSettings() | ||
| 382 | 382 | ||||
| 383 | - | configureNativePopupWindow(&windowInfo, &settings, 72, 0xFF112233) | 383 | + | configureNativePopupWindow(&windowInfo, &settings, 72) |
| 384 | 384 | ||||
| 385 | require.Equal(t, purecef.WindowHandle(0), windowInfo.ParentWindow) | 385 | require.Equal(t, purecef.WindowHandle(0), windowInfo.ParentWindow) | ||
| 386 | require.Equal(t, int32(1), windowInfo.WindowlessRenderingEnabled) | 386 | require.Equal(t, int32(1), windowInfo.WindowlessRenderingEnabled) | ||
| 387 | require.Equal(t, int32(1), windowInfo.SharedTextureEnabled) | 387 | require.Equal(t, int32(1), windowInfo.SharedTextureEnabled) | ||
| 388 | require.Equal(t, int32(72), settings.WindowlessFrameRate) | 388 | require.Equal(t, int32(72), settings.WindowlessFrameRate) | ||
| 389 | require.Equal(t, int32(1), settings.LocalStorage) | 389 | require.Equal(t, int32(1), settings.LocalStorage) | ||
| 390 | - | require.Equal(t, uint32(0xFF112233), settings.BackgroundColor) | 390 | + | require.Equal(t, opaqueWhiteBackground, settings.BackgroundColor) |
| 391 | } | 391 | } | ||
| 392 | 392 | ||||
| 393 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | 393 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | ||
M vendor/github.com/bnema/purego-cef/cef/cef_browser_gen.go +15 -2 generated file: collapsed
M vendor/github.com/bnema/purego-cef/cef/cef_command_line_gen.go +15 -2 generated file: collapsed
A vendor/github.com/bnema/purego-cef/cef/cef_component_updater_gen.go +323 -0 generated file: collapsed
M vendor/github.com/bnema/purego-cef/cef/cef_cookie_gen.go +14 -1 generated file: collapsed
M vendor/github.com/bnema/purego-cef/cef/cef_drag_data_gen.go +14 -1 generated file: collapsed
M vendor/github.com/bnema/purego-cef/cef/cef_image_gen.go +14 -1
| @@ -221,8 +221,21 @@ func wrapImage(ptr unsafe.Pointer) Image { | |||
| 221 | 221 | return impl | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | + | // takeImage adopts a CEF Image pointer whose reference is already owned by | |
| 225 | + | // the caller (as returned by a global factory function). Unlike wrapImage it | |
| 226 | + | // does NOT call AddRef, because the C API already transferred one reference to us. | |
| 227 | + | func takeImage(ptr unsafe.Pointer) Image { | |
| 228 | + | if ptr == nil { | |
| 229 | + | return nil | |
| 230 | + | } | |
| 231 | + | r := (*capi.CEFImageT)(ptr) | |
| 232 | + | impl := &imageImpl{rawPtr: r} | |
| 233 | + | runtime.SetFinalizer(impl, (*imageImpl).Release) | |
| 234 | + | return impl | |
| 235 | + | } | |
| 236 | + | ||
| 224 | 237 | // ImageCreate Create a new cef_image_t. It will initially be NULL. Use the Add*() functions to add representations at different scale factors. | |
| 225 | 238 | func ImageCreate() Image { | |
| 226 | 239 | ret := capi.CEFImageCreate() | |
| 227 | - | return wrapImage(ret) | |
| 240 | + | return takeImage(ret) | |
| 228 | 241 | } | |
| @@ -221,8 +221,21 @@ func wrapImage(ptr unsafe.Pointer) Image { | |||||
| 221 | return impl | 221 | return impl | ||
| 222 | } | 222 | } | ||
| 223 | 223 | ||||
| 224 | + | // takeImage adopts a CEF Image pointer whose reference is already owned by | |||
| 225 | + | // the caller (as returned by a global factory function). Unlike wrapImage it | |||
| 226 | + | // does NOT call AddRef, because the C API already transferred one reference to us. | |||
| 227 | + | func takeImage(ptr unsafe.Pointer) Image { | |||
| 228 | + | if ptr == nil { | |||
| 229 | + | return nil | |||
| 230 | + | } | |||
| 231 | + | r := (*capi.CEFImageT)(ptr) | |||
| 232 | + | impl := &imageImpl{rawPtr: r} | |||
| 233 | + | runtime.SetFinalizer(impl, (*imageImpl).Release) | |||
| 234 | + | return impl | |||
| 235 | + | } | |||
| 236 | + | ||||
| 224 | // ImageCreate Create a new cef_image_t. It will initially be NULL. Use the Add*() functions to add representations at different scale factors. | 237 | // ImageCreate Create a new cef_image_t. It will initially be NULL. Use the Add*() functions to add representations at different scale factors. | ||
| 225 | func ImageCreate() Image { | 238 | func ImageCreate() Image { | ||
| 226 | ret := capi.CEFImageCreate() | 239 | ret := capi.CEFImageCreate() | ||
| 227 | - | return wrapImage(ret) | 240 | + | return takeImage(ret) |
| 228 | } | 241 | } | ||