detached 0d3b8ed

fix(cef): address touchpad navigation review feedback

commit
0d3b8ed9c553b94413c028f8b11b18d09f4f5288
author
brice · · 2026-06-18 09:05 UTC
committer
brice · · 2026-06-18 09:51 UTC
parent
34fd6d3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +354 -63
M internal/application/port/webview.go +20 -4
@@ -87,24 +87,40 @@ type Texture interface {
8787 GoPointer() uintptr
8888 }
8989
90-// WebViewCallbacks defines callback handlers for WebView events.
91-// Implementations should invoke these on the main thread/goroutine.
90+// TouchpadNavigationAction indicates the direction of a touchpad navigation gesture.
9291 type TouchpadNavigationAction int
9392
9493 const (
94+ // TouchpadNavigationBack indicates a backward navigation gesture.
9595 TouchpadNavigationBack TouchpadNavigationAction = iota
96+ // TouchpadNavigationForward indicates a forward navigation gesture.
9697 TouchpadNavigationForward
9798 )
9899
100+// String returns a human-readable representation of the navigation action.
101+func (a TouchpadNavigationAction) String() string {
102+ switch a {
103+ case TouchpadNavigationBack:
104+ return "back"
105+ case TouchpadNavigationForward:
106+ return "forward"
107+ default:
108+ return "unknown"
109+ }
110+}
111+
99112 // TouchpadNavigationGesture describes visual progress for a deliberate
100113 // two-finger history navigation gesture.
101114 type TouchpadNavigationGesture struct {
102- Action TouchpadNavigationAction
115+ Action TouchpadNavigationAction
116+ // Progress is normalized gesture progress from 0.0 to 1.0.
103117 Progress float64
104118 ThresholdReached bool
105119 Active bool
106120 }
107121
122+// WebViewCallbacks defines callback handlers for WebView events.
123+// Implementations should invoke these on the main thread/goroutine.
108124 type WebViewCallbacks struct {
109125 // OnLoadChanged is called when load state changes.
110126 OnLoadChanged func(event LoadEvent)
@@ -153,7 +169,7 @@ type WebViewCallbacks struct {
153169
154170 // OnAudioStateChanged is called when audio playback starts or stops.
155171 OnAudioStateChanged func(playing bool)
156- // OnTouchpadNavigationGesture is called while a CEF two-finger history
172+ // OnTouchpadNavigationGesture is called while a two-finger touchpad history
157173 // navigation gesture is progressing or finishing.
158174 OnTouchpadNavigationGesture func(gesture TouchpadNavigationGesture)
159175 }
@@ -87,24 +87,40 @@ type Texture interface {
87 GoPointer() uintptr87 GoPointer() uintptr
88 }88 }
89 89
90-// WebViewCallbacks defines callback handlers for WebView events.90+// TouchpadNavigationAction indicates the direction of a touchpad navigation gesture.
91-// Implementations should invoke these on the main thread/goroutine.
92 type TouchpadNavigationAction int91 type TouchpadNavigationAction int
93 92
94 const (93 const (
94+ // TouchpadNavigationBack indicates a backward navigation gesture.
95 TouchpadNavigationBack TouchpadNavigationAction = iota95 TouchpadNavigationBack TouchpadNavigationAction = iota
96+ // TouchpadNavigationForward indicates a forward navigation gesture.
96 TouchpadNavigationForward97 TouchpadNavigationForward
97 )98 )
98 99
100+// String returns a human-readable representation of the navigation action.
101+func (a TouchpadNavigationAction) String() string {
102+ switch a {
103+ case TouchpadNavigationBack:
104+ return "back"
105+ case TouchpadNavigationForward:
106+ return "forward"
107+ default:
108+ return "unknown"
109+ }
110+}
111+
99 // TouchpadNavigationGesture describes visual progress for a deliberate112 // TouchpadNavigationGesture describes visual progress for a deliberate
100 // two-finger history navigation gesture.113 // two-finger history navigation gesture.
101 type TouchpadNavigationGesture struct {114 type TouchpadNavigationGesture struct {
102- Action TouchpadNavigationAction115+ Action TouchpadNavigationAction
116+ // Progress is normalized gesture progress from 0.0 to 1.0.
103 Progress float64117 Progress float64
104 ThresholdReached bool118 ThresholdReached bool
105 Active bool119 Active bool
106 }120 }
107 121
122+// WebViewCallbacks defines callback handlers for WebView events.
123+// Implementations should invoke these on the main thread/goroutine.
108 type WebViewCallbacks struct {124 type WebViewCallbacks struct {
109 // OnLoadChanged is called when load state changes.125 // OnLoadChanged is called when load state changes.
110 OnLoadChanged func(event LoadEvent)126 OnLoadChanged func(event LoadEvent)
@@ -153,7 +169,7 @@ type WebViewCallbacks struct {
153 169
154 // OnAudioStateChanged is called when audio playback starts or stops.170 // OnAudioStateChanged is called when audio playback starts or stops.
155 OnAudioStateChanged func(playing bool)171 OnAudioStateChanged func(playing bool)
156- // OnTouchpadNavigationGesture is called while a CEF two-finger history172+ // OnTouchpadNavigationGesture is called while a two-finger touchpad history
157 // navigation gesture is progressing or finishing.173 // navigation gesture is progressing or finishing.
158 OnTouchpadNavigationGesture func(gesture TouchpadNavigationGesture)174 OnTouchpadNavigationGesture func(gesture TouchpadNavigationGesture)
159 }175 }
M internal/infrastructure/cef/touchpad_navigation.go +24 -17
@@ -93,8 +93,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu
9393 }
9494
9595 threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta)
96- progress := clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1)
97- r.thresholdReached = progress >= 1
96+ absDX := math.Abs(r.cumulativeDX)
97+ progress := clampFloat(absDX/threshold, 0, 1)
98+ r.thresholdReached = absDX > threshold
9899 if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction {
99100 return touchpadNavigationResult{}
100101 }
@@ -147,23 +148,18 @@ func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationI
147148 }
148149
149150 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
150- if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
151- return port.TouchpadNavigationBack, true
152- }
153- if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
154- return port.TouchpadNavigationForward, true
155- }
156- return port.TouchpadNavigationBack, false
151+ return r.resolvedDirection(input)
157152 }
158153
159154 func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationResult {
160- action, ok := r.indicatorAction(touchpadNavigationInput{})
161- if !ok && r.indicatorShown {
162- action, ok = r.lastIndicatorAction, true
155+ var action port.TouchpadNavigationAction
156+ hasAction := r.indicatorShown
157+ if hasAction {
158+ action = r.lastIndicatorAction
163159 }
164160 r.reset()
165161 r.verticalCanceled = true
166- if !ok {
162+ if !hasAction {
167163 return touchpadNavigationResult{}
168164 }
169165 return touchpadNavigationResult{
@@ -176,13 +172,24 @@ func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationR
176172 }
177173
178174 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {
175+ action, ok := r.resolvedDirection(input)
176+ if !ok {
177+ return cef2gtk.NavigationSwipeBack, false
178+ }
179+ if action == port.TouchpadNavigationForward {
180+ return cef2gtk.NavigationSwipeForward, true
181+ }
182+ return cef2gtk.NavigationSwipeBack, true
183+}
184+
185+func (r *touchpadNavigationRecognizer) resolvedDirection(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
179186 if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
180- return cef2gtk.NavigationSwipeBack, true
187+ return port.TouchpadNavigationBack, true
181188 }
182189 if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
183- return cef2gtk.NavigationSwipeForward, true
190+ return port.TouchpadNavigationForward, true
184191 }
185- return cef2gtk.NavigationSwipeBack, false
192+ return port.TouchpadNavigationBack, false
186193 }
187194
188195 func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {
@@ -193,7 +200,7 @@ func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationI
193200
194201 func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action port.TouchpadNavigationAction) bool {
195202 if r.gestureViewWidth <= 0 {
196- return true
203+ return false
197204 }
198205 edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)
199206 if action == port.TouchpadNavigationForward {
@@ -93,8 +93,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu
93 }93 }
94 94
95 threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta)95 threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta)
96- progress := clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1)96+ absDX := math.Abs(r.cumulativeDX)
97- r.thresholdReached = progress >= 197+ progress := clampFloat(absDX/threshold, 0, 1)
98+ r.thresholdReached = absDX > threshold
98 if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction {99 if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction {
99 return touchpadNavigationResult{}100 return touchpadNavigationResult{}
100 }101 }
@@ -147,23 +148,18 @@ func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationI
147 }148 }
148 149
149 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {150 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
150- if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {151+ return r.resolvedDirection(input)
151- return port.TouchpadNavigationBack, true
152- }
153- if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
154- return port.TouchpadNavigationForward, true
155- }
156- return port.TouchpadNavigationBack, false
157 }152 }
158 153
159 func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationResult {154 func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationResult {
160- action, ok := r.indicatorAction(touchpadNavigationInput{})155+ var action port.TouchpadNavigationAction
161- if !ok && r.indicatorShown {156+ hasAction := r.indicatorShown
162- action, ok = r.lastIndicatorAction, true157+ if hasAction {
158+ action = r.lastIndicatorAction
163 }159 }
164 r.reset()160 r.reset()
165 r.verticalCanceled = true161 r.verticalCanceled = true
166- if !ok {162+ if !hasAction {
167 return touchpadNavigationResult{}163 return touchpadNavigationResult{}
168 }164 }
169 return touchpadNavigationResult{165 return touchpadNavigationResult{
@@ -176,13 +172,24 @@ func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationR
176 }172 }
177 173
178 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {174 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {
175+ action, ok := r.resolvedDirection(input)
176+ if !ok {
177+ return cef2gtk.NavigationSwipeBack, false
178+ }
179+ if action == port.TouchpadNavigationForward {
180+ return cef2gtk.NavigationSwipeForward, true
181+ }
182+ return cef2gtk.NavigationSwipeBack, true
183+}
184+
185+func (r *touchpadNavigationRecognizer) resolvedDirection(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
179 if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {186 if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
180- return cef2gtk.NavigationSwipeBack, true187+ return port.TouchpadNavigationBack, true
181 }188 }
182 if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {189 if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
183- return cef2gtk.NavigationSwipeForward, true190+ return port.TouchpadNavigationForward, true
184 }191 }
185- return cef2gtk.NavigationSwipeBack, false192+ return port.TouchpadNavigationBack, false
186 }193 }
187 194
188 func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {195 func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {
@@ -193,7 +200,7 @@ func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationI
193 200
194 func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action port.TouchpadNavigationAction) bool {201 func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action port.TouchpadNavigationAction) bool {
195 if r.gestureViewWidth <= 0 {202 if r.gestureViewWidth <= 0 {
196- return true203+ return false
197 }204 }
198 edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)205 edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)
199 if action == port.TouchpadNavigationForward {206 if action == port.TouchpadNavigationForward {
M internal/infrastructure/cef/touchpad_navigation_test.go +280 -2
@@ -22,6 +22,7 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT
2222 Config: cfg,
2323 CanGoBack: true,
2424 CanGoForward: true,
25+ ViewWidth: 1000,
2526 })
2627 require.False(t, begin.HasIndicator)
2728 require.False(t, begin.HasAction)
@@ -73,6 +74,83 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT
7374 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
7475 }
7576
77+func TestTouchpadNavigationRecognizer_ReportsForwardProgressAndNavigatesOnReleaseAfterThreshold(t *testing.T) {
78+ recognizer := newTouchpadNavigationRecognizer()
79+ cfg := RuntimeInputConfig{
80+ TouchpadNavigationEnabled: true,
81+ TouchpadNavigationMinDelta: 200,
82+ TouchpadNavigationMaxVerticalRatio: 0.5,
83+ }
84+
85+ begin := recognizer.Handle(touchpadNavigationInput{
86+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
87+ Config: cfg,
88+ CanGoBack: true,
89+ CanGoForward: true,
90+ ViewWidth: 1000,
91+ })
92+ require.False(t, begin.HasIndicator)
93+ require.False(t, begin.HasAction)
94+
95+ armed := recognizer.Handle(touchpadNavigationInput{
96+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
97+ Config: cfg,
98+ CanGoBack: true,
99+ CanGoForward: true,
100+ })
101+ require.True(t, armed.HasIndicator)
102+ require.Equal(t, port.TouchpadNavigationForward, armed.Indicator.Action)
103+ require.True(t, armed.Indicator.Active)
104+ require.InDelta(t, 1.0, armed.Indicator.Progress, 0.001)
105+ require.True(t, armed.Indicator.ThresholdReached)
106+ require.False(t, armed.HasAction)
107+
108+ end := recognizer.Handle(touchpadNavigationInput{
109+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
110+ Config: cfg,
111+ CanGoBack: true,
112+ CanGoForward: true,
113+ })
114+ require.True(t, end.HasIndicator)
115+ require.False(t, end.Indicator.Active)
116+ require.True(t, end.HasAction)
117+ require.Equal(t, cef2gtk.NavigationSwipeForward, end.Action)
118+}
119+
120+func TestTouchpadNavigationRecognizer_DoesNotNavigateAtExactThreshold(t *testing.T) {
121+ recognizer := newTouchpadNavigationRecognizer()
122+ cfg := RuntimeInputConfig{
123+ TouchpadNavigationEnabled: true,
124+ TouchpadNavigationMinDelta: 200,
125+ TouchpadNavigationMaxVerticalRatio: 0.5,
126+ }
127+
128+ _ = recognizer.Handle(touchpadNavigationInput{
129+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
130+ Config: cfg,
131+ CanGoBack: true,
132+ ViewWidth: 1000,
133+ })
134+ exact := recognizer.Handle(touchpadNavigationInput{
135+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -200, 0),
136+ Config: cfg,
137+ CanGoBack: true,
138+ })
139+ require.True(t, exact.HasIndicator)
140+ require.InDelta(t, 1.0, exact.Indicator.Progress, 0.001)
141+ require.False(t, exact.Indicator.ThresholdReached)
142+
143+ end := recognizer.Handle(touchpadNavigationInput{
144+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
145+ Config: cfg,
146+ CanGoBack: true,
147+ })
148+ require.True(t, end.HasIndicator)
149+ require.False(t, end.Indicator.Active)
150+ require.False(t, end.Indicator.ThresholdReached)
151+ require.False(t, end.HasAction)
152+}
153+
76154 func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) {
77155 recognizer := newTouchpadNavigationRecognizer()
78156 cfg := RuntimeInputConfig{
@@ -85,6 +163,7 @@ func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistanc
85163 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
86164 Config: cfg,
87165 CanGoBack: true,
166+ ViewWidth: 1000,
88167 })
89168 ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
90169 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0),
@@ -121,7 +200,6 @@ func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t
121200 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),
122201 Config: cfg,
123202 CanGoBack: true,
124- ViewWidth: 1000,
125203 })
126204 require.True(t, armed.HasIndicator)
127205 require.True(t, armed.Indicator.ThresholdReached)
@@ -130,7 +208,6 @@ func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t
130208 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),
131209 Config: cfg,
132210 CanGoBack: true,
133- ViewWidth: 1000,
134211 })
135212 require.True(t, end.HasAction)
136213 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
@@ -169,6 +246,153 @@ func TestTouchpadNavigationRecognizer_DoesNotTreatInContentHorizontalScrollAsNav
169246 require.False(t, end.HasAction)
170247 }
171248
249+func TestTouchpadNavigationRecognizer_IgnoresNonPreciseScrollEvents(t *testing.T) {
250+ tests := []struct {
251+ name string
252+ event cef2gtk.ScrollEvent
253+ }{
254+ {
255+ name: "unknown unit",
256+ event: cef2gtk.ScrollEvent{
257+ Phase: cef2gtk.ScrollPhaseUpdate,
258+ DX: -240,
259+ Unit: gdk.ScrollUnitSurfaceValue,
260+ UnitKnown: false,
261+ },
262+ },
263+ {
264+ name: "wheel unit",
265+ event: cef2gtk.ScrollEvent{
266+ Phase: cef2gtk.ScrollPhaseUpdate,
267+ DX: -240,
268+ Unit: gdk.ScrollUnitWheelValue,
269+ UnitKnown: true,
270+ },
271+ },
272+ }
273+
274+ for _, tt := range tests {
275+ t.Run(tt.name, func(t *testing.T) {
276+ recognizer := newTouchpadNavigationRecognizer()
277+ cfg := RuntimeInputConfig{
278+ TouchpadNavigationEnabled: true,
279+ TouchpadNavigationMinDelta: 200,
280+ TouchpadNavigationMaxVerticalRatio: 0.5,
281+ }
282+
283+ _ = recognizer.Handle(touchpadNavigationInput{
284+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
285+ Config: cfg,
286+ CanGoBack: true,
287+ ViewWidth: 1000,
288+ })
289+ result := recognizer.Handle(touchpadNavigationInput{
290+ Event: tt.event,
291+ Config: cfg,
292+ CanGoBack: true,
293+ })
294+ require.False(t, result.HasIndicator)
295+ require.False(t, result.HasAction)
296+ })
297+ }
298+}
299+
300+func TestTouchpadNavigationRecognizer_DisabledConfigSuppressesGestureRecognition(t *testing.T) {
301+ recognizer := newTouchpadNavigationRecognizer()
302+ cfg := RuntimeInputConfig{
303+ TouchpadNavigationEnabled: false,
304+ TouchpadNavigationMinDelta: 200,
305+ TouchpadNavigationMaxVerticalRatio: 0.5,
306+ }
307+
308+ begin := recognizer.Handle(touchpadNavigationInput{
309+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
310+ Config: cfg,
311+ CanGoBack: true,
312+ ViewWidth: 1000,
313+ })
314+ require.False(t, begin.HasIndicator)
315+ require.False(t, begin.HasAction)
316+
317+ update := recognizer.Handle(touchpadNavigationInput{
318+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
319+ Config: cfg,
320+ CanGoBack: true,
321+ })
322+ require.False(t, update.HasIndicator)
323+ require.False(t, update.HasAction)
324+
325+ end := recognizer.Handle(touchpadNavigationInput{
326+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
327+ Config: cfg,
328+ CanGoBack: true,
329+ })
330+ require.False(t, end.HasIndicator)
331+ require.False(t, end.HasAction)
332+}
333+
334+func TestTouchpadNavigationRecognizer_BackCapabilitySuppressesBackGesture(t *testing.T) {
335+ recognizer := newTouchpadNavigationRecognizer()
336+ cfg := RuntimeInputConfig{
337+ TouchpadNavigationEnabled: true,
338+ TouchpadNavigationMinDelta: 200,
339+ TouchpadNavigationMaxVerticalRatio: 0.5,
340+ }
341+
342+ _ = recognizer.Handle(touchpadNavigationInput{
343+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
344+ Config: cfg,
345+ CanGoBack: false,
346+ ViewWidth: 1000,
347+ })
348+ update := recognizer.Handle(touchpadNavigationInput{
349+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
350+ Config: cfg,
351+ CanGoBack: false,
352+ })
353+ require.False(t, update.HasIndicator)
354+ require.False(t, update.HasAction)
355+
356+ end := recognizer.Handle(touchpadNavigationInput{
357+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
358+ Config: cfg,
359+ CanGoBack: false,
360+ })
361+ require.False(t, end.HasIndicator)
362+ require.False(t, end.HasAction)
363+}
364+
365+func TestTouchpadNavigationRecognizer_ForwardCapabilitySuppressesForwardGesture(t *testing.T) {
366+ recognizer := newTouchpadNavigationRecognizer()
367+ cfg := RuntimeInputConfig{
368+ TouchpadNavigationEnabled: true,
369+ TouchpadNavigationMinDelta: 200,
370+ TouchpadNavigationMaxVerticalRatio: 0.5,
371+ }
372+
373+ _ = recognizer.Handle(touchpadNavigationInput{
374+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
375+ Config: cfg,
376+ CanGoForward: false,
377+ ViewWidth: 1000,
378+ })
379+ update := recognizer.Handle(touchpadNavigationInput{
380+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
381+ Config: cfg,
382+ CanGoForward: false,
383+ })
384+ require.False(t, update.HasIndicator)
385+ require.False(t, update.HasAction)
386+
387+ end := recognizer.Handle(touchpadNavigationInput{
388+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
389+ Config: cfg,
390+ CanGoForward: false,
391+ })
392+ require.False(t, end.HasIndicator)
393+ require.False(t, end.HasAction)
394+}
395+
172396 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {
173397 recognizer := newTouchpadNavigationRecognizer()
174398 cfg := RuntimeInputConfig{
@@ -181,6 +405,7 @@ func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenRelease
181405 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
182406 Config: cfg,
183407 CanGoBack: true,
408+ ViewWidth: 1000,
184409 })
185410 progress := recognizer.Handle(touchpadNavigationInput{
186411 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -120, 0),
@@ -201,6 +426,56 @@ func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenRelease
201426 require.False(t, end.Indicator.Active)
202427 }
203428
429+func TestTouchpadNavigationRecognizer_ResetsBetweenConsecutiveGestures(t *testing.T) {
430+ recognizer := newTouchpadNavigationRecognizer()
431+ cfg := RuntimeInputConfig{
432+ TouchpadNavigationEnabled: true,
433+ TouchpadNavigationMinDelta: 200,
434+ TouchpadNavigationMaxVerticalRatio: 0.5,
435+ }
436+
437+ _ = recognizer.Handle(touchpadNavigationInput{
438+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
439+ Config: cfg,
440+ CanGoBack: true,
441+ ViewWidth: 1000,
442+ })
443+ firstUpdate := recognizer.Handle(touchpadNavigationInput{
444+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
445+ Config: cfg,
446+ CanGoBack: true,
447+ })
448+ require.True(t, firstUpdate.HasIndicator)
449+ firstEnd := recognizer.Handle(touchpadNavigationInput{
450+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
451+ Config: cfg,
452+ CanGoBack: true,
453+ })
454+ require.True(t, firstEnd.HasAction)
455+ require.Equal(t, cef2gtk.NavigationSwipeBack, firstEnd.Action)
456+
457+ _ = recognizer.Handle(touchpadNavigationInput{
458+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
459+ Config: cfg,
460+ CanGoForward: true,
461+ ViewWidth: 1000,
462+ })
463+ secondUpdate := recognizer.Handle(touchpadNavigationInput{
464+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
465+ Config: cfg,
466+ CanGoForward: true,
467+ })
468+ require.True(t, secondUpdate.HasIndicator)
469+ require.Equal(t, port.TouchpadNavigationForward, secondUpdate.Indicator.Action)
470+ secondEnd := recognizer.Handle(touchpadNavigationInput{
471+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
472+ Config: cfg,
473+ CanGoForward: true,
474+ })
475+ require.True(t, secondEnd.HasAction)
476+ require.Equal(t, cef2gtk.NavigationSwipeForward, secondEnd.Action)
477+}
478+
204479 func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
205480 recognizer := newTouchpadNavigationRecognizer()
206481 cfg := RuntimeInputConfig{
@@ -213,6 +488,7 @@ func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
213488 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
214489 Config: cfg,
215490 CanGoBack: true,
491+ ViewWidth: 1000,
216492 })
217493 vertical := recognizer.Handle(touchpadNavigationInput{
218494 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -60, 80),
@@ -242,6 +518,8 @@ func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) ce
242518 return touchpadNavigationScrollEventAt(phase, 0, dx, dy)
243519 }
244520
521+// ViewWidth is provided separately on touchpadNavigationInput; the recognizer
522+// caches it from the initial Begin event for subsequent Update and End phases.
245523 func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {
246524 return cef2gtk.ScrollEvent{
247525 Phase: phase,
@@ -22,6 +22,7 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT
22 Config: cfg,22 Config: cfg,
23 CanGoBack: true,23 CanGoBack: true,
24 CanGoForward: true,24 CanGoForward: true,
25+ ViewWidth: 1000,
25 })26 })
26 require.False(t, begin.HasIndicator)27 require.False(t, begin.HasIndicator)
27 require.False(t, begin.HasAction)28 require.False(t, begin.HasAction)
@@ -73,6 +74,83 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT
73 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)74 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
74 }75 }
75 76
77+func TestTouchpadNavigationRecognizer_ReportsForwardProgressAndNavigatesOnReleaseAfterThreshold(t *testing.T) {
78+ recognizer := newTouchpadNavigationRecognizer()
79+ cfg := RuntimeInputConfig{
80+ TouchpadNavigationEnabled: true,
81+ TouchpadNavigationMinDelta: 200,
82+ TouchpadNavigationMaxVerticalRatio: 0.5,
83+ }
84+
85+ begin := recognizer.Handle(touchpadNavigationInput{
86+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
87+ Config: cfg,
88+ CanGoBack: true,
89+ CanGoForward: true,
90+ ViewWidth: 1000,
91+ })
92+ require.False(t, begin.HasIndicator)
93+ require.False(t, begin.HasAction)
94+
95+ armed := recognizer.Handle(touchpadNavigationInput{
96+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
97+ Config: cfg,
98+ CanGoBack: true,
99+ CanGoForward: true,
100+ })
101+ require.True(t, armed.HasIndicator)
102+ require.Equal(t, port.TouchpadNavigationForward, armed.Indicator.Action)
103+ require.True(t, armed.Indicator.Active)
104+ require.InDelta(t, 1.0, armed.Indicator.Progress, 0.001)
105+ require.True(t, armed.Indicator.ThresholdReached)
106+ require.False(t, armed.HasAction)
107+
108+ end := recognizer.Handle(touchpadNavigationInput{
109+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
110+ Config: cfg,
111+ CanGoBack: true,
112+ CanGoForward: true,
113+ })
114+ require.True(t, end.HasIndicator)
115+ require.False(t, end.Indicator.Active)
116+ require.True(t, end.HasAction)
117+ require.Equal(t, cef2gtk.NavigationSwipeForward, end.Action)
118+}
119+
120+func TestTouchpadNavigationRecognizer_DoesNotNavigateAtExactThreshold(t *testing.T) {
121+ recognizer := newTouchpadNavigationRecognizer()
122+ cfg := RuntimeInputConfig{
123+ TouchpadNavigationEnabled: true,
124+ TouchpadNavigationMinDelta: 200,
125+ TouchpadNavigationMaxVerticalRatio: 0.5,
126+ }
127+
128+ _ = recognizer.Handle(touchpadNavigationInput{
129+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
130+ Config: cfg,
131+ CanGoBack: true,
132+ ViewWidth: 1000,
133+ })
134+ exact := recognizer.Handle(touchpadNavigationInput{
135+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -200, 0),
136+ Config: cfg,
137+ CanGoBack: true,
138+ })
139+ require.True(t, exact.HasIndicator)
140+ require.InDelta(t, 1.0, exact.Indicator.Progress, 0.001)
141+ require.False(t, exact.Indicator.ThresholdReached)
142+
143+ end := recognizer.Handle(touchpadNavigationInput{
144+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
145+ Config: cfg,
146+ CanGoBack: true,
147+ })
148+ require.True(t, end.HasIndicator)
149+ require.False(t, end.Indicator.Active)
150+ require.False(t, end.Indicator.ThresholdReached)
151+ require.False(t, end.HasAction)
152+}
153+
76 func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) {154 func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) {
77 recognizer := newTouchpadNavigationRecognizer()155 recognizer := newTouchpadNavigationRecognizer()
78 cfg := RuntimeInputConfig{156 cfg := RuntimeInputConfig{
@@ -85,6 +163,7 @@ func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistanc
85 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),163 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
86 Config: cfg,164 Config: cfg,
87 CanGoBack: true,165 CanGoBack: true,
166+ ViewWidth: 1000,
88 })167 })
89 ordinaryScroll := recognizer.Handle(touchpadNavigationInput{168 ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
90 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0),169 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0),
@@ -121,7 +200,6 @@ func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t
121 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),200 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),
122 Config: cfg,201 Config: cfg,
123 CanGoBack: true,202 CanGoBack: true,
124- ViewWidth: 1000,
125 })203 })
126 require.True(t, armed.HasIndicator)204 require.True(t, armed.HasIndicator)
127 require.True(t, armed.Indicator.ThresholdReached)205 require.True(t, armed.Indicator.ThresholdReached)
@@ -130,7 +208,6 @@ func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t
130 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),208 Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),
131 Config: cfg,209 Config: cfg,
132 CanGoBack: true,210 CanGoBack: true,
133- ViewWidth: 1000,
134 })211 })
135 require.True(t, end.HasAction)212 require.True(t, end.HasAction)
136 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)213 require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
@@ -169,6 +246,153 @@ func TestTouchpadNavigationRecognizer_DoesNotTreatInContentHorizontalScrollAsNav
169 require.False(t, end.HasAction)246 require.False(t, end.HasAction)
170 }247 }
171 248
249+func TestTouchpadNavigationRecognizer_IgnoresNonPreciseScrollEvents(t *testing.T) {
250+ tests := []struct {
251+ name string
252+ event cef2gtk.ScrollEvent
253+ }{
254+ {
255+ name: "unknown unit",
256+ event: cef2gtk.ScrollEvent{
257+ Phase: cef2gtk.ScrollPhaseUpdate,
258+ DX: -240,
259+ Unit: gdk.ScrollUnitSurfaceValue,
260+ UnitKnown: false,
261+ },
262+ },
263+ {
264+ name: "wheel unit",
265+ event: cef2gtk.ScrollEvent{
266+ Phase: cef2gtk.ScrollPhaseUpdate,
267+ DX: -240,
268+ Unit: gdk.ScrollUnitWheelValue,
269+ UnitKnown: true,
270+ },
271+ },
272+ }
273+
274+ for _, tt := range tests {
275+ t.Run(tt.name, func(t *testing.T) {
276+ recognizer := newTouchpadNavigationRecognizer()
277+ cfg := RuntimeInputConfig{
278+ TouchpadNavigationEnabled: true,
279+ TouchpadNavigationMinDelta: 200,
280+ TouchpadNavigationMaxVerticalRatio: 0.5,
281+ }
282+
283+ _ = recognizer.Handle(touchpadNavigationInput{
284+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
285+ Config: cfg,
286+ CanGoBack: true,
287+ ViewWidth: 1000,
288+ })
289+ result := recognizer.Handle(touchpadNavigationInput{
290+ Event: tt.event,
291+ Config: cfg,
292+ CanGoBack: true,
293+ })
294+ require.False(t, result.HasIndicator)
295+ require.False(t, result.HasAction)
296+ })
297+ }
298+}
299+
300+func TestTouchpadNavigationRecognizer_DisabledConfigSuppressesGestureRecognition(t *testing.T) {
301+ recognizer := newTouchpadNavigationRecognizer()
302+ cfg := RuntimeInputConfig{
303+ TouchpadNavigationEnabled: false,
304+ TouchpadNavigationMinDelta: 200,
305+ TouchpadNavigationMaxVerticalRatio: 0.5,
306+ }
307+
308+ begin := recognizer.Handle(touchpadNavigationInput{
309+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
310+ Config: cfg,
311+ CanGoBack: true,
312+ ViewWidth: 1000,
313+ })
314+ require.False(t, begin.HasIndicator)
315+ require.False(t, begin.HasAction)
316+
317+ update := recognizer.Handle(touchpadNavigationInput{
318+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
319+ Config: cfg,
320+ CanGoBack: true,
321+ })
322+ require.False(t, update.HasIndicator)
323+ require.False(t, update.HasAction)
324+
325+ end := recognizer.Handle(touchpadNavigationInput{
326+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
327+ Config: cfg,
328+ CanGoBack: true,
329+ })
330+ require.False(t, end.HasIndicator)
331+ require.False(t, end.HasAction)
332+}
333+
334+func TestTouchpadNavigationRecognizer_BackCapabilitySuppressesBackGesture(t *testing.T) {
335+ recognizer := newTouchpadNavigationRecognizer()
336+ cfg := RuntimeInputConfig{
337+ TouchpadNavigationEnabled: true,
338+ TouchpadNavigationMinDelta: 200,
339+ TouchpadNavigationMaxVerticalRatio: 0.5,
340+ }
341+
342+ _ = recognizer.Handle(touchpadNavigationInput{
343+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
344+ Config: cfg,
345+ CanGoBack: false,
346+ ViewWidth: 1000,
347+ })
348+ update := recognizer.Handle(touchpadNavigationInput{
349+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
350+ Config: cfg,
351+ CanGoBack: false,
352+ })
353+ require.False(t, update.HasIndicator)
354+ require.False(t, update.HasAction)
355+
356+ end := recognizer.Handle(touchpadNavigationInput{
357+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
358+ Config: cfg,
359+ CanGoBack: false,
360+ })
361+ require.False(t, end.HasIndicator)
362+ require.False(t, end.HasAction)
363+}
364+
365+func TestTouchpadNavigationRecognizer_ForwardCapabilitySuppressesForwardGesture(t *testing.T) {
366+ recognizer := newTouchpadNavigationRecognizer()
367+ cfg := RuntimeInputConfig{
368+ TouchpadNavigationEnabled: true,
369+ TouchpadNavigationMinDelta: 200,
370+ TouchpadNavigationMaxVerticalRatio: 0.5,
371+ }
372+
373+ _ = recognizer.Handle(touchpadNavigationInput{
374+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
375+ Config: cfg,
376+ CanGoForward: false,
377+ ViewWidth: 1000,
378+ })
379+ update := recognizer.Handle(touchpadNavigationInput{
380+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
381+ Config: cfg,
382+ CanGoForward: false,
383+ })
384+ require.False(t, update.HasIndicator)
385+ require.False(t, update.HasAction)
386+
387+ end := recognizer.Handle(touchpadNavigationInput{
388+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
389+ Config: cfg,
390+ CanGoForward: false,
391+ })
392+ require.False(t, end.HasIndicator)
393+ require.False(t, end.HasAction)
394+}
395+
172 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {396 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {
173 recognizer := newTouchpadNavigationRecognizer()397 recognizer := newTouchpadNavigationRecognizer()
174 cfg := RuntimeInputConfig{398 cfg := RuntimeInputConfig{
@@ -181,6 +405,7 @@ func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenRelease
181 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),405 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
182 Config: cfg,406 Config: cfg,
183 CanGoBack: true,407 CanGoBack: true,
408+ ViewWidth: 1000,
184 })409 })
185 progress := recognizer.Handle(touchpadNavigationInput{410 progress := recognizer.Handle(touchpadNavigationInput{
186 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -120, 0),411 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -120, 0),
@@ -201,6 +426,56 @@ func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenRelease
201 require.False(t, end.Indicator.Active)426 require.False(t, end.Indicator.Active)
202 }427 }
203 428
429+func TestTouchpadNavigationRecognizer_ResetsBetweenConsecutiveGestures(t *testing.T) {
430+ recognizer := newTouchpadNavigationRecognizer()
431+ cfg := RuntimeInputConfig{
432+ TouchpadNavigationEnabled: true,
433+ TouchpadNavigationMinDelta: 200,
434+ TouchpadNavigationMaxVerticalRatio: 0.5,
435+ }
436+
437+ _ = recognizer.Handle(touchpadNavigationInput{
438+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
439+ Config: cfg,
440+ CanGoBack: true,
441+ ViewWidth: 1000,
442+ })
443+ firstUpdate := recognizer.Handle(touchpadNavigationInput{
444+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
445+ Config: cfg,
446+ CanGoBack: true,
447+ })
448+ require.True(t, firstUpdate.HasIndicator)
449+ firstEnd := recognizer.Handle(touchpadNavigationInput{
450+ Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
451+ Config: cfg,
452+ CanGoBack: true,
453+ })
454+ require.True(t, firstEnd.HasAction)
455+ require.Equal(t, cef2gtk.NavigationSwipeBack, firstEnd.Action)
456+
457+ _ = recognizer.Handle(touchpadNavigationInput{
458+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 976, 0, 0),
459+ Config: cfg,
460+ CanGoForward: true,
461+ ViewWidth: 1000,
462+ })
463+ secondUpdate := recognizer.Handle(touchpadNavigationInput{
464+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 976, 240, 0),
465+ Config: cfg,
466+ CanGoForward: true,
467+ })
468+ require.True(t, secondUpdate.HasIndicator)
469+ require.Equal(t, port.TouchpadNavigationForward, secondUpdate.Indicator.Action)
470+ secondEnd := recognizer.Handle(touchpadNavigationInput{
471+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 976, 0, 0),
472+ Config: cfg,
473+ CanGoForward: true,
474+ })
475+ require.True(t, secondEnd.HasAction)
476+ require.Equal(t, cef2gtk.NavigationSwipeForward, secondEnd.Action)
477+}
478+
204 func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {479 func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
205 recognizer := newTouchpadNavigationRecognizer()480 recognizer := newTouchpadNavigationRecognizer()
206 cfg := RuntimeInputConfig{481 cfg := RuntimeInputConfig{
@@ -213,6 +488,7 @@ func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
213 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),488 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
214 Config: cfg,489 Config: cfg,
215 CanGoBack: true,490 CanGoBack: true,
491+ ViewWidth: 1000,
216 })492 })
217 vertical := recognizer.Handle(touchpadNavigationInput{493 vertical := recognizer.Handle(touchpadNavigationInput{
218 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -60, 80),494 Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -60, 80),
@@ -242,6 +518,8 @@ func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) ce
242 return touchpadNavigationScrollEventAt(phase, 0, dx, dy)518 return touchpadNavigationScrollEventAt(phase, 0, dx, dy)
243 }519 }
244 520
521+// ViewWidth is provided separately on touchpadNavigationInput; the recognizer
522+// caches it from the initial Begin event for subsequent Update and End phases.
245 func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {523 func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {
246 return cef2gtk.ScrollEvent{524 return cef2gtk.ScrollEvent{
247 Phase: phase,525 Phase: phase,
M internal/infrastructure/cef/webview.go +9 -6
@@ -1137,6 +1137,7 @@ func (wv *WebView) Destroy() {
11371137 wv.openerMessageCallbacks = nil
11381138 wv.openerNavigationCallbacks = nil
11391139 wv.popupReadyToShow = nil
1140+ wv.touchpadNavigation = nil
11401141 wv.inputAttached = false
11411142 wv.pendingNativePopups = nil
11421143 wv.nativePopupParent = nil
@@ -1486,10 +1487,7 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14861487 // waits for a deliberate release past the visual threshold.
14871488 Enabled: false,
14881489 },
1489- CanNavigateBack: wv.CanGoBack,
1490- CanNavigateForward: wv.CanGoForward,
1491- OnNavigateSwipe: wv.handleNavigationSwipeAction,
1492- SelectionText: wv.selectedTextSnapshot,
1490+ SelectionText: wv.selectedTextSnapshot,
14931491 OnClipboardShortcut: func(action, text string) {
14941492 if wv.engine != nil {
14951493 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
@@ -1498,13 +1496,16 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14981496 }
14991497 }
15001498
1499+// handleScrollInput is invoked by cef2gtk from GTK scroll event callbacks on
1500+// the GTK main thread, which keeps lazy touchpad recognizer ownership
1501+// single-threaded while every scroll event is still forwarded to CEF.
15011502 func (wv *WebView) handleScrollInput(event cef2gtk.ScrollEvent) cef2gtk.ScrollDecision {
15021503 wv.handleTouchpadNavigationScroll(event)
15031504 return wv.handleScrollInputDiagnostic(event)
15041505 }
15051506
15061507 func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
1507- if wv == nil {
1508+ if wv == nil || wv.destroyed.Load() {
15081509 return
15091510 }
15101511 if wv.touchpadNavigation == nil {
@@ -1543,7 +1544,9 @@ func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigation
15431544 cb := wv.callbacks
15441545 wv.mu.RUnlock()
15451546 if cb != nil && cb.OnTouchpadNavigationGesture != nil {
1546- cb.OnTouchpadNavigationGesture(gesture)
1547+ wv.runOnGTK(func() {
1548+ cb.OnTouchpadNavigationGesture(gesture)
1549+ })
15471550 }
15481551 }
15491552
@@ -1137,6 +1137,7 @@ func (wv *WebView) Destroy() {
1137 wv.openerMessageCallbacks = nil1137 wv.openerMessageCallbacks = nil
1138 wv.openerNavigationCallbacks = nil1138 wv.openerNavigationCallbacks = nil
1139 wv.popupReadyToShow = nil1139 wv.popupReadyToShow = nil
1140+ wv.touchpadNavigation = nil
1140 wv.inputAttached = false1141 wv.inputAttached = false
1141 wv.pendingNativePopups = nil1142 wv.pendingNativePopups = nil
1142 wv.nativePopupParent = nil1143 wv.nativePopupParent = nil
@@ -1486,10 +1487,7 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1486 // waits for a deliberate release past the visual threshold.1487 // waits for a deliberate release past the visual threshold.
1487 Enabled: false,1488 Enabled: false,
1488 },1489 },
1489- CanNavigateBack: wv.CanGoBack,1490+ SelectionText: wv.selectedTextSnapshot,
1490- CanNavigateForward: wv.CanGoForward,
1491- OnNavigateSwipe: wv.handleNavigationSwipeAction,
1492- SelectionText: wv.selectedTextSnapshot,
1493 OnClipboardShortcut: func(action, text string) {1491 OnClipboardShortcut: func(action, text string) {
1494 if wv.engine != nil {1492 if wv.engine != nil {
1495 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)1493 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
@@ -1498,13 +1496,16 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1498 }1496 }
1499 }1497 }
1500 1498
1499+// handleScrollInput is invoked by cef2gtk from GTK scroll event callbacks on
1500+// the GTK main thread, which keeps lazy touchpad recognizer ownership
1501+// single-threaded while every scroll event is still forwarded to CEF.
1501 func (wv *WebView) handleScrollInput(event cef2gtk.ScrollEvent) cef2gtk.ScrollDecision {1502 func (wv *WebView) handleScrollInput(event cef2gtk.ScrollEvent) cef2gtk.ScrollDecision {
1502 wv.handleTouchpadNavigationScroll(event)1503 wv.handleTouchpadNavigationScroll(event)
1503 return wv.handleScrollInputDiagnostic(event)1504 return wv.handleScrollInputDiagnostic(event)
1504 }1505 }
1505 1506
1506 func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {1507 func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
1507- if wv == nil {1508+ if wv == nil || wv.destroyed.Load() {
1508 return1509 return
1509 }1510 }
1510 if wv.touchpadNavigation == nil {1511 if wv.touchpadNavigation == nil {
@@ -1543,7 +1544,9 @@ func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigation
1543 cb := wv.callbacks1544 cb := wv.callbacks
1544 wv.mu.RUnlock()1545 wv.mu.RUnlock()
1545 if cb != nil && cb.OnTouchpadNavigationGesture != nil {1546 if cb != nil && cb.OnTouchpadNavigationGesture != nil {
1546- cb.OnTouchpadNavigationGesture(gesture)1547+ wv.runOnGTK(func() {
1548+ cb.OnTouchpadNavigationGesture(gesture)
1549+ })
1547 }1550 }
1548 }1551 }
1549 1552
M internal/infrastructure/cef/webview_input_test.go +6 -3
@@ -39,9 +39,9 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
3939 require.False(t, opts.NavigationSwipe.Enabled)
4040 require.Zero(t, opts.NavigationSwipe.MinDelta)
4141 require.Zero(t, opts.NavigationSwipe.MaxVerticalRatio)
42- require.NotNil(t, opts.CanNavigateBack)
43- require.NotNil(t, opts.CanNavigateForward)
44- require.NotNil(t, opts.OnNavigateSwipe)
42+ require.Nil(t, opts.CanNavigateBack)
43+ require.Nil(t, opts.CanNavigateForward)
44+ require.Nil(t, opts.OnNavigateSwipe)
4545 require.NotNil(t, opts.SelectionText)
4646 require.NotNil(t, opts.OnClipboardShortcut)
4747 require.Same(t, nativeWidget, wv.nativeWidget)
@@ -58,6 +58,9 @@ func TestWebViewHandleScrollInput_ForwardsOrdinaryHorizontalScroll(t *testing.T)
5858 canGoBack: true,
5959 }
6060
61+ // This verifies the bridge-level forwarding contract for ordinary scroll
62+ // updates. The nil native widget leaves view width unknown, so local history
63+ // gesture recognition fails closed and the event continues to CEF.
6164 decision := wv.handleScrollInput(cef2gtk.ScrollEvent{
6265 Phase: cef2gtk.ScrollPhaseUpdate,
6366 X: 500,
@@ -39,9 +39,9 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
39 require.False(t, opts.NavigationSwipe.Enabled)39 require.False(t, opts.NavigationSwipe.Enabled)
40 require.Zero(t, opts.NavigationSwipe.MinDelta)40 require.Zero(t, opts.NavigationSwipe.MinDelta)
41 require.Zero(t, opts.NavigationSwipe.MaxVerticalRatio)41 require.Zero(t, opts.NavigationSwipe.MaxVerticalRatio)
42- require.NotNil(t, opts.CanNavigateBack)42+ require.Nil(t, opts.CanNavigateBack)
43- require.NotNil(t, opts.CanNavigateForward)43+ require.Nil(t, opts.CanNavigateForward)
44- require.NotNil(t, opts.OnNavigateSwipe)44+ require.Nil(t, opts.OnNavigateSwipe)
45 require.NotNil(t, opts.SelectionText)45 require.NotNil(t, opts.SelectionText)
46 require.NotNil(t, opts.OnClipboardShortcut)46 require.NotNil(t, opts.OnClipboardShortcut)
47 require.Same(t, nativeWidget, wv.nativeWidget)47 require.Same(t, nativeWidget, wv.nativeWidget)
@@ -58,6 +58,9 @@ func TestWebViewHandleScrollInput_ForwardsOrdinaryHorizontalScroll(t *testing.T)
58 canGoBack: true,58 canGoBack: true,
59 }59 }
60 60
61+ // This verifies the bridge-level forwarding contract for ordinary scroll
62+ // updates. The nil native widget leaves view width unknown, so local history
63+ // gesture recognition fails closed and the event continues to CEF.
61 decision := wv.handleScrollInput(cef2gtk.ScrollEvent{64 decision := wv.handleScrollInput(cef2gtk.ScrollEvent{
62 Phase: cef2gtk.ScrollPhaseUpdate,65 Phase: cef2gtk.ScrollPhaseUpdate,
63 X: 500,66 X: 500,
M internal/infrastructure/config/defaults.go +2 -0
@@ -88,6 +88,8 @@ const (
8888 defaultCEFScrollPreciseMultiplier = 2.5
8989 defaultCEFScrollMaxDelta = 0
9090 defaultCEFTouchpadNavigation = true
91+ // Require a deliberate swipe before history navigation so ordinary
92+ // horizontal scrolling is unlikely to cross the commit threshold.
9193 defaultCEFTouchpadNavigationDelta = 320.0
9294 defaultCEFTouchpadNavigationRatio = 0.5
9395
@@ -88,6 +88,8 @@ const (
88 defaultCEFScrollPreciseMultiplier = 2.588 defaultCEFScrollPreciseMultiplier = 2.5
89 defaultCEFScrollMaxDelta = 089 defaultCEFScrollMaxDelta = 0
90 defaultCEFTouchpadNavigation = true90 defaultCEFTouchpadNavigation = true
91+ // Require a deliberate swipe before history navigation so ordinary
92+ // horizontal scrolling is unlikely to cross the commit threshold.
91 defaultCEFTouchpadNavigationDelta = 320.093 defaultCEFTouchpadNavigationDelta = 320.0
92 defaultCEFTouchpadNavigationRatio = 0.594 defaultCEFTouchpadNavigationRatio = 0.5
93 95
M internal/infrastructure/config/defaults_test.go +0 -1
@@ -32,7 +32,6 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
3232 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
3333 assert.Equal(t, defaultCEFTouchpadNavigation, cfg.Engine.CEF.Input.TouchpadNavigationEnabled)
3434 assert.InDelta(t, defaultCEFTouchpadNavigationDelta, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001)
35- assert.InDelta(t, 320.0, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001, "touchpad navigation should require a deliberate swipe by default")
3635 assert.InDelta(t, defaultCEFTouchpadNavigationRatio, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
3736 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
3837
@@ -32,7 +32,6 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
33 assert.Equal(t, defaultCEFTouchpadNavigation, cfg.Engine.CEF.Input.TouchpadNavigationEnabled)33 assert.Equal(t, defaultCEFTouchpadNavigation, cfg.Engine.CEF.Input.TouchpadNavigationEnabled)
34 assert.InDelta(t, defaultCEFTouchpadNavigationDelta, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001)34 assert.InDelta(t, defaultCEFTouchpadNavigationDelta, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001)
35- assert.InDelta(t, 320.0, cfg.Engine.CEF.Input.TouchpadNavigationMinDelta, 0.001, "touchpad navigation should require a deliberate swipe by default")
36 assert.InDelta(t, defaultCEFTouchpadNavigationRatio, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)35 assert.InDelta(t, defaultCEFTouchpadNavigationRatio, cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio, 0.001)
37 assert.True(t, cfg.Engine.WebKit.ITPEnabled)36 assert.True(t, cfg.Engine.WebKit.ITPEnabled)
38 37
M internal/ui/component/touchpad_navigation_indicator.go +13 -30
@@ -1,8 +1,6 @@
11 package component
22
33 import (
4- "sync"
5-
64 "github.com/bnema/dumber/internal/application/port"
75 "github.com/bnema/puregotk/v4/glib"
86 "github.com/bnema/puregotk/v4/gtk"
@@ -29,7 +27,6 @@ type TouchpadNavigationIndicator struct {
2927 bar *gtk.ProgressBar
3028
3129 hideTimer uint
32- mu sync.Mutex
3330 }
3431
3532 func NewTouchpadNavigationIndicator() *TouchpadNavigationIndicator {
@@ -121,9 +118,7 @@ func (i *TouchpadNavigationIndicator) ShowGesture(gesture port.TouchpadNavigatio
121118 if i == nil || i.container == nil || i.icon == nil || i.label == nil || i.bar == nil {
122119 return
123120 }
124- i.mu.Lock()
125- defer i.mu.Unlock()
126- i.cancelHideTimerLocked()
121+ i.cancelHideTimer()
127122
128123 i.container.RemoveCssClass("back")
129124 i.container.RemoveCssClass("forward")
@@ -149,29 +144,19 @@ func (i *TouchpadNavigationIndicator) ShowGesture(gesture port.TouchpadNavigatio
149144 i.container.SetVisible(true)
150145
151146 if !gesture.Active {
152- i.scheduleHideLocked(gesture.ThresholdReached)
153- }
154-}
155-
156-func (i *TouchpadNavigationIndicator) Hide() {
157- if i == nil || i.container == nil {
158- return
147+ i.scheduleHide(gesture.ThresholdReached)
159148 }
160- i.mu.Lock()
161- defer i.mu.Unlock()
162- i.hideLocked()
163149 }
164150
165151 func (i *TouchpadNavigationIndicator) Destroy() {
166152 if i == nil {
167153 return
168154 }
169- i.mu.Lock()
170- defer i.mu.Unlock()
171- i.cancelHideTimerLocked()
155+ i.cancelHideTimer()
172156 if i.container != nil {
173157 i.container.SetVisible(false)
174158 i.container.Unparent()
159+ i.container.Unref()
175160 }
176161 i.container = nil
177162 i.icon = nil
@@ -179,27 +164,25 @@ func (i *TouchpadNavigationIndicator) Destroy() {
179164 i.bar = nil
180165 }
181166
182-func (i *TouchpadNavigationIndicator) scheduleHideLocked(triggered bool) {
167+func (i *TouchpadNavigationIndicator) scheduleHide(triggered bool) {
183168 delay := uint(0)
184169 if triggered {
185170 delay = touchpadNavigationIndicatorHideMs
186171 }
172+ if delay == 0 {
173+ i.hide()
174+ return
175+ }
187176 cb := glib.SourceFunc(func(_ uintptr) bool {
188- i.mu.Lock()
189- defer i.mu.Unlock()
190177 i.hideTimer = 0
191- i.hideLocked()
178+ i.hide()
192179 return false
193180 })
194- if delay == 0 {
195- i.hideLocked()
196- return
197- }
198181 i.hideTimer = glib.TimeoutAdd(delay, &cb, 0)
199182 }
200183
201-func (i *TouchpadNavigationIndicator) hideLocked() {
202- i.cancelHideTimerLocked()
184+func (i *TouchpadNavigationIndicator) hide() {
185+ i.cancelHideTimer()
203186 if i.container != nil {
204187 i.container.RemoveCssClass("threshold-reached")
205188 i.container.RemoveCssClass("back")
@@ -211,7 +194,7 @@ func (i *TouchpadNavigationIndicator) hideLocked() {
211194 }
212195 }
213196
214-func (i *TouchpadNavigationIndicator) cancelHideTimerLocked() {
197+func (i *TouchpadNavigationIndicator) cancelHideTimer() {
215198 if i.hideTimer != 0 {
216199 glib.SourceRemove(i.hideTimer)
217200 i.hideTimer = 0
@@ -1,8 +1,6 @@
1 package component1 package component
2 2
3 import (3 import (
4- "sync"
5-
6 "github.com/bnema/dumber/internal/application/port"4 "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/puregotk/v4/glib"5 "github.com/bnema/puregotk/v4/glib"
8 "github.com/bnema/puregotk/v4/gtk"6 "github.com/bnema/puregotk/v4/gtk"
@@ -29,7 +27,6 @@ type TouchpadNavigationIndicator struct {
29 bar *gtk.ProgressBar27 bar *gtk.ProgressBar
30 28
31 hideTimer uint29 hideTimer uint
32- mu sync.Mutex
33 }30 }
34 31
35 func NewTouchpadNavigationIndicator() *TouchpadNavigationIndicator {32 func NewTouchpadNavigationIndicator() *TouchpadNavigationIndicator {
@@ -121,9 +118,7 @@ func (i *TouchpadNavigationIndicator) ShowGesture(gesture port.TouchpadNavigatio
121 if i == nil || i.container == nil || i.icon == nil || i.label == nil || i.bar == nil {118 if i == nil || i.container == nil || i.icon == nil || i.label == nil || i.bar == nil {
122 return119 return
123 }120 }
124- i.mu.Lock()121+ i.cancelHideTimer()
125- defer i.mu.Unlock()
126- i.cancelHideTimerLocked()
127 122
128 i.container.RemoveCssClass("back")123 i.container.RemoveCssClass("back")
129 i.container.RemoveCssClass("forward")124 i.container.RemoveCssClass("forward")
@@ -149,29 +144,19 @@ func (i *TouchpadNavigationIndicator) ShowGesture(gesture port.TouchpadNavigatio
149 i.container.SetVisible(true)144 i.container.SetVisible(true)
150 145
151 if !gesture.Active {146 if !gesture.Active {
152- i.scheduleHideLocked(gesture.ThresholdReached)147+ i.scheduleHide(gesture.ThresholdReached)
153- }
154-}
155-
156-func (i *TouchpadNavigationIndicator) Hide() {
157- if i == nil || i.container == nil {
158- return
159 }148 }
160- i.mu.Lock()
161- defer i.mu.Unlock()
162- i.hideLocked()
163 }149 }
164 150
165 func (i *TouchpadNavigationIndicator) Destroy() {151 func (i *TouchpadNavigationIndicator) Destroy() {
166 if i == nil {152 if i == nil {
167 return153 return
168 }154 }
169- i.mu.Lock()155+ i.cancelHideTimer()
170- defer i.mu.Unlock()
171- i.cancelHideTimerLocked()
172 if i.container != nil {156 if i.container != nil {
173 i.container.SetVisible(false)157 i.container.SetVisible(false)
174 i.container.Unparent()158 i.container.Unparent()
159+ i.container.Unref()
175 }160 }
176 i.container = nil161 i.container = nil
177 i.icon = nil162 i.icon = nil
@@ -179,27 +164,25 @@ func (i *TouchpadNavigationIndicator) Destroy() {
179 i.bar = nil164 i.bar = nil
180 }165 }
181 166
182-func (i *TouchpadNavigationIndicator) scheduleHideLocked(triggered bool) {167+func (i *TouchpadNavigationIndicator) scheduleHide(triggered bool) {
183 delay := uint(0)168 delay := uint(0)
184 if triggered {169 if triggered {
185 delay = touchpadNavigationIndicatorHideMs170 delay = touchpadNavigationIndicatorHideMs
186 }171 }
172+ if delay == 0 {
173+ i.hide()
174+ return
175+ }
187 cb := glib.SourceFunc(func(_ uintptr) bool {176 cb := glib.SourceFunc(func(_ uintptr) bool {
188- i.mu.Lock()
189- defer i.mu.Unlock()
190 i.hideTimer = 0177 i.hideTimer = 0
191- i.hideLocked()178+ i.hide()
192 return false179 return false
193 })180 })
194- if delay == 0 {
195- i.hideLocked()
196- return
197- }
198 i.hideTimer = glib.TimeoutAdd(delay, &cb, 0)181 i.hideTimer = glib.TimeoutAdd(delay, &cb, 0)
199 }182 }
200 183
201-func (i *TouchpadNavigationIndicator) hideLocked() {184+func (i *TouchpadNavigationIndicator) hide() {
202- i.cancelHideTimerLocked()185+ i.cancelHideTimer()
203 if i.container != nil {186 if i.container != nil {
204 i.container.RemoveCssClass("threshold-reached")187 i.container.RemoveCssClass("threshold-reached")
205 i.container.RemoveCssClass("back")188 i.container.RemoveCssClass("back")
@@ -211,7 +194,7 @@ func (i *TouchpadNavigationIndicator) hideLocked() {
211 }194 }
212 }195 }
213 196
214-func (i *TouchpadNavigationIndicator) cancelHideTimerLocked() {197+func (i *TouchpadNavigationIndicator) cancelHideTimer() {
215 if i.hideTimer != 0 {198 if i.hideTimer != 0 {
216 glib.SourceRemove(i.hideTimer)199 glib.SourceRemove(i.hideTimer)
217 i.hideTimer = 0200 i.hideTimer = 0

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