detached 237f69b

Merge pull request #318 from bnema/fix/cef-viewport-refit

fix(cef): keep webview viewport synced during resize
commit
237f69bdcd8a4ba5abe08403a83ba012bfaefa3a
author
bnema · · 2026-06-21 08:37 UTC
committer
GitHub · · 2026-06-21 08:37 UTC
merge of
7855cc6 + eb5ee21 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +120 -9
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.1
88 github.com/bnema/purego v0.11.0-bnema.4
99 github.com/bnema/purego-cef v0.13.3
10- github.com/bnema/purego-cef2gtk v0.8.1
10+ github.com/bnema/purego-cef2gtk v0.8.2
1111 github.com/bnema/purego-pipewire v0.1.4
1212 github.com/bnema/purego-sqlite v0.1.3
1313 github.com/bnema/puregotk v0.7.0
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.17 github.com/andybalholm/brotli v1.2.1
8 github.com/bnema/purego v0.11.0-bnema.48 github.com/bnema/purego v0.11.0-bnema.4
9 github.com/bnema/purego-cef v0.13.39 github.com/bnema/purego-cef v0.13.3
10- github.com/bnema/purego-cef2gtk v0.8.110+ github.com/bnema/purego-cef2gtk v0.8.2
11 github.com/bnema/purego-pipewire v0.1.411 github.com/bnema/purego-pipewire v0.1.4
12 github.com/bnema/purego-sqlite v0.1.312 github.com/bnema/purego-sqlite v0.1.3
13 github.com/bnema/puregotk v0.7.013 github.com/bnema/puregotk v0.7.0
M go.sum +2 -2 generated file: collapsed
M internal/infrastructure/cef/webview_viewport.go +22 -5
@@ -94,7 +94,11 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
9494 if wv == nil {
9595 return
9696 }
97- wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason())
97+ reason := wv.takeViewportSyncReason()
98+ ctx = wv.viewportSyncContext(ctx)
99+ if wv.syncViewportNowOnGTK(ctx, reason) {
100+ wv.scheduleResizeRepaintPulse(ctx, reason)
101+ }
98102 }
99103
100104 func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {
@@ -148,6 +152,12 @@ func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string
148152 if wv == nil || wv.destroyed.Load() {
149153 return
150154 }
155+ wv.mu.RLock()
156+ host := wv.host
157+ wv.mu.RUnlock()
158+ if host == nil {
159+ return
160+ }
151161 seq := wv.viewportResizePulseSeq.Add(1)
152162 for _, delayMs := range [...]int64{16, 48} {
153163 task := cefNewTask(cefTaskFunc(func() {
@@ -155,17 +165,17 @@ func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string
155165 return
156166 }
157167 wv.mu.RLock()
158- host := wv.host
168+ currentHost := wv.host
159169 wv.mu.RUnlock()
160- if host == nil {
170+ if currentHost != host {
161171 return
162172 }
163- host.Invalidate(purecef.PaintElementTypePetView)
173+ notifyBrowserViewportResize(host)
164174 logging.FromContext(ctx).Debug().
165175 Uint64("webview_id", uint64(wv.id)).
166176 Int64("delay_ms", delayMs).
167177 Str("reason", reason).
168- Msg("cef: delayed resize repaint pulse")
178+ Msg("cef: delayed resize viewport sync")
169179 }))
170180 if task == nil {
171181 continue
@@ -522,6 +532,13 @@ func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {
522532 if visible {
523533 host.WasHidden(0)
524534 }
535+ notifyBrowserViewportResize(host)
536+}
537+
538+func notifyBrowserViewportResize(host viewportSyncBrowserHost) {
539+ if host == nil {
540+ return
541+ }
525542 host.NotifyScreenInfoChanged()
526543 notifyBrowserResize(host)
527544 }
@@ -94,7 +94,11 @@ func (wv *WebView) syncViewportOnGTK(ctx context.Context) {
94 if wv == nil {94 if wv == nil {
95 return95 return
96 }96 }
97- wv.syncViewportNowOnGTK(ctx, wv.takeViewportSyncReason())97+ reason := wv.takeViewportSyncReason()
98+ ctx = wv.viewportSyncContext(ctx)
99+ if wv.syncViewportNowOnGTK(ctx, reason) {
100+ wv.scheduleResizeRepaintPulse(ctx, reason)
101+ }
98 }102 }
99 103
100 func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {104 func (wv *WebView) syncViewportNowOnGTK(ctx context.Context, reason string) bool {
@@ -148,6 +152,12 @@ func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string
148 if wv == nil || wv.destroyed.Load() {152 if wv == nil || wv.destroyed.Load() {
149 return153 return
150 }154 }
155+ wv.mu.RLock()
156+ host := wv.host
157+ wv.mu.RUnlock()
158+ if host == nil {
159+ return
160+ }
151 seq := wv.viewportResizePulseSeq.Add(1)161 seq := wv.viewportResizePulseSeq.Add(1)
152 for _, delayMs := range [...]int64{16, 48} {162 for _, delayMs := range [...]int64{16, 48} {
153 task := cefNewTask(cefTaskFunc(func() {163 task := cefNewTask(cefTaskFunc(func() {
@@ -155,17 +165,17 @@ func (wv *WebView) scheduleResizeRepaintPulse(ctx context.Context, reason string
155 return165 return
156 }166 }
157 wv.mu.RLock()167 wv.mu.RLock()
158- host := wv.host168+ currentHost := wv.host
159 wv.mu.RUnlock()169 wv.mu.RUnlock()
160- if host == nil {170+ if currentHost != host {
161 return171 return
162 }172 }
163- host.Invalidate(purecef.PaintElementTypePetView)173+ notifyBrowserViewportResize(host)
164 logging.FromContext(ctx).Debug().174 logging.FromContext(ctx).Debug().
165 Uint64("webview_id", uint64(wv.id)).175 Uint64("webview_id", uint64(wv.id)).
166 Int64("delay_ms", delayMs).176 Int64("delay_ms", delayMs).
167 Str("reason", reason).177 Str("reason", reason).
168- Msg("cef: delayed resize repaint pulse")178+ Msg("cef: delayed resize viewport sync")
169 }))179 }))
170 if task == nil {180 if task == nil {
171 continue181 continue
@@ -522,6 +532,13 @@ func notifyBrowserViewportSync(host viewportSyncBrowserHost, visible bool) {
522 if visible {532 if visible {
523 host.WasHidden(0)533 host.WasHidden(0)
524 }534 }
535+ notifyBrowserViewportResize(host)
536+}
537+
538+func notifyBrowserViewportResize(host viewportSyncBrowserHost) {
539+ if host == nil {
540+ return
541+ }
525 host.NotifyScreenInfoChanged()542 host.NotifyScreenInfoChanged()
526 notifyBrowserResize(host)543 notifyBrowserResize(host)
527 }544 }
M internal/infrastructure/cef/webview_viewport_test.go +95 -1
@@ -450,5 +450,99 @@ func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
450450
451451 scheduled[2].Execute()
452452 scheduled[3].Execute()
453- require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls)
453+ require.Equal(t, []string{
454+ "NotifyScreenInfoChanged",
455+ "WasResized",
456+ "Invalidate",
457+ "NotifyScreenInfoChanged",
458+ "WasResized",
459+ "Invalidate",
460+ }, host.calls)
461+}
462+
463+func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
464+ oldNewTask := cefNewTask
465+ oldPostDelayedTask := cefPostDelayedTask
466+ defer func() {
467+ cefNewTask = oldNewTask
468+ cefPostDelayedTask = oldPostDelayedTask
469+ }()
470+
471+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
472+
473+ var scheduled []purecef.Task
474+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
475+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
476+ require.NotNil(t, task)
477+ scheduled = append(scheduled, task)
478+ return 1
479+ }
480+
481+ capturedHost := &viewportSyncOrderHost{}
482+ currentHost := &viewportSyncOrderHost{}
483+ wv := &WebView{ctx: context.Background(), host: capturedHost}
484+
485+ wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
486+ wv.host = currentHost
487+
488+ require.Len(t, scheduled, 2)
489+ scheduled[0].Execute()
490+ scheduled[1].Execute()
491+
492+ require.Empty(t, capturedHost.calls)
493+ require.Empty(t, currentHost.calls)
494+}
495+
496+func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
497+ oldNewTask := cefNewTask
498+ oldPostDelayedTask := cefPostDelayedTask
499+ defer func() {
500+ cefNewTask = oldNewTask
501+ cefPostDelayedTask = oldPostDelayedTask
502+ }()
503+
504+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
505+
506+ var scheduled []purecef.Task
507+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
508+ scheduled = append(scheduled, task)
509+ return 1
510+ }
511+
512+ wv := &WebView{ctx: context.Background()}
513+
514+ wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
515+
516+ require.Empty(t, scheduled)
517+}
518+
519+func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
520+ oldNewTask := cefNewTask
521+ oldPostDelayedTask := cefPostDelayedTask
522+ defer func() {
523+ cefNewTask = oldNewTask
524+ cefPostDelayedTask = oldPostDelayedTask
525+ }()
526+
527+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
528+
529+ var scheduled []purecef.Task
530+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
531+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
532+ require.NotNil(t, task)
533+ scheduled = append(scheduled, task)
534+ return 1
535+ }
536+
537+ host := &viewportSyncOrderHost{}
538+ wv := &WebView{ctx: context.Background(), host: host}
539+
540+ wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
541+ wv.host = nil
542+
543+ require.Len(t, scheduled, 2)
544+ scheduled[0].Execute()
545+ scheduled[1].Execute()
546+
547+ require.Empty(t, host.calls)
454548 }
@@ -450,5 +450,99 @@ func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
450 450
451 scheduled[2].Execute()451 scheduled[2].Execute()
452 scheduled[3].Execute()452 scheduled[3].Execute()
453- require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls)453+ require.Equal(t, []string{
454+ "NotifyScreenInfoChanged",
455+ "WasResized",
456+ "Invalidate",
457+ "NotifyScreenInfoChanged",
458+ "WasResized",
459+ "Invalidate",
460+ }, host.calls)
461+}
462+
463+func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
464+ oldNewTask := cefNewTask
465+ oldPostDelayedTask := cefPostDelayedTask
466+ defer func() {
467+ cefNewTask = oldNewTask
468+ cefPostDelayedTask = oldPostDelayedTask
469+ }()
470+
471+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
472+
473+ var scheduled []purecef.Task
474+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
475+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
476+ require.NotNil(t, task)
477+ scheduled = append(scheduled, task)
478+ return 1
479+ }
480+
481+ capturedHost := &viewportSyncOrderHost{}
482+ currentHost := &viewportSyncOrderHost{}
483+ wv := &WebView{ctx: context.Background(), host: capturedHost}
484+
485+ wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
486+ wv.host = currentHost
487+
488+ require.Len(t, scheduled, 2)
489+ scheduled[0].Execute()
490+ scheduled[1].Execute()
491+
492+ require.Empty(t, capturedHost.calls)
493+ require.Empty(t, currentHost.calls)
494+}
495+
496+func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
497+ oldNewTask := cefNewTask
498+ oldPostDelayedTask := cefPostDelayedTask
499+ defer func() {
500+ cefNewTask = oldNewTask
501+ cefPostDelayedTask = oldPostDelayedTask
502+ }()
503+
504+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
505+
506+ var scheduled []purecef.Task
507+ cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
508+ scheduled = append(scheduled, task)
509+ return 1
510+ }
511+
512+ wv := &WebView{ctx: context.Background()}
513+
514+ wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
515+
516+ require.Empty(t, scheduled)
517+}
518+
519+func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
520+ oldNewTask := cefNewTask
521+ oldPostDelayedTask := cefPostDelayedTask
522+ defer func() {
523+ cefNewTask = oldNewTask
524+ cefPostDelayedTask = oldPostDelayedTask
525+ }()
526+
527+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
528+
529+ var scheduled []purecef.Task
530+ cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
531+ require.Equal(t, purecef.ThreadIDTidUi, threadID)
532+ require.NotNil(t, task)
533+ scheduled = append(scheduled, task)
534+ return 1
535+ }
536+
537+ host := &viewportSyncOrderHost{}
538+ wv := &WebView{ctx: context.Background(), host: host}
539+
540+ wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
541+ wv.host = nil
542+
543+ require.Len(t, scheduled, 2)
544+ scheduled[0].Execute()
545+ scheduled[1].Execute()
546+
547+ require.Empty(t, host.calls)
454 }548 }

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