fix(cef): synchronize effective visibility transitions
- commit
- 55dbfae6c0c68efd715ba4dccafb850175e5ed2d
- author
- brice · · 2026-07-14 06:18 UTC
- committer
- brice · · 2026-07-14 07:06 UTC
- parent
- d49b626
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +31 -4
M internal/infrastructure/cef/webview.go +4 -0
| @@ -112,6 +112,10 @@ type WebView struct { | |||
| 112 | 112 | viewportShowSignalID uint | |
| 113 | 113 | viewportRealizeSignalID uint | |
| 114 | 114 | viewportResizePulseSeq atomic.Uint64 | |
| 115 | + | // effectiveVisibility records the last CEF WasHidden state dispatched. It | |
| 116 | + | // is guarded by mu because GTK and CEF UI work cross threads. | |
| 117 | + | effectiveVisibilityKnown bool | |
| 118 | + | effectiveVisible bool | |
| 115 | 119 | ||
| 116 | 120 | adaptiveWindowlessFrameRate bool | |
| 117 | 121 | windowlessFrameRateMax int32 | |
| @@ -112,6 +112,10 @@ type WebView struct { | |||||
| 112 | viewportShowSignalID uint | 112 | viewportShowSignalID uint | ||
| 113 | viewportRealizeSignalID uint | 113 | viewportRealizeSignalID uint | ||
| 114 | viewportResizePulseSeq atomic.Uint64 | 114 | viewportResizePulseSeq atomic.Uint64 | ||
| 115 | + | // effectiveVisibility records the last CEF WasHidden state dispatched. It | |||
| 116 | + | // is guarded by mu because GTK and CEF UI work cross threads. | |||
| 117 | + | effectiveVisibilityKnown bool | |||
| 118 | + | effectiveVisible bool | |||
| 115 | 119 | ||||
| 116 | adaptiveWindowlessFrameRate bool | 120 | adaptiveWindowlessFrameRate bool | ||
| 117 | windowlessFrameRateMax int32 | 121 | windowlessFrameRateMax int32 | ||
M internal/infrastructure/cef/webview_handlers.go +3 -3
| @@ -819,10 +819,10 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||
| 819 | 819 | }) | |
| 820 | 820 | } | |
| 821 | 821 | ||
| 822 | - | // Mark browser as visible — CEF OSR starts in hidden state and suppresses | |
| 823 | - | // painting/caret updates until explicitly told the browser is shown. | |
| 824 | - | host.WasHidden(0) | |
| 822 | + | // CEF OSR starts hidden; record the initial effective state before viewport | |
| 823 | + | // synchronization so the following visible sync is deduplicated. | |
| 825 | 824 | if h.wv != nil { | |
| 825 | + | h.wv.applyEffectiveVisibility(host, true) | |
| 826 | 826 | if h.wv.ctx != nil { | |
| 827 | 827 | logging.FromContext(h.wv.ctx).Info(). | |
| 828 | 828 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | |
| @@ -819,10 +819,10 @@ func (h *handlerSet) attachAfterCreatedBrowser( | |||||
| 819 | }) | 819 | }) | ||
| 820 | } | 820 | } | ||
| 821 | 821 | ||||
| 822 | - | // Mark browser as visible — CEF OSR starts in hidden state and suppresses | 822 | + | // CEF OSR starts hidden; record the initial effective state before viewport |
| 823 | - | // painting/caret updates until explicitly told the browser is shown. | 823 | + | // synchronization so the following visible sync is deduplicated. |
| 824 | - | host.WasHidden(0) | |||
| 825 | if h.wv != nil { | 824 | if h.wv != nil { | ||
| 825 | + | h.wv.applyEffectiveVisibility(host, true) | |||
| 826 | if h.wv.ctx != nil { | 826 | if h.wv.ctx != nil { | ||
| 827 | logging.FromContext(h.wv.ctx).Info(). | 827 | logging.FromContext(h.wv.ctx).Info(). | ||
| 828 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | 828 | Int32("windowless_frame_rate_configured", host.GetWindowlessFrameRate()). | ||
M internal/infrastructure/cef/webview_viewport.go +24 -1
| @@ -488,7 +488,8 @@ func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, | |||
| 488 | 488 | if currentHost != host { | |
| 489 | 489 | return | |
| 490 | 490 | } | |
| 491 | - | notifyBrowserViewportSync(host, visible) | |
| 491 | + | wv.applyEffectiveVisibility(host, visible) | |
| 492 | + | notifyBrowserViewportResize(host) | |
| 492 | 493 | })) | |
| 493 | 494 | if task == nil { | |
| 494 | 495 | return | |
| @@ -526,6 +527,28 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost | |||
| 526 | 527 | cefPostDelayedTask(purecef.ThreadIDTidUi, task, 0) | |
| 527 | 528 | } | |
| 528 | 529 | ||
| 530 | + | // applyEffectiveVisibility reports the initial effective visibility and later | |
| 531 | + | // transitions exactly once. CEF OSR begins hidden, so even an initially hidden | |
| 532 | + | // GTK widget must be explicitly reported to keep both lifecycles aligned. | |
| 533 | + | func (wv *WebView) applyEffectiveVisibility(host viewportSyncBrowserHost, visible bool) { | |
| 534 | + | if wv == nil || host == nil || wv.destroyed.Load() { | |
| 535 | + | return | |
| 536 | + | } | |
| 537 | + | wv.mu.Lock() | |
| 538 | + | if wv.effectiveVisibilityKnown && wv.effectiveVisible == visible { | |
| 539 | + | wv.mu.Unlock() | |
| 540 | + | return | |
| 541 | + | } | |
| 542 | + | wv.effectiveVisibilityKnown = true | |
| 543 | + | wv.effectiveVisible = visible | |
| 544 | + | wv.mu.Unlock() | |
| 545 | + | if visible { | |
| 546 | + | host.WasHidden(0) | |
| 547 | + | return | |
| 548 | + | } | |
| 549 | + | host.WasHidden(1) | |
| 550 | + | } | |
| 551 | + | ||
| 529 | 552 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | |
| 530 | 553 | if host == nil { | |
| 531 | 554 | return | |
| @@ -488,7 +488,8 @@ func (wv *WebView) notifyViewportSyncOnCEFUIThread(host viewportSyncBrowserHost, | |||||
| 488 | if currentHost != host { | 488 | if currentHost != host { | ||
| 489 | return | 489 | return | ||
| 490 | } | 490 | } | ||
| 491 | - | notifyBrowserViewportSync(host, visible) | 491 | + | wv.applyEffectiveVisibility(host, visible) |
| 492 | + | notifyBrowserViewportResize(host) | |||
| 492 | })) | 493 | })) | ||
| 493 | if task == nil { | 494 | if task == nil { | ||
| 494 | return | 495 | return | ||
| @@ -526,6 +527,28 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost | |||||
| 526 | cefPostDelayedTask(purecef.ThreadIDTidUi, task, 0) | 527 | cefPostDelayedTask(purecef.ThreadIDTidUi, task, 0) | ||
| 527 | } | 528 | } | ||
| 528 | 529 | ||||
| 530 | + | // applyEffectiveVisibility reports the initial effective visibility and later | |||
| 531 | + | // transitions exactly once. CEF OSR begins hidden, so even an initially hidden | |||
| 532 | + | // GTK widget must be explicitly reported to keep both lifecycles aligned. | |||
| 533 | + | func (wv *WebView) applyEffectiveVisibility(host viewportSyncBrowserHost, visible bool) { | |||
| 534 | + | if wv == nil || host == nil || wv.destroyed.Load() { | |||
| 535 | + | return | |||
| 536 | + | } | |||
| 537 | + | wv.mu.Lock() | |||
| 538 | + | if wv.effectiveVisibilityKnown && wv.effectiveVisible == visible { | |||
| 539 | + | wv.mu.Unlock() | |||
| 540 | + | return | |||
| 541 | + | } | |||
| 542 | + | wv.effectiveVisibilityKnown = true | |||
| 543 | + | wv.effectiveVisible = visible | |||
| 544 | + | wv.mu.Unlock() | |||
| 545 | + | if visible { | |||
| 546 | + | host.WasHidden(0) | |||
| 547 | + | return | |||
| 548 | + | } | |||
| 549 | + | host.WasHidden(1) | |||
| 550 | + | } | |||
| 551 | + | ||||
| 529 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | 552 | func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) { | ||
| 530 | if host == nil { | 553 | if host == nil { | ||
| 531 | return | 554 | return | ||