webview_scroll_cancel.go RawHistoryFolder
1package cef
2
3import (
4 "github.com/bnema/dumber/internal/shared/syncdispatch"
5)
6
7// This file owns inertial-scroll cancellation ordering for CEF WebViews.
8//
9// Threading contract: scrollMotionRetired is safe from any thread (it only
10// bumps the library epoch and never touches GTK). GTK cleanup runs either
11// inline inside runNavigationWithScrollCancel's ordered GTK operation or via
12// runOnGTK-queued epoch-scoped cleanup. Library imports stay inside this
13// infrastructure boundary; only the Cef2gtkAdapter seam is extended.
14
15// scrollCancelBridge is the narrow adapter seam for scroll cancellation.
16// *Cef2gtkAdapter satisfies it in production; tests inject a recording
17// fake. It covers only invalidation and epoch-scoped cleanup.
18type scrollCancelBridge interface {
19 InvalidateScroll() uint64
20 CancelScrollEpoch(epoch uint64) bool
21}
22
23// scrollCancelTarget resolves the live adapter for scroll cancellation.
24func (wv *WebView) scrollCancelTarget() scrollCancelBridge {
25 if wv == nil {
26 return nil
27 }
28 if wv.scrollCancelSeam != nil {
29 return wv.scrollCancelSeam
30 }
31 // Teardown clears viewBridge under wv.mu from the GTK thread while this
32 // runs from any thread (CEF UI, timers, GTK).
33 wv.mu.RLock()
34 bridge := wv.viewBridge
35 wv.mu.RUnlock()
36 if bridge == nil {
37 return nil
38 }
39 return bridge
40}
41
42// scrollMotionRetired immediately retires animated scroll motion and release
43// eligibility, returning the retired epoch for epoch-scoped GTK cleanup.
44// Safe to call from any thread, including during destruction: a destroyed
45// or absent bridge reports zero, so no destroyed check is needed here.
46func (wv *WebView) scrollMotionRetired() uint64 {
47 target := wv.scrollCancelTarget()
48 if wv == nil || target == nil {
49 return 0
50 }
51 return target.InvalidateScroll()
52}
53
54// cancelScrollEpochOnGTK performs GTK-only cleanup for a retired epoch. It
55// never clears a newer session. Call only on the GTK thread.
56func (wv *WebView) cancelScrollEpochOnGTK(epoch uint64) {
57 if wv == nil || epoch == 0 {
58 return
59 }
60 target := wv.scrollCancelTarget()
61 if target == nil {
62 return
63 }
64 target.CancelScrollEpoch(epoch)
65}
66
67// invalidateScrollMotion retires motion immediately and queues epoch-scoped
68// GTK cleanup through the owning dispatcher. Safe to call from any thread;
69// used by lifecycle callbacks (load handlers, visibility) where navigation
70// dispatch is not part of the call.
71func (wv *WebView) invalidateScrollMotion() uint64 {
72 epoch := wv.scrollMotionRetired()
73 if epoch == 0 {
74 return 0
75 }
76 target := wv.scrollCancelTarget()
77 wv.runOnGTK(func() {
78 if target == nil {
79 return
80 }
81 target.CancelScrollEpoch(epoch)
82 })
83 return epoch
84}
85
86// runNavigationWithScrollCancel invalidates scroll motion immediately, then
87// performs GTK cleanup followed by the existing navigation dispatch within
88// one ordered GTK operation. Calls already on the GTK thread execute inline;
89// off-thread calls use the existing bounded synchronous dispatch, which
90// skips abandoned queued work so a timed-out dispatch can never navigate
91// late. Dispatch errors preserve the command's existing error contract;
92// dispatch failure reports the existing GTK-sync error form.
93func (wv *WebView) runNavigationWithScrollCancel(label string, dispatch func() error) error {
94 if wv.destroyed.Load() {
95 return errDestroyed
96 }
97 epoch := wv.scrollMotionRetired()
98 var dispatchErr error
99 result := wv.runOnGTKSyncLabel(label, func() {
100 wv.cancelScrollEpochOnGTK(epoch)
101 dispatchErr = dispatch()
102 })
103 switch result.Status {
104 case syncdispatch.SyncDispatchInline,
105 syncdispatch.SyncDispatchCompleted,
106 syncdispatch.SyncDispatchCompletedAfterTimeout:
107 return dispatchErr
108 default:
109 return errGTKSyncDispatchIncomplete(label, result)
110 }
111}