fix(cef): delay touchpad navigation indicator
- commit
- a9f60b26a32458f0653e3b7e3007746c77a98a0a
- author
- brice · · 2026-06-18 07:30 UTC
- parent
- 4cf5b9e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +53 -10
M internal/infrastructure/cef/touchpad_navigation.go +10 -6
| @@ -9,8 +9,9 @@ import ( | |||
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | 11 | const ( | |
| 12 | - | defaultTouchpadNavigationCommitDistance = 320.0 | |
| 13 | - | defaultTouchpadNavigationVerticalRatio = 0.5 | |
| 12 | + | defaultTouchpadNavigationCommitDistance = 320.0 | |
| 13 | + | defaultTouchpadNavigationVerticalRatio = 0.5 | |
| 14 | + | touchpadNavigationIndicatorActivationFraction = 0.5 | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| 16 | 17 | type touchpadNavigationRecognizer struct { | |
| @@ -84,6 +85,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu | |||
| 84 | 85 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | |
| 85 | 86 | progress := clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1) | |
| 86 | 87 | r.thresholdReached = progress >= 1 | |
| 88 | + | if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction { | |
| 89 | + | return touchpadNavigationResult{} | |
| 90 | + | } | |
| 87 | 91 | r.indicatorShown = true | |
| 88 | 92 | r.lastIndicatorAction = action | |
| 89 | 93 | return touchpadNavigationResult{ | |
| @@ -113,12 +117,12 @@ func (r *touchpadNavigationRecognizer) handleEnd(input touchpadNavigationInput) | |||
| 113 | 117 | } | |
| 114 | 118 | ||
| 115 | 119 | func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationInput, triggered bool) touchpadNavigationResult { | |
| 116 | - | action, ok := r.indicatorAction(input) | |
| 117 | - | if !ok && r.indicatorShown { | |
| 118 | - | action, ok = r.lastIndicatorAction, true | |
| 120 | + | if !r.indicatorShown { | |
| 121 | + | return touchpadNavigationResult{} | |
| 119 | 122 | } | |
| 123 | + | action, ok := r.indicatorAction(input) | |
| 120 | 124 | if !ok { | |
| 121 | - | return touchpadNavigationResult{} | |
| 125 | + | action, ok = r.lastIndicatorAction, true | |
| 122 | 126 | } | |
| 123 | 127 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | |
| 124 | 128 | return touchpadNavigationResult{ | |
| @@ -9,8 +9,9 @@ import ( | |||||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | const ( | 11 | const ( | ||
| 12 | - | defaultTouchpadNavigationCommitDistance = 320.0 | 12 | + | defaultTouchpadNavigationCommitDistance = 320.0 |
| 13 | - | defaultTouchpadNavigationVerticalRatio = 0.5 | 13 | + | defaultTouchpadNavigationVerticalRatio = 0.5 |
| 14 | + | touchpadNavigationIndicatorActivationFraction = 0.5 | |||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| 16 | type touchpadNavigationRecognizer struct { | 17 | type touchpadNavigationRecognizer struct { | ||
| @@ -84,6 +85,9 @@ func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInpu | |||||
| 84 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | 85 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | ||
| 85 | progress := clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1) | 86 | progress := clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1) | ||
| 86 | r.thresholdReached = progress >= 1 | 87 | r.thresholdReached = progress >= 1 | ||
| 88 | + | if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction { | |||
| 89 | + | return touchpadNavigationResult{} | |||
| 90 | + | } | |||
| 87 | r.indicatorShown = true | 91 | r.indicatorShown = true | ||
| 88 | r.lastIndicatorAction = action | 92 | r.lastIndicatorAction = action | ||
| 89 | return touchpadNavigationResult{ | 93 | return touchpadNavigationResult{ | ||
| @@ -113,12 +117,12 @@ func (r *touchpadNavigationRecognizer) handleEnd(input touchpadNavigationInput) | |||||
| 113 | } | 117 | } | ||
| 114 | 118 | ||||
| 115 | func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationInput, triggered bool) touchpadNavigationResult { | 119 | func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationInput, triggered bool) touchpadNavigationResult { | ||
| 116 | - | action, ok := r.indicatorAction(input) | 120 | + | if !r.indicatorShown { |
| 117 | - | if !ok && r.indicatorShown { | 121 | + | return touchpadNavigationResult{} |
| 118 | - | action, ok = r.lastIndicatorAction, true | |||
| 119 | } | 122 | } | ||
| 123 | + | action, ok := r.indicatorAction(input) | |||
| 120 | if !ok { | 124 | if !ok { | ||
| 121 | - | return touchpadNavigationResult{} | 125 | + | action, ok = r.lastIndicatorAction, true |
| 122 | } | 126 | } | ||
| 123 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | 127 | threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta) | ||
| 124 | return touchpadNavigationResult{ | 128 | return touchpadNavigationResult{ | ||
M internal/infrastructure/cef/touchpad_navigation_test.go +43 -4
| @@ -26,21 +26,30 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT | |||
| 26 | 26 | require.False(t, begin.HasIndicator) | |
| 27 | 27 | require.False(t, begin.HasAction) | |
| 28 | 28 | ||
| 29 | - | progress := recognizer.Handle(touchpadNavigationInput{ | |
| 29 | + | ordinaryScroll := recognizer.Handle(touchpadNavigationInput{ | |
| 30 | 30 | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0), | |
| 31 | 31 | Config: cfg, | |
| 32 | 32 | CanGoBack: true, | |
| 33 | 33 | CanGoForward: true, | |
| 34 | 34 | }) | |
| 35 | + | require.False(t, ordinaryScroll.HasIndicator, "ordinary horizontal scrolling below the activation distance should not show navigation UI") | |
| 36 | + | require.False(t, ordinaryScroll.HasAction) | |
| 37 | + | ||
| 38 | + | progress := recognizer.Handle(touchpadNavigationInput{ | |
| 39 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -40, 0), | |
| 40 | + | Config: cfg, | |
| 41 | + | CanGoBack: true, | |
| 42 | + | CanGoForward: true, | |
| 43 | + | }) | |
| 35 | 44 | require.True(t, progress.HasIndicator) | |
| 36 | 45 | require.Equal(t, port.TouchpadNavigationBack, progress.Indicator.Action) | |
| 37 | 46 | require.True(t, progress.Indicator.Active) | |
| 38 | - | require.InDelta(t, 0.4, progress.Indicator.Progress, 0.001) | |
| 47 | + | require.InDelta(t, 0.6, progress.Indicator.Progress, 0.001) | |
| 39 | 48 | require.False(t, progress.Indicator.ThresholdReached) | |
| 40 | 49 | require.False(t, progress.HasAction) | |
| 41 | 50 | ||
| 42 | 51 | armed := recognizer.Handle(touchpadNavigationInput{ | |
| 43 | - | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -140, 0), | |
| 52 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -100, 0), | |
| 44 | 53 | Config: cfg, | |
| 45 | 54 | CanGoBack: true, | |
| 46 | 55 | CanGoForward: true, | |
| @@ -64,7 +73,37 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT | |||
| 64 | 73 | require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action) | |
| 65 | 74 | } | |
| 66 | 75 | ||
| 67 | - | func TestTouchpadNavigationRecognizer_DoesNotNavigateBelowThreshold(t *testing.T) { | |
| 76 | + | func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) { | |
| 77 | + | recognizer := newTouchpadNavigationRecognizer() | |
| 78 | + | cfg := RuntimeInputConfig{ | |
| 79 | + | TouchpadNavigationEnabled: true, | |
| 80 | + | TouchpadNavigationMinDelta: 200, | |
| 81 | + | TouchpadNavigationMaxVerticalRatio: 0.5, | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | _ = recognizer.Handle(touchpadNavigationInput{ | |
| 85 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0), | |
| 86 | + | Config: cfg, | |
| 87 | + | CanGoBack: true, | |
| 88 | + | }) | |
| 89 | + | ordinaryScroll := recognizer.Handle(touchpadNavigationInput{ | |
| 90 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0), | |
| 91 | + | Config: cfg, | |
| 92 | + | CanGoBack: true, | |
| 93 | + | }) | |
| 94 | + | require.False(t, ordinaryScroll.HasIndicator) | |
| 95 | + | require.False(t, ordinaryScroll.HasAction) | |
| 96 | + | ||
| 97 | + | end := recognizer.Handle(touchpadNavigationInput{ | |
| 98 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0), | |
| 99 | + | Config: cfg, | |
| 100 | + | CanGoBack: true, | |
| 101 | + | }) | |
| 102 | + | require.False(t, end.HasAction) | |
| 103 | + | require.False(t, end.HasIndicator) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) { | |
| 68 | 107 | recognizer := newTouchpadNavigationRecognizer() | |
| 69 | 108 | cfg := RuntimeInputConfig{ | |
| 70 | 109 | TouchpadNavigationEnabled: true, | |
| @@ -26,21 +26,30 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT | |||||
| 26 | require.False(t, begin.HasIndicator) | 26 | require.False(t, begin.HasIndicator) | ||
| 27 | require.False(t, begin.HasAction) | 27 | require.False(t, begin.HasAction) | ||
| 28 | 28 | ||||
| 29 | - | progress := recognizer.Handle(touchpadNavigationInput{ | 29 | + | ordinaryScroll := recognizer.Handle(touchpadNavigationInput{ |
| 30 | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0), | 30 | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0), | ||
| 31 | Config: cfg, | 31 | Config: cfg, | ||
| 32 | CanGoBack: true, | 32 | CanGoBack: true, | ||
| 33 | CanGoForward: true, | 33 | CanGoForward: true, | ||
| 34 | }) | 34 | }) | ||
| 35 | + | require.False(t, ordinaryScroll.HasIndicator, "ordinary horizontal scrolling below the activation distance should not show navigation UI") | |||
| 36 | + | require.False(t, ordinaryScroll.HasAction) | |||
| 37 | + | ||||
| 38 | + | progress := recognizer.Handle(touchpadNavigationInput{ | |||
| 39 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -40, 0), | |||
| 40 | + | Config: cfg, | |||
| 41 | + | CanGoBack: true, | |||
| 42 | + | CanGoForward: true, | |||
| 43 | + | }) | |||
| 35 | require.True(t, progress.HasIndicator) | 44 | require.True(t, progress.HasIndicator) | ||
| 36 | require.Equal(t, port.TouchpadNavigationBack, progress.Indicator.Action) | 45 | require.Equal(t, port.TouchpadNavigationBack, progress.Indicator.Action) | ||
| 37 | require.True(t, progress.Indicator.Active) | 46 | require.True(t, progress.Indicator.Active) | ||
| 38 | - | require.InDelta(t, 0.4, progress.Indicator.Progress, 0.001) | 47 | + | require.InDelta(t, 0.6, progress.Indicator.Progress, 0.001) |
| 39 | require.False(t, progress.Indicator.ThresholdReached) | 48 | require.False(t, progress.Indicator.ThresholdReached) | ||
| 40 | require.False(t, progress.HasAction) | 49 | require.False(t, progress.HasAction) | ||
| 41 | 50 | ||||
| 42 | armed := recognizer.Handle(touchpadNavigationInput{ | 51 | armed := recognizer.Handle(touchpadNavigationInput{ | ||
| 43 | - | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -140, 0), | 52 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -100, 0), |
| 44 | Config: cfg, | 53 | Config: cfg, | ||
| 45 | CanGoBack: true, | 54 | CanGoBack: true, | ||
| 46 | CanGoForward: true, | 55 | CanGoForward: true, | ||
| @@ -64,7 +73,37 @@ func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterT | |||||
| 64 | require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action) | 73 | require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action) | ||
| 65 | } | 74 | } | ||
| 66 | 75 | ||||
| 67 | - | func TestTouchpadNavigationRecognizer_DoesNotNavigateBelowThreshold(t *testing.T) { | 76 | + | func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) { |
| 77 | + | recognizer := newTouchpadNavigationRecognizer() | |||
| 78 | + | cfg := RuntimeInputConfig{ | |||
| 79 | + | TouchpadNavigationEnabled: true, | |||
| 80 | + | TouchpadNavigationMinDelta: 200, | |||
| 81 | + | TouchpadNavigationMaxVerticalRatio: 0.5, | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | _ = recognizer.Handle(touchpadNavigationInput{ | |||
| 85 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0), | |||
| 86 | + | Config: cfg, | |||
| 87 | + | CanGoBack: true, | |||
| 88 | + | }) | |||
| 89 | + | ordinaryScroll := recognizer.Handle(touchpadNavigationInput{ | |||
| 90 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0), | |||
| 91 | + | Config: cfg, | |||
| 92 | + | CanGoBack: true, | |||
| 93 | + | }) | |||
| 94 | + | require.False(t, ordinaryScroll.HasIndicator) | |||
| 95 | + | require.False(t, ordinaryScroll.HasAction) | |||
| 96 | + | ||||
| 97 | + | end := recognizer.Handle(touchpadNavigationInput{ | |||
| 98 | + | Event: touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0), | |||
| 99 | + | Config: cfg, | |||
| 100 | + | CanGoBack: true, | |||
| 101 | + | }) | |||
| 102 | + | require.False(t, end.HasAction) | |||
| 103 | + | require.False(t, end.HasIndicator) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) { | |||
| 68 | recognizer := newTouchpadNavigationRecognizer() | 107 | recognizer := newTouchpadNavigationRecognizer() | ||
| 69 | cfg := RuntimeInputConfig{ | 108 | cfg := RuntimeInputConfig{ | ||
| 70 | TouchpadNavigationEnabled: true, | 109 | TouchpadNavigationEnabled: true, | ||