detached 167c9b2

feat(cef): restrict touchpad navigation to edge swipes

- Only trigger navigation when gesture starts near left/right edge of
the view
- Pass ViewWidth to recognizer and get allocated width from native
widget
- Add tests for edge swipe and in-content scroll not triggering
navigation
commit
167c9b271add355119430061a770bfba6de5ced7
author
brice · · 2026-06-18 08:19 UTC
parent
a9f60b2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +141 -4
M internal/infrastructure/cef/touchpad_navigation.go +34 -4
@@ -12,11 +12,16 @@ const (
1212 defaultTouchpadNavigationCommitDistance = 320.0
1313 defaultTouchpadNavigationVerticalRatio = 0.5
1414 touchpadNavigationIndicatorActivationFraction = 0.5
15+ touchpadNavigationEdgeFraction = 0.15
16+ touchpadNavigationMaxEdgeDistance = 96.0
1517 )
1618
1719 type touchpadNavigationRecognizer struct {
1820 cumulativeDX float64
1921 cumulativeDY float64
22+ gestureStartX float64
23+ gestureViewWidth float64
24+ hasGestureStart bool
2025 thresholdReached bool
2126 verticalCanceled bool
2227 indicatorShown bool
@@ -28,6 +33,7 @@ type touchpadNavigationInput struct {
2833 Config RuntimeInputConfig
2934 CanGoBack bool
3035 CanGoForward bool
36+ ViewWidth float64
3137 }
3238
3339 type touchpadNavigationResult struct {
@@ -49,6 +55,7 @@ func (r *touchpadNavigationRecognizer) Handle(input touchpadNavigationInput) tou
4955 switch input.Event.Phase {
5056 case cef2gtk.ScrollPhaseBegin:
5157 r.reset()
58+ r.setGestureStart(input)
5259 return touchpadNavigationResult{}
5360 case cef2gtk.ScrollPhaseUpdate:
5461 if !isTouchpadNavigationPreciseEvent(input.Event) {
@@ -66,6 +73,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu
6673 if r.verticalCanceled {
6774 return touchpadNavigationResult{}
6875 }
76+ if !r.hasGestureStart {
77+ r.setGestureStart(input)
78+ }
6979
7080 // GTK scroll deltas are inverted compared to browser-history direction:
7181 // negative horizontal surface deltas indicate a back swipe, positive indicate
@@ -137,10 +147,10 @@ func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationI
137147 }
138148
139149 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
140- if r.cumulativeDX > 0 && input.CanGoBack {
150+ if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
141151 return port.TouchpadNavigationBack, true
142152 }
143- if r.cumulativeDX < 0 && input.CanGoForward {
153+ if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
144154 return port.TouchpadNavigationForward, true
145155 }
146156 return port.TouchpadNavigationBack, false
@@ -166,15 +176,32 @@ func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationR
166176 }
167177
168178 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {
169- if r.cumulativeDX > 0 && input.CanGoBack {
179+ if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
170180 return cef2gtk.NavigationSwipeBack, true
171181 }
172- if r.cumulativeDX < 0 && input.CanGoForward {
182+ if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
173183 return cef2gtk.NavigationSwipeForward, true
174184 }
175185 return cef2gtk.NavigationSwipeBack, false
176186 }
177187
188+func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {
189+ r.gestureStartX = input.Event.X
190+ r.gestureViewWidth = input.ViewWidth
191+ r.hasGestureStart = true
192+}
193+
194+func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action port.TouchpadNavigationAction) bool {
195+ if r.gestureViewWidth <= 0 {
196+ return true
197+ }
198+ edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)
199+ if action == port.TouchpadNavigationForward {
200+ return r.gestureStartX >= r.gestureViewWidth-edgeDistance
201+ }
202+ return r.gestureStartX <= edgeDistance
203+}
204+
178205 func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) bool {
179206 absDX, absDY := math.Abs(r.cumulativeDX), math.Abs(r.cumulativeDY)
180207 return absDX == 0 || absDY >= absDX*normalizedTouchpadNavigationRatio(cfg.TouchpadNavigationMaxVerticalRatio)
@@ -183,6 +210,9 @@ func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) boo
183210 func (r *touchpadNavigationRecognizer) reset() {
184211 r.cumulativeDX = 0
185212 r.cumulativeDY = 0
213+ r.gestureStartX = 0
214+ r.gestureViewWidth = 0
215+ r.hasGestureStart = false
186216 r.thresholdReached = false
187217 r.verticalCanceled = false
188218 r.indicatorShown = false
@@ -12,11 +12,16 @@ const (
12 defaultTouchpadNavigationCommitDistance = 320.012 defaultTouchpadNavigationCommitDistance = 320.0
13 defaultTouchpadNavigationVerticalRatio = 0.513 defaultTouchpadNavigationVerticalRatio = 0.5
14 touchpadNavigationIndicatorActivationFraction = 0.514 touchpadNavigationIndicatorActivationFraction = 0.5
15+ touchpadNavigationEdgeFraction = 0.15
16+ touchpadNavigationMaxEdgeDistance = 96.0
15 )17 )
16 18
17 type touchpadNavigationRecognizer struct {19 type touchpadNavigationRecognizer struct {
18 cumulativeDX float6420 cumulativeDX float64
19 cumulativeDY float6421 cumulativeDY float64
22+ gestureStartX float64
23+ gestureViewWidth float64
24+ hasGestureStart bool
20 thresholdReached bool25 thresholdReached bool
21 verticalCanceled bool26 verticalCanceled bool
22 indicatorShown bool27 indicatorShown bool
@@ -28,6 +33,7 @@ type touchpadNavigationInput struct {
28 Config RuntimeInputConfig33 Config RuntimeInputConfig
29 CanGoBack bool34 CanGoBack bool
30 CanGoForward bool35 CanGoForward bool
36+ ViewWidth float64
31 }37 }
32 38
33 type touchpadNavigationResult struct {39 type touchpadNavigationResult struct {
@@ -49,6 +55,7 @@ func (r *touchpadNavigationRecognizer) Handle(input touchpadNavigationInput) tou
49 switch input.Event.Phase {55 switch input.Event.Phase {
50 case cef2gtk.ScrollPhaseBegin:56 case cef2gtk.ScrollPhaseBegin:
51 r.reset()57 r.reset()
58+ r.setGestureStart(input)
52 return touchpadNavigationResult{}59 return touchpadNavigationResult{}
53 case cef2gtk.ScrollPhaseUpdate:60 case cef2gtk.ScrollPhaseUpdate:
54 if !isTouchpadNavigationPreciseEvent(input.Event) {61 if !isTouchpadNavigationPreciseEvent(input.Event) {
@@ -66,6 +73,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu
66 if r.verticalCanceled {73 if r.verticalCanceled {
67 return touchpadNavigationResult{}74 return touchpadNavigationResult{}
68 }75 }
76+ if !r.hasGestureStart {
77+ r.setGestureStart(input)
78+ }
69 79
70 // GTK scroll deltas are inverted compared to browser-history direction:80 // GTK scroll deltas are inverted compared to browser-history direction:
71 // negative horizontal surface deltas indicate a back swipe, positive indicate81 // negative horizontal surface deltas indicate a back swipe, positive indicate
@@ -137,10 +147,10 @@ func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationI
137 }147 }
138 148
139 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {149 func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (port.TouchpadNavigationAction, bool) {
140- if r.cumulativeDX > 0 && input.CanGoBack {150+ if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
141 return port.TouchpadNavigationBack, true151 return port.TouchpadNavigationBack, true
142 }152 }
143- if r.cumulativeDX < 0 && input.CanGoForward {153+ if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
144 return port.TouchpadNavigationForward, true154 return port.TouchpadNavigationForward, true
145 }155 }
146 return port.TouchpadNavigationBack, false156 return port.TouchpadNavigationBack, false
@@ -166,15 +176,32 @@ func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationR
166 }176 }
167 177
168 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {178 func (r *touchpadNavigationRecognizer) navigationAction(input touchpadNavigationInput) (cef2gtk.NavigationSwipeAction, bool) {
169- if r.cumulativeDX > 0 && input.CanGoBack {179+ if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(port.TouchpadNavigationBack) {
170 return cef2gtk.NavigationSwipeBack, true180 return cef2gtk.NavigationSwipeBack, true
171 }181 }
172- if r.cumulativeDX < 0 && input.CanGoForward {182+ if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(port.TouchpadNavigationForward) {
173 return cef2gtk.NavigationSwipeForward, true183 return cef2gtk.NavigationSwipeForward, true
174 }184 }
175 return cef2gtk.NavigationSwipeBack, false185 return cef2gtk.NavigationSwipeBack, false
176 }186 }
177 187
188+func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {
189+ r.gestureStartX = input.Event.X
190+ r.gestureViewWidth = input.ViewWidth
191+ r.hasGestureStart = true
192+}
193+
194+func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action port.TouchpadNavigationAction) bool {
195+ if r.gestureViewWidth <= 0 {
196+ return true
197+ }
198+ edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)
199+ if action == port.TouchpadNavigationForward {
200+ return r.gestureStartX >= r.gestureViewWidth-edgeDistance
201+ }
202+ return r.gestureStartX <= edgeDistance
203+}
204+
178 func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) bool {205 func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) bool {
179 absDX, absDY := math.Abs(r.cumulativeDX), math.Abs(r.cumulativeDY)206 absDX, absDY := math.Abs(r.cumulativeDX), math.Abs(r.cumulativeDY)
180 return absDX == 0 || absDY >= absDX*normalizedTouchpadNavigationRatio(cfg.TouchpadNavigationMaxVerticalRatio)207 return absDX == 0 || absDY >= absDX*normalizedTouchpadNavigationRatio(cfg.TouchpadNavigationMaxVerticalRatio)
@@ -183,6 +210,9 @@ func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) boo
183 func (r *touchpadNavigationRecognizer) reset() {210 func (r *touchpadNavigationRecognizer) reset() {
184 r.cumulativeDX = 0211 r.cumulativeDX = 0
185 r.cumulativeDY = 0212 r.cumulativeDY = 0
213+ r.gestureStartX = 0
214+ r.gestureViewWidth = 0
215+ r.hasGestureStart = false
186 r.thresholdReached = false216 r.thresholdReached = false
187 r.verticalCanceled = false217 r.verticalCanceled = false
188 r.indicatorShown = false218 r.indicatorShown = false
M internal/infrastructure/cef/touchpad_navigation_test.go +71 -0
@@ -103,6 +103,72 @@ func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistanc
103103 require.False(t, end.HasIndicator)
104104 }
105105
106+func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t *testing.T) {
107+ recognizer := newTouchpadNavigationRecognizer()
108+ cfg := RuntimeInputConfig{
109+ TouchpadNavigationEnabled: true,
110+ TouchpadNavigationMinDelta: 200,
111+ TouchpadNavigationMaxVerticalRatio: 0.5,
112+ }
113+
114+ _ = recognizer.Handle(touchpadNavigationInput{
115+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 24, 0, 0),
116+ Config: cfg,
117+ CanGoBack: true,
118+ ViewWidth: 1000,
119+ })
120+ armed := recognizer.Handle(touchpadNavigationInput{
121+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),
122+ Config: cfg,
123+ CanGoBack: true,
124+ ViewWidth: 1000,
125+ })
126+ require.True(t, armed.HasIndicator)
127+ require.True(t, armed.Indicator.ThresholdReached)
128+
129+ end := recognizer.Handle(touchpadNavigationInput{
130+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),
131+ Config: cfg,
132+ CanGoBack: true,
133+ ViewWidth: 1000,
134+ })
135+ require.True(t, end.HasAction)
136+ require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
137+}
138+
139+func TestTouchpadNavigationRecognizer_DoesNotTreatInContentHorizontalScrollAsNavigation(t *testing.T) {
140+ recognizer := newTouchpadNavigationRecognizer()
141+ cfg := RuntimeInputConfig{
142+ TouchpadNavigationEnabled: true,
143+ TouchpadNavigationMinDelta: 200,
144+ TouchpadNavigationMaxVerticalRatio: 0.5,
145+ }
146+
147+ _ = recognizer.Handle(touchpadNavigationInput{
148+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 500, 0, 0),
149+ Config: cfg,
150+ CanGoBack: true,
151+ ViewWidth: 1000,
152+ })
153+ ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
154+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 500, -240, 0),
155+ Config: cfg,
156+ CanGoBack: true,
157+ ViewWidth: 1000,
158+ })
159+ require.False(t, ordinaryScroll.HasIndicator)
160+ require.False(t, ordinaryScroll.HasAction)
161+
162+ end := recognizer.Handle(touchpadNavigationInput{
163+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 500, 0, 0),
164+ Config: cfg,
165+ CanGoBack: true,
166+ ViewWidth: 1000,
167+ })
168+ require.False(t, end.HasIndicator)
169+ require.False(t, end.HasAction)
170+}
171+
106172 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {
107173 recognizer := newTouchpadNavigationRecognizer()
108174 cfg := RuntimeInputConfig{
@@ -173,8 +239,13 @@ func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
173239 }
174240
175241 func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) cef2gtk.ScrollEvent {
242+ return touchpadNavigationScrollEventAt(phase, 0, dx, dy)
243+}
244+
245+func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {
176246 return cef2gtk.ScrollEvent{
177247 Phase: phase,
248+ X: x,
178249 DX: dx,
179250 DY: dy,
180251 Unit: gdk.ScrollUnitSurfaceValue,
@@ -103,6 +103,72 @@ func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistanc
103 require.False(t, end.HasIndicator)103 require.False(t, end.HasIndicator)
104 }104 }
105 105
106+func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t *testing.T) {
107+ recognizer := newTouchpadNavigationRecognizer()
108+ cfg := RuntimeInputConfig{
109+ TouchpadNavigationEnabled: true,
110+ TouchpadNavigationMinDelta: 200,
111+ TouchpadNavigationMaxVerticalRatio: 0.5,
112+ }
113+
114+ _ = recognizer.Handle(touchpadNavigationInput{
115+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 24, 0, 0),
116+ Config: cfg,
117+ CanGoBack: true,
118+ ViewWidth: 1000,
119+ })
120+ armed := recognizer.Handle(touchpadNavigationInput{
121+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),
122+ Config: cfg,
123+ CanGoBack: true,
124+ ViewWidth: 1000,
125+ })
126+ require.True(t, armed.HasIndicator)
127+ require.True(t, armed.Indicator.ThresholdReached)
128+
129+ end := recognizer.Handle(touchpadNavigationInput{
130+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),
131+ Config: cfg,
132+ CanGoBack: true,
133+ ViewWidth: 1000,
134+ })
135+ require.True(t, end.HasAction)
136+ require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
137+}
138+
139+func TestTouchpadNavigationRecognizer_DoesNotTreatInContentHorizontalScrollAsNavigation(t *testing.T) {
140+ recognizer := newTouchpadNavigationRecognizer()
141+ cfg := RuntimeInputConfig{
142+ TouchpadNavigationEnabled: true,
143+ TouchpadNavigationMinDelta: 200,
144+ TouchpadNavigationMaxVerticalRatio: 0.5,
145+ }
146+
147+ _ = recognizer.Handle(touchpadNavigationInput{
148+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 500, 0, 0),
149+ Config: cfg,
150+ CanGoBack: true,
151+ ViewWidth: 1000,
152+ })
153+ ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
154+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 500, -240, 0),
155+ Config: cfg,
156+ CanGoBack: true,
157+ ViewWidth: 1000,
158+ })
159+ require.False(t, ordinaryScroll.HasIndicator)
160+ require.False(t, ordinaryScroll.HasAction)
161+
162+ end := recognizer.Handle(touchpadNavigationInput{
163+ Event: touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 500, 0, 0),
164+ Config: cfg,
165+ CanGoBack: true,
166+ ViewWidth: 1000,
167+ })
168+ require.False(t, end.HasIndicator)
169+ require.False(t, end.HasAction)
170+}
171+
106 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {172 func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {
107 recognizer := newTouchpadNavigationRecognizer()173 recognizer := newTouchpadNavigationRecognizer()
108 cfg := RuntimeInputConfig{174 cfg := RuntimeInputConfig{
@@ -173,8 +239,13 @@ func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
173 }239 }
174 240
175 func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) cef2gtk.ScrollEvent {241 func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) cef2gtk.ScrollEvent {
242+ return touchpadNavigationScrollEventAt(phase, 0, dx, dy)
243+}
244+
245+func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {
176 return cef2gtk.ScrollEvent{246 return cef2gtk.ScrollEvent{
177 Phase: phase,247 Phase: phase,
248+ X: x,
178 DX: dx,249 DX: dx,
179 DY: dy,250 DY: dy,
180 Unit: gdk.ScrollUnitSurfaceValue,251 Unit: gdk.ScrollUnitSurfaceValue,
M internal/infrastructure/cef/webview.go +11 -0
@@ -1515,6 +1515,7 @@ func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
15151515 Config: wv.inputConfig,
15161516 CanGoBack: wv.CanGoBack(),
15171517 CanGoForward: wv.CanGoForward(),
1518+ ViewWidth: wv.touchpadNavigationViewWidth(),
15181519 })
15191520 if result.HasIndicator {
15201521 wv.emitTouchpadNavigationGesture(result.Indicator)
@@ -1524,6 +1525,16 @@ func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
15241525 }
15251526 }
15261527
1528+func (wv *WebView) touchpadNavigationViewWidth() float64 {
1529+ if wv == nil || wv.nativeWidget == nil {
1530+ return 0
1531+ }
1532+ if width := wv.nativeWidget.GetAllocatedWidth(); width > 0 {
1533+ return float64(width)
1534+ }
1535+ return float64(wv.nativeWidget.GetWidth())
1536+}
1537+
15271538 func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigationGesture) {
15281539 if wv == nil {
15291540 return
@@ -1515,6 +1515,7 @@ func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
1515 Config: wv.inputConfig,1515 Config: wv.inputConfig,
1516 CanGoBack: wv.CanGoBack(),1516 CanGoBack: wv.CanGoBack(),
1517 CanGoForward: wv.CanGoForward(),1517 CanGoForward: wv.CanGoForward(),
1518+ ViewWidth: wv.touchpadNavigationViewWidth(),
1518 })1519 })
1519 if result.HasIndicator {1520 if result.HasIndicator {
1520 wv.emitTouchpadNavigationGesture(result.Indicator)1521 wv.emitTouchpadNavigationGesture(result.Indicator)
@@ -1524,6 +1525,16 @@ func (wv *WebView) handleTouchpadNavigationScroll(event cef2gtk.ScrollEvent) {
1524 }1525 }
1525 }1526 }
1526 1527
1528+func (wv *WebView) touchpadNavigationViewWidth() float64 {
1529+ if wv == nil || wv.nativeWidget == nil {
1530+ return 0
1531+ }
1532+ if width := wv.nativeWidget.GetAllocatedWidth(); width > 0 {
1533+ return float64(width)
1534+ }
1535+ return float64(wv.nativeWidget.GetWidth())
1536+}
1537+
1527 func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigationGesture) {1538 func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigationGesture) {
1528 if wv == nil {1539 if wv == nil {
1529 return1540 return
M internal/infrastructure/cef/webview_input_test.go +25 -0
@@ -1,8 +1,11 @@
11 package cef
22
33 import (
4+ "context"
45 "testing"
56
7+ "github.com/bnema/purego-cef2gtk"
8+ "github.com/bnema/puregotk/v4/gdk"
69 "github.com/bnema/puregotk/v4/gtk"
710 "github.com/stretchr/testify/require"
811 )
@@ -43,3 +46,25 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
4346 require.NotNil(t, opts.OnClipboardShortcut)
4447 require.Same(t, nativeWidget, wv.nativeWidget)
4548 }
49+
50+func TestWebViewHandleScrollInput_ForwardsOrdinaryHorizontalScroll(t *testing.T) {
51+ wv := &WebView{
52+ ctx: context.Background(),
53+ inputConfig: RuntimeInputConfig{
54+ TouchpadNavigationEnabled: true,
55+ TouchpadNavigationMinDelta: 200,
56+ TouchpadNavigationMaxVerticalRatio: 0.5,
57+ },
58+ canGoBack: true,
59+ }
60+
61+ decision := wv.handleScrollInput(cef2gtk.ScrollEvent{
62+ Phase: cef2gtk.ScrollPhaseUpdate,
63+ X: 500,
64+ DX: -240,
65+ Unit: gdk.ScrollUnitSurfaceValue,
66+ UnitKnown: true,
67+ })
68+
69+ require.Equal(t, cef2gtk.ScrollForwardToCEF, decision)
70+}
@@ -1,8 +1,11 @@
1 package cef1 package cef
2 2
3 import (3 import (
4+ "context"
4 "testing"5 "testing"
5 6
7+ "github.com/bnema/purego-cef2gtk"
8+ "github.com/bnema/puregotk/v4/gdk"
6 "github.com/bnema/puregotk/v4/gtk"9 "github.com/bnema/puregotk/v4/gtk"
7 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
8 )11 )
@@ -43,3 +46,25 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
43 require.NotNil(t, opts.OnClipboardShortcut)46 require.NotNil(t, opts.OnClipboardShortcut)
44 require.Same(t, nativeWidget, wv.nativeWidget)47 require.Same(t, nativeWidget, wv.nativeWidget)
45 }48 }
49+
50+func TestWebViewHandleScrollInput_ForwardsOrdinaryHorizontalScroll(t *testing.T) {
51+ wv := &WebView{
52+ ctx: context.Background(),
53+ inputConfig: RuntimeInputConfig{
54+ TouchpadNavigationEnabled: true,
55+ TouchpadNavigationMinDelta: 200,
56+ TouchpadNavigationMaxVerticalRatio: 0.5,
57+ },
58+ canGoBack: true,
59+ }
60+
61+ decision := wv.handleScrollInput(cef2gtk.ScrollEvent{
62+ Phase: cef2gtk.ScrollPhaseUpdate,
63+ X: 500,
64+ DX: -240,
65+ Unit: gdk.ScrollUnitSurfaceValue,
66+ UnitKnown: true,
67+ })
68+
69+ require.Equal(t, cef2gtk.ScrollForwardToCEF, decision)
70+}

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 9

Tags 42

↑↓ moveenter openesc close