fix(cef): derive visibility from mapped GTK hierarchy
- commit
- a12a01ce7cd69209de0b60a82bd41920df92f4a8
- author
- brice · · 2026-07-14 07:17 UTC
- committer
- brice · · 2026-07-14 09:32 UTC
- parent
- 04c1bf8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +57 -4
M internal/infrastructure/cef/webview.go +4 -0
| @@ -111,9 +111,13 @@ type WebView struct { | |||
| 111 | 111 | viewportSyncReason string | |
| 112 | 112 | viewportMapFunc func(gtk.Widget) | |
| 113 | 113 | viewportShowFunc func(gtk.Widget) | |
| 114 | + | viewportHideFunc func(gtk.Widget) | |
| 115 | + | viewportUnmapFunc func(gtk.Widget) | |
| 114 | 116 | viewportRealizeFunc func(gtk.Widget) | |
| 115 | 117 | viewportMapSignalID uint | |
| 116 | 118 | viewportShowSignalID uint | |
| 119 | + | viewportHideSignalID uint | |
| 120 | + | viewportUnmapSignalID uint | |
| 117 | 121 | viewportRealizeSignalID uint | |
| 118 | 122 | viewportResizePulseSeq atomic.Uint64 | |
| 119 | 123 | // effectiveVisibility records the last CEF WasHidden state dispatched. It | |
| @@ -111,9 +111,13 @@ type WebView struct { | |||||
| 111 | viewportSyncReason string | 111 | viewportSyncReason string | ||
| 112 | viewportMapFunc func(gtk.Widget) | 112 | viewportMapFunc func(gtk.Widget) | ||
| 113 | viewportShowFunc func(gtk.Widget) | 113 | viewportShowFunc func(gtk.Widget) | ||
| 114 | + | viewportHideFunc func(gtk.Widget) | |||
| 115 | + | viewportUnmapFunc func(gtk.Widget) | |||
| 114 | viewportRealizeFunc func(gtk.Widget) | 116 | viewportRealizeFunc func(gtk.Widget) | ||
| 115 | viewportMapSignalID uint | 117 | viewportMapSignalID uint | ||
| 116 | viewportShowSignalID uint | 118 | viewportShowSignalID uint | ||
| 119 | + | viewportHideSignalID uint | |||
| 120 | + | viewportUnmapSignalID uint | |||
| 117 | viewportRealizeSignalID uint | 121 | viewportRealizeSignalID uint | ||
| 118 | viewportResizePulseSeq atomic.Uint64 | 122 | viewportResizePulseSeq atomic.Uint64 | ||
| 119 | // effectiveVisibility records the last CEF WasHidden state dispatched. It | 123 | // effectiveVisibility records the last CEF WasHidden state dispatched. It | ||
M internal/infrastructure/cef/webview_handlers.go +2 -3
| @@ -819,10 +819,9 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||
| 819 | 819 | }) | |
| 820 | 820 | } | |
| 821 | 821 | ||
| 822 | - | // CEF OSR starts hidden; record the initial effective state before viewport | |
| 823 | - | // synchronization so the following visible sync is deduplicated. | |
| 822 | + | // CEF OSR starts hidden. SyncViewport reads GTK's mapped, ancestor-aware | |
| 823 | + | // effective state on the main context and reports that truthful initial state. | |
| 824 | 824 | if h.wv != nil { | |
| 825 | - | h.wv.applyEffectiveVisibility(host, true) | |
| 826 | 825 | if h.wv.ctx != nil { | |
| 827 | 826 | logging.FromContext(h.wv.ctx).Info(). | |
| 828 | 827 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | |
| @@ -819,10 +819,9 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||||
| 819 | }) | 819 | }) | ||
| 820 | } | 820 | } | ||
| 821 | 821 | ||||
| 822 | - | // CEF OSR starts hidden; record the initial effective state before viewport | 822 | + | // CEF OSR starts hidden. SyncViewport reads GTK's mapped, ancestor-aware |
| 823 | - | // synchronization so the following visible sync is deduplicated. | 823 | + | // effective state on the main context and reports that truthful initial state. |
| 824 | if h.wv != nil { | 824 | if h.wv != nil { | ||
| 825 | - | h.wv.applyEffectiveVisibility(host, true) | |||
| 826 | if h.wv.ctx != nil { | 825 | if h.wv.ctx != nil { | ||
| 827 | logging.FromContext(h.wv.ctx).Info(). | 826 | logging.FromContext(h.wv.ctx).Info(). | ||
| 828 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | 827 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | ||
M internal/infrastructure/cef/webview_viewport.go +51 -1
| @@ -24,6 +24,38 @@ type pendingBrowserCreateObservedSizeBridge interface { | |||
| 24 | 24 | RefreshObservedSizeOnGTKThread() (int32, int32) | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | + | // viewportWidgetVisibility is the effective GTK state of a widget in its | |
| 28 | + | // hierarchy. Gtk.Widget.IsVisible already includes ancestor visibility; mapped | |
| 29 | + | // is checked separately because a visible widget can still be unmapped. | |
| 30 | + | type viewportWidgetVisibility struct { | |
| 31 | + | visible bool | |
| 32 | + | mapped bool | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func effectiveViewportVisibility(states []viewportWidgetVisibility) bool { | |
| 36 | + | if len(states) == 0 { | |
| 37 | + | return false | |
| 38 | + | } | |
| 39 | + | for _, state := range states { | |
| 40 | + | if !state.visible || !state.mapped { | |
| 41 | + | return false | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | return true | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func effectiveViewportWidgetVisibility(widget *gtk.Widget) bool { | |
| 48 | + | if widget == nil { | |
| 49 | + | return false | |
| 50 | + | } | |
| 51 | + | // IsVisible is GTK's ancestor-aware visibility predicate. Pairing it with | |
| 52 | + | // GetMapped makes hidden/unmapped ancestors effective CEF-hidden states. | |
| 53 | + | return effectiveViewportVisibility([]viewportWidgetVisibility{{ | |
| 54 | + | visible: widget.IsVisible(), | |
| 55 | + | mapped: widget.GetMapped(), | |
| 56 | + | }}) | |
| 57 | + | } | |
| 58 | + | ||
| 27 | 59 | const ( | |
| 28 | 60 | // pendingBrowserCreateObservedSizeRetryDelay spaces retries while waiting | |
| 29 | 61 | // for the GTK bridge to observe the widget's allocated size. | |
| @@ -129,7 +161,7 @@ func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool | |||
| 129 | 161 | } | |
| 130 | 162 | ||
| 131 | 163 | widget := wv.viewBridge.Widget() | |
| 132 | - | visible := widget != nil && widget.IsVisible() | |
| 164 | + | visible := effectiveViewportWidgetVisibility(widget) | |
| 133 | 165 | wv.notifyViewportSyncOnCEFUIThread(host, visible) | |
| 134 | 166 | wv.syncZoomForBackingScaleOnCEFUIThread(host, reason) | |
| 135 | 167 | logging.FromContext(ctx).Debug(). | |
| @@ -437,12 +469,20 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() { | |||
| 437 | 469 | wv.viewportShowFunc = func(gtk.Widget) { | |
| 438 | 470 | wv.SyncViewport(wv.ctx, "gtk-show") | |
| 439 | 471 | } | |
| 472 | + | wv.viewportHideFunc = func(gtk.Widget) { | |
| 473 | + | wv.SyncViewport(wv.ctx, "gtk-hide") | |
| 474 | + | } | |
| 475 | + | wv.viewportUnmapFunc = func(gtk.Widget) { | |
| 476 | + | wv.SyncViewport(wv.ctx, "gtk-unmap") | |
| 477 | + | } | |
| 440 | 478 | wv.viewportRealizeFunc = func(gtk.Widget) { | |
| 441 | 479 | wv.SyncViewport(wv.ctx, "gtk-realize") | |
| 442 | 480 | } | |
| 443 | 481 | ||
| 444 | 482 | wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc) | |
| 445 | 483 | wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc) | |
| 484 | + | wv.viewportHideSignalID = widget.ConnectHide(&wv.viewportHideFunc) | |
| 485 | + | wv.viewportUnmapSignalID = widget.ConnectUnmap(&wv.viewportUnmapFunc) | |
| 446 | 486 | wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc) | |
| 447 | 487 | } | |
| 448 | 488 | ||
| @@ -463,6 +503,14 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||
| 463 | 503 | gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID) | |
| 464 | 504 | wv.viewportShowSignalID = 0 | |
| 465 | 505 | } | |
| 506 | + | if wv.viewportHideSignalID != 0 { | |
| 507 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportHideSignalID) | |
| 508 | + | wv.viewportHideSignalID = 0 | |
| 509 | + | } | |
| 510 | + | if wv.viewportUnmapSignalID != 0 { | |
| 511 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportUnmapSignalID) | |
| 512 | + | wv.viewportUnmapSignalID = 0 | |
| 513 | + | } | |
| 466 | 514 | if wv.viewportRealizeSignalID != 0 { | |
| 467 | 515 | gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID) | |
| 468 | 516 | wv.viewportRealizeSignalID = 0 | |
| @@ -471,6 +519,8 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||
| 471 | 519 | } | |
| 472 | 520 | wv.viewportMapFunc = nil | |
| 473 | 521 | wv.viewportShowFunc = nil | |
| 522 | + | wv.viewportHideFunc = nil | |
| 523 | + | wv.viewportUnmapFunc = nil | |
| 474 | 524 | wv.viewportRealizeFunc = nil | |
| 475 | 525 | } | |
| 476 | 526 | ||
| @@ -24,6 +24,38 @@ type pendingBrowserCreateObservedSizeBridge interface { | |||||
| 24 | RefreshObservedSizeOnGTKThread() (int32, int32) | 24 | RefreshObservedSizeOnGTKThread() (int32, int32) | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | + | // viewportWidgetVisibility is the effective GTK state of a widget in its | |||
| 28 | + | // hierarchy. Gtk.Widget.IsVisible already includes ancestor visibility; mapped | |||
| 29 | + | // is checked separately because a visible widget can still be unmapped. | |||
| 30 | + | type viewportWidgetVisibility struct { | |||
| 31 | + | visible bool | |||
| 32 | + | mapped bool | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func effectiveViewportVisibility(states []viewportWidgetVisibility) bool { | |||
| 36 | + | if len(states) == 0 { | |||
| 37 | + | return false | |||
| 38 | + | } | |||
| 39 | + | for _, state := range states { | |||
| 40 | + | if !state.visible || !state.mapped { | |||
| 41 | + | return false | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | return true | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func effectiveViewportWidgetVisibility(widget *gtk.Widget) bool { | |||
| 48 | + | if widget == nil { | |||
| 49 | + | return false | |||
| 50 | + | } | |||
| 51 | + | // IsVisible is GTK's ancestor-aware visibility predicate. Pairing it with | |||
| 52 | + | // GetMapped makes hidden/unmapped ancestors effective CEF-hidden states. | |||
| 53 | + | return effectiveViewportVisibility([]viewportWidgetVisibility{{ | |||
| 54 | + | visible: widget.IsVisible(), | |||
| 55 | + | mapped: widget.GetMapped(), | |||
| 56 | + | }}) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 27 | const ( | 59 | const ( | ||
| 28 | // pendingBrowserCreateObservedSizeRetryDelay spaces retries while waiting | 60 | // pendingBrowserCreateObservedSizeRetryDelay spaces retries while waiting | ||
| 29 | // for the GTK bridge to observe the widget's allocated size. | 61 | // for the GTK bridge to observe the widget's allocated size. | ||
| @@ -129,7 +161,7 @@ func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool | |||||
| 129 | } | 161 | } | ||
| 130 | 162 | ||||
| 131 | widget := wv.viewBridge.Widget() | 163 | widget := wv.viewBridge.Widget() | ||
| 132 | - | visible := widget != nil && widget.IsVisible() | 164 | + | visible := effectiveViewportWidgetVisibility(widget) |
| 133 | wv.notifyViewportSyncOnCEFUIThread(host, visible) | 165 | wv.notifyViewportSyncOnCEFUIThread(host, visible) | ||
| 134 | wv.syncZoomForBackingScaleOnCEFUIThread(host, reason) | 166 | wv.syncZoomForBackingScaleOnCEFUIThread(host, reason) | ||
| 135 | logging.FromContext(ctx).Debug(). | 167 | logging.FromContext(ctx).Debug(). | ||
| @@ -437,12 +469,20 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() { | |||||
| 437 | wv.viewportShowFunc = func(gtk.Widget) { | 469 | wv.viewportShowFunc = func(gtk.Widget) { | ||
| 438 | wv.SyncViewport(wv.ctx, "gtk-show") | 470 | wv.SyncViewport(wv.ctx, "gtk-show") | ||
| 439 | } | 471 | } | ||
| 472 | + | wv.viewportHideFunc = func(gtk.Widget) { | |||
| 473 | + | wv.SyncViewport(wv.ctx, "gtk-hide") | |||
| 474 | + | } | |||
| 475 | + | wv.viewportUnmapFunc = func(gtk.Widget) { | |||
| 476 | + | wv.SyncViewport(wv.ctx, "gtk-unmap") | |||
| 477 | + | } | |||
| 440 | wv.viewportRealizeFunc = func(gtk.Widget) { | 478 | wv.viewportRealizeFunc = func(gtk.Widget) { | ||
| 441 | wv.SyncViewport(wv.ctx, "gtk-realize") | 479 | wv.SyncViewport(wv.ctx, "gtk-realize") | ||
| 442 | } | 480 | } | ||
| 443 | 481 | ||||
| 444 | wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc) | 482 | wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc) | ||
| 445 | wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc) | 483 | wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc) | ||
| 484 | + | wv.viewportHideSignalID = widget.ConnectHide(&wv.viewportHideFunc) | |||
| 485 | + | wv.viewportUnmapSignalID = widget.ConnectUnmap(&wv.viewportUnmapFunc) | |||
| 446 | wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc) | 486 | wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc) | ||
| 447 | } | 487 | } | ||
| 448 | 488 | ||||
| @@ -463,6 +503,14 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||||
| 463 | gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID) | 503 | gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID) | ||
| 464 | wv.viewportShowSignalID = 0 | 504 | wv.viewportShowSignalID = 0 | ||
| 465 | } | 505 | } | ||
| 506 | + | if wv.viewportHideSignalID != 0 { | |||
| 507 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportHideSignalID) | |||
| 508 | + | wv.viewportHideSignalID = 0 | |||
| 509 | + | } | |||
| 510 | + | if wv.viewportUnmapSignalID != 0 { | |||
| 511 | + | gobject.SignalHandlerDisconnect(obj, wv.viewportUnmapSignalID) | |||
| 512 | + | wv.viewportUnmapSignalID = 0 | |||
| 513 | + | } | |||
| 466 | if wv.viewportRealizeSignalID != 0 { | 514 | if wv.viewportRealizeSignalID != 0 { | ||
| 467 | gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID) | 515 | gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID) | ||
| 468 | wv.viewportRealizeSignalID = 0 | 516 | wv.viewportRealizeSignalID = 0 | ||
| @@ -471,6 +519,8 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() { | |||||
| 471 | } | 519 | } | ||
| 472 | wv.viewportMapFunc = nil | 520 | wv.viewportMapFunc = nil | ||
| 473 | wv.viewportShowFunc = nil | 521 | wv.viewportShowFunc = nil | ||
| 522 | + | wv.viewportHideFunc = nil | |||
| 523 | + | wv.viewportUnmapFunc = nil | |||
| 474 | wv.viewportRealizeFunc = nil | 524 | wv.viewportRealizeFunc = nil | ||
| 475 | } | 525 | } | ||
| 476 | 526 | ||||