detached a12a01c

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
111111 viewportSyncReason string
112112 viewportMapFunc func(gtk.Widget)
113113 viewportShowFunc func(gtk.Widget)
114+ viewportHideFunc func(gtk.Widget)
115+ viewportUnmapFunc func(gtk.Widget)
114116 viewportRealizeFunc func(gtk.Widget)
115117 viewportMapSignalID uint
116118 viewportShowSignalID uint
119+ viewportHideSignalID uint
120+ viewportUnmapSignalID uint
117121 viewportRealizeSignalID uint
118122 viewportResizePulseSeq atomic.Uint64
119123 // effectiveVisibility records the last CEF WasHidden state dispatched. It
@@ -111,9 +111,13 @@ type WebView struct {
111 viewportSyncReason string111 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 uint117 viewportMapSignalID uint
116 viewportShowSignalID uint118 viewportShowSignalID uint
119+ viewportHideSignalID uint
120+ viewportUnmapSignalID uint
117 viewportRealizeSignalID uint121 viewportRealizeSignalID uint
118 viewportResizePulseSeq atomic.Uint64122 viewportResizePulseSeq atomic.Uint64
119 // effectiveVisibility records the last CEF WasHidden state dispatched. It123 // 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(
819819 })
820820 }
821821
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.
824824 if h.wv != nil {
825- h.wv.applyEffectiveVisibility(host, true)
826825 if h.wv.ctx != nil {
827826 logging.FromContext(h.wv.ctx).Info().
828827 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 viewport822+ // 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 {
2424 RefreshObservedSizeOnGTKThread() (int32, int32)
2525 }
2626
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+
2759 const (
2860 // pendingBrowserCreateObservedSizeRetryDelay spaces retries while waiting
2961 // 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
129161 }
130162
131163 widget := wv.viewBridge.Widget()
132- visible := widget != nil && widget.IsVisible()
164+ visible := effectiveViewportWidgetVisibility(widget)
133165 wv.notifyViewportSyncOnCEFUIThread(host, visible)
134166 wv.syncZoomForBackingScaleOnCEFUIThread(host, reason)
135167 logging.FromContext(ctx).Debug().
@@ -437,12 +469,20 @@ func (wv *WebView) installViewportSyncHooksOnGTKThread() {
437469 wv.viewportShowFunc = func(gtk.Widget) {
438470 wv.SyncViewport(wv.ctx, "gtk-show")
439471 }
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+ }
440478 wv.viewportRealizeFunc = func(gtk.Widget) {
441479 wv.SyncViewport(wv.ctx, "gtk-realize")
442480 }
443481
444482 wv.viewportMapSignalID = widget.ConnectMap(&wv.viewportMapFunc)
445483 wv.viewportShowSignalID = widget.ConnectShow(&wv.viewportShowFunc)
484+ wv.viewportHideSignalID = widget.ConnectHide(&wv.viewportHideFunc)
485+ wv.viewportUnmapSignalID = widget.ConnectUnmap(&wv.viewportUnmapFunc)
446486 wv.viewportRealizeSignalID = widget.ConnectRealize(&wv.viewportRealizeFunc)
447487 }
448488
@@ -463,6 +503,14 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
463503 gobject.SignalHandlerDisconnect(obj, wv.viewportShowSignalID)
464504 wv.viewportShowSignalID = 0
465505 }
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+ }
466514 if wv.viewportRealizeSignalID != 0 {
467515 gobject.SignalHandlerDisconnect(obj, wv.viewportRealizeSignalID)
468516 wv.viewportRealizeSignalID = 0
@@ -471,6 +519,8 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
471519 }
472520 wv.viewportMapFunc = nil
473521 wv.viewportShowFunc = nil
522+ wv.viewportHideFunc = nil
523+ wv.viewportUnmapFunc = nil
474524 wv.viewportRealizeFunc = nil
475525 }
476526
@@ -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 waiting60 // 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 = 0504 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 = 0516 wv.viewportRealizeSignalID = 0
@@ -471,6 +519,8 @@ func (wv *WebView) disconnectViewportSyncHooksOnGTKThread() {
471 }519 }
472 wv.viewportMapFunc = nil520 wv.viewportMapFunc = nil
473 wv.viewportShowFunc = nil521 wv.viewportShowFunc = nil
522+ wv.viewportHideFunc = nil
523+ wv.viewportUnmapFunc = nil
474 wv.viewportRealizeFunc = nil524 wv.viewportRealizeFunc = nil
475 }525 }
476 526

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