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 if wv.viewBridge == nil {
32 return nil
33 }
34 return wv.viewBridge
35}
36
37// scrollMotionRetired immediately retires animated scroll motion and release
38// eligibility, returning the retired epoch for epoch-scoped GTK cleanup.
39// Safe to call from any thread, including during destruction: a destroyed
40// or absent bridge reports zero, so no destroyed check is needed here.
41func (wv *WebView) scrollMotionRetired() uint64 {
42 target := wv.scrollCancelTarget()
43 if wv == nil || target == nil {
44 return 0
45 }
46 return target.InvalidateScroll()
47}
48
49// cancelScrollEpochOnGTK performs GTK-only cleanup for a retired epoch. It
50// never clears a newer session. Call only on the GTK thread.
51func (wv *WebView) cancelScrollEpochOnGTK(epoch uint64) {
52 if wv == nil || epoch == 0 {
53 return
54 }
55 target := wv.scrollCancelTarget()
56 if target == nil {
57 return
58 }
59 target.CancelScrollEpoch(epoch)
60}
61
62// invalidateScrollMotion retires motion immediately and queues epoch-scoped
63// GTK cleanup through the owning dispatcher. Safe to call from any thread;
64// used by lifecycle callbacks (load handlers, visibility) where navigation
65// dispatch is not part of the call.
66func (wv *WebView) invalidateScrollMotion() uint64 {
67 epoch := wv.scrollMotionRetired()
68 if epoch == 0 {
69 return 0
70 }
71 target := wv.scrollCancelTarget()
72 wv.runOnGTK(func() {
73 if target == nil {
74 return
75 }
76 target.CancelScrollEpoch(epoch)
77 })
78 return epoch
79}
80
81// runNavigationWithScrollCancel invalidates scroll motion immediately, then
82// performs GTK cleanup followed by the existing navigation dispatch within
83// one ordered GTK operation. Calls already on the GTK thread execute inline;
84// off-thread calls use the existing bounded synchronous dispatch, which
85// skips abandoned queued work so a timed-out dispatch can never navigate
86// late. Dispatch errors preserve the command's existing error contract;
87// dispatch failure reports the existing GTK-sync error form.
88func (wv *WebView) runNavigationWithScrollCancel(label string, dispatch func() error) error {
89 if wv.destroyed.Load() {
90 return errDestroyed
91 }
92 epoch := wv.scrollMotionRetired()
93 var dispatchErr error
94 result := wv.runOnGTKSyncLabel(label, func() {
95 wv.cancelScrollEpochOnGTK(epoch)
96 dispatchErr = dispatch()
97 })
98 switch result.Status {
99 case syncdispatch.SyncDispatchInline,
100 syncdispatch.SyncDispatchCompleted,
101 syncdispatch.SyncDispatchCompletedAfterTimeout:
102 return dispatchErr
103 default:
104 return errGTKSyncDispatchIncomplete(label, result)
105 }
106}