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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 12 | 12 | defaultTouchpadNavigationCommitDistance = 320.0 | |
| 13 | 13 | defaultTouchpadNavigationVerticalRatio = 0.5 | |
| 14 | 14 | touchpadNavigationIndicatorActivationFraction = 0.5 | |
| 15 | + | touchpadNavigationEdgeFraction = 0.15 | |
| 16 | + | touchpadNavigationMaxEdgeDistance = 96.0 | |
| 15 | 17 | ) | |
| 16 | 18 | ||
| 17 | 19 | type touchpadNavigationRecognizer struct { | |
| 18 | 20 | cumulativeDX float64 | |
| 19 | 21 | cumulativeDY float64 | |
| 22 | + | gestureStartX float64 | |
| 23 | + | gestureViewWidth float64 | |
| 24 | + | hasGestureStart bool | |
| 20 | 25 | thresholdReached bool | |
| 21 | 26 | verticalCanceled bool | |
| 22 | 27 | indicatorShown bool | |
| @@ -28,6 +33,7 @@ type touchpadNavigationInput struct { | |||
| 28 | 33 | Config RuntimeInputConfig | |
| 29 | 34 | CanGoBack bool | |
| 30 | 35 | CanGoForward bool | |
| 36 | + | ViewWidth float64 | |
| 31 | 37 | } | |
| 32 | 38 | ||
| 33 | 39 | type touchpadNavigationResult struct { | |
| @@ -49,6 +55,7 @@ func (r *touchpadNavigationRecognizer) Handle(input touchpadNavigationInput) tou | |||
| 49 | 55 | switch input.Event.Phase { | |
| 50 | 56 | case cef2gtk.ScrollPhaseBegin: | |
| 51 | 57 | r.reset() | |
| 58 | + | r.setGestureStart(input) | |
| 52 | 59 | return touchpadNavigationResult{} | |
| 53 | 60 | case cef2gtk.ScrollPhaseUpdate: | |
| 54 | 61 | if !isTouchpadNavigationPreciseEvent(input.Event) { | |
| @@ -66,6 +73,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu | |||
| 66 | 73 | if r.verticalCanceled { | |
| 67 | 74 | return touchpadNavigationResult{} | |
| 68 | 75 | } | |
| 76 | + | if !r.hasGestureStart { | |
| 77 | + | r.setGestureStart(input) | |
| 78 | + | } | |
| 69 | 79 | ||
| 70 | 80 | // GTK scroll deltas are inverted compared to browser-history direction: | |
| 71 | 81 | // 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 | 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 | 151 | 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 | 154 | return port.TouchpadNavigationForward, true | |
| 145 | 155 | } | |
| 146 | 156 | return port.TouchpadNavigationBack, false | |
| @@ -166,15 +176,32 @@ func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationR | |||
| 166 | 176 | } | |
| 167 | 177 | ||
| 168 | 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 | 180 | 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 | 183 | return cef2gtk.NavigationSwipeForward, true | |
| 174 | 184 | } | |
| 175 | 185 | 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 | 205 | func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) bool { | |
| 179 | 206 | absDX, absDY := math.Abs(r.cumulativeDX), math.Abs(r.cumulativeDY) | |
| 180 | 207 | return absDX == 0 || absDY >= absDX*normalizedTouchpadNavigationRatio(cfg.TouchpadNavigationMaxVerticalRatio) | |
| @@ -183,6 +210,9 @@ func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) boo | |||
| 183 | 210 | func (r *touchpadNavigationRecognizer) reset() { | |
| 184 | 211 | r.cumulativeDX = 0 | |
| 185 | 212 | r.cumulativeDY = 0 | |
| 213 | + | r.gestureStartX = 0 | |
| 214 | + | r.gestureViewWidth = 0 | |
| 215 | + | r.hasGestureStart = false | |
| 186 | 216 | r.thresholdReached = false | |
| 187 | 217 | r.verticalCanceled = false | |
| 188 | 218 | r.indicatorShown = false | |
| @@ -12,11 +12,16 @@ const ( | |||||
| 12 | defaultTouchpadNavigationCommitDistance = 320.0 | 12 | defaultTouchpadNavigationCommitDistance = 320.0 | ||
| 13 | defaultTouchpadNavigationVerticalRatio = 0.5 | 13 | defaultTouchpadNavigationVerticalRatio = 0.5 | ||
| 14 | touchpadNavigationIndicatorActivationFraction = 0.5 | 14 | 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 float64 | 20 | cumulativeDX float64 | ||
| 19 | cumulativeDY float64 | 21 | cumulativeDY float64 | ||
| 22 | + | gestureStartX float64 | |||
| 23 | + | gestureViewWidth float64 | |||
| 24 | + | hasGestureStart bool | |||
| 20 | thresholdReached bool | 25 | thresholdReached bool | ||
| 21 | verticalCanceled bool | 26 | verticalCanceled bool | ||
| 22 | indicatorShown bool | 27 | indicatorShown bool | ||
| @@ -28,6 +33,7 @@ type touchpadNavigationInput struct { | |||||
| 28 | Config RuntimeInputConfig | 33 | Config RuntimeInputConfig | ||
| 29 | CanGoBack bool | 34 | CanGoBack bool | ||
| 30 | CanGoForward bool | 35 | 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 indicate | 81 | // 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, true | 151 | 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, true | 154 | return port.TouchpadNavigationForward, true | ||
| 145 | } | 155 | } | ||
| 146 | return port.TouchpadNavigationBack, false | 156 | 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, true | 180 | 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, true | 183 | return cef2gtk.NavigationSwipeForward, true | ||
| 174 | } | 184 | } | ||
| 175 | return cef2gtk.NavigationSwipeBack, false | 185 | 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 = 0 | 211 | r.cumulativeDX = 0 | ||
| 185 | r.cumulativeDY = 0 | 212 | r.cumulativeDY = 0 | ||
| 213 | + | r.gestureStartX = 0 | |||
| 214 | + | r.gestureViewWidth = 0 | |||
| 215 | + | r.hasGestureStart = false | |||
| 186 | r.thresholdReached = false | 216 | r.thresholdReached = false | ||
| 187 | r.verticalCanceled = false | 217 | r.verticalCanceled = false | ||
| 188 | r.indicatorShown = false | 218 | r.indicatorShown = false | ||
M internal/infrastructure/cef/touchpad_navigation_test.go +71 -0
| @@ -103,6 +103,72 @@ func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistanc | |||
| 103 | 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 | 172 | func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) { | |
| 107 | 173 | recognizer := newTouchpadNavigationRecognizer() | |
| 108 | 174 | cfg := RuntimeInputConfig{ | |
| @@ -173,8 +239,13 @@ func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) { | |||
| 173 | 239 | } | |
| 174 | 240 | ||
| 175 | 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 | 246 | return cef2gtk.ScrollEvent{ | |
| 177 | 247 | Phase: phase, | |
| 248 | + | X: x, | |
| 178 | 249 | DX: dx, | |
| 179 | 250 | DY: dy, | |
| 180 | 251 | 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) { | |||
| 1515 | 1515 | Config: wv.inputConfig, | |
| 1516 | 1516 | CanGoBack: wv.CanGoBack(), | |
| 1517 | 1517 | CanGoForward: wv.CanGoForward(), | |
| 1518 | + | ViewWidth: wv.touchpadNavigationViewWidth(), | |
| 1518 | 1519 | }) | |
| 1519 | 1520 | if result.HasIndicator { | |
| 1520 | 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 | 1538 | func (wv *WebView) emitTouchpadNavigationGesture(gesture port.TouchpadNavigationGesture) { | |
| 1528 | 1539 | if wv == nil { | |
| 1529 | 1540 | 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 | return | 1540 | return | ||
M internal/infrastructure/cef/webview_input_test.go +25 -0
| @@ -1,8 +1,11 @@ | |||
| 1 | 1 | package cef | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "testing" | |
| 5 | 6 | ||
| 7 | + | "github.com/bnema/purego-cef2gtk" | |
| 8 | + | "github.com/bnema/puregotk/v4/gdk" | |
| 6 | 9 | "github.com/bnema/puregotk/v4/gtk" | |
| 7 | 10 | "github.com/stretchr/testify/require" | |
| 8 | 11 | ) | |
| @@ -43,3 +46,25 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T) | |||
| 43 | 46 | require.NotNil(t, opts.OnClipboardShortcut) | |
| 44 | 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 | + | } | |
| @@ -1,8 +1,11 @@ | |||||
| 1 | package cef | 1 | 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 | + | } | |||