v0.30.0 eb3068f

touchpad_navigation.go RawHistoryFolder

1package cef
2
3import (
4	"math"
5
6	"github.com/bnema/dumber/internal/domain/entity"
7	cef2gtk "github.com/bnema/purego-cef2gtk"
8	"github.com/bnema/puregotk/v4/gdk"
9)
10
11const (
12	defaultTouchpadNavigationCommitDistance       = 320.0
13	defaultTouchpadNavigationVerticalRatio        = 0.5
14	touchpadNavigationIndicatorActivationFraction = 0.5
15	touchpadNavigationEdgeFraction                = 0.15
16	touchpadNavigationMaxEdgeDistance             = 96.0
17)
18
19type touchpadNavigationRecognizer struct {
20	cumulativeDX        float64
21	cumulativeDY        float64
22	gestureStartX       float64
23	gestureViewWidth    float64
24	hasGestureStart     bool
25	thresholdReached    bool
26	verticalCanceled    bool
27	indicatorShown      bool
28	lastIndicatorAction entity.TouchpadNavigationAction
29}
30
31type touchpadNavigationInput struct {
32	Event        cef2gtk.ScrollEvent
33	Config       RuntimeInputConfig
34	CanGoBack    bool
35	CanGoForward bool
36	ViewWidth    float64
37}
38
39type touchpadNavigationResult struct {
40	HasIndicator bool
41	Indicator    entity.TouchpadNavigationGesture
42	HasAction    bool
43	Action       cef2gtk.NavigationSwipeAction
44}
45
46func newTouchpadNavigationRecognizer() *touchpadNavigationRecognizer {
47	return &touchpadNavigationRecognizer{}
48}
49
50func (r *touchpadNavigationRecognizer) Handle(input touchpadNavigationInput) touchpadNavigationResult {
51	if r == nil || !input.Config.TouchpadNavigationEnabled {
52		return touchpadNavigationResult{}
53	}
54
55	switch input.Event.Phase {
56	case cef2gtk.ScrollPhaseBegin:
57		r.reset()
58		r.setGestureStart(input)
59		return touchpadNavigationResult{}
60	case cef2gtk.ScrollPhaseUpdate:
61		if !isTouchpadNavigationPreciseEvent(input.Event) {
62			return touchpadNavigationResult{}
63		}
64		return r.handleUpdate(input)
65	case cef2gtk.ScrollPhaseEnd:
66		return r.handleEnd(input)
67	default:
68		return touchpadNavigationResult{}
69	}
70}
71
72func (r *touchpadNavigationRecognizer) handleUpdate(input touchpadNavigationInput) touchpadNavigationResult {
73	if r.verticalCanceled {
74		return touchpadNavigationResult{}
75	}
76	if !r.hasGestureStart {
77		r.setGestureStart(input)
78	}
79
80	// GTK scroll deltas are inverted compared to browser-history direction:
81	// negative horizontal surface deltas indicate a back swipe, positive indicate
82	// forward. Match the prior bridge behavior while keeping recognition local so
83	// normal scroll forwarding remains untouched.
84	r.cumulativeDX += -input.Event.DX
85	r.cumulativeDY += input.Event.DY
86	if r.isTooVertical(input.Config) {
87		return r.cancelAsTooVertical()
88	}
89
90	action, ok := r.indicatorAction(input)
91	if !ok {
92		return touchpadNavigationResult{}
93	}
94
95	threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta)
96	absDX := math.Abs(r.cumulativeDX)
97	progress := clampFloat(absDX/threshold, 0, 1)
98	r.thresholdReached = absDX > threshold
99	if !r.indicatorShown && progress < touchpadNavigationIndicatorActivationFraction {
100		return touchpadNavigationResult{}
101	}
102	r.indicatorShown = true
103	r.lastIndicatorAction = action
104	return touchpadNavigationResult{
105		HasIndicator: true,
106		Indicator: entity.TouchpadNavigationGesture{
107			Action:           action,
108			Progress:         progress,
109			ThresholdReached: r.thresholdReached,
110			Active:           true,
111		},
112	}
113}
114
115func (r *touchpadNavigationRecognizer) handleEnd(input touchpadNavigationInput) touchpadNavigationResult {
116	defer r.reset()
117	if r.verticalCanceled || !r.thresholdReached || r.isTooVertical(input.Config) {
118		return r.finishIndicator(input, false)
119	}
120	action, ok := r.navigationAction(input)
121	if !ok {
122		return r.finishIndicator(input, false)
123	}
124	result := r.finishIndicator(input, true)
125	result.HasAction = true
126	result.Action = action
127	return result
128}
129
130func (r *touchpadNavigationRecognizer) finishIndicator(input touchpadNavigationInput, triggered bool) touchpadNavigationResult {
131	if !r.indicatorShown {
132		return touchpadNavigationResult{}
133	}
134	action, ok := r.indicatorAction(input)
135	if !ok {
136		action = r.lastIndicatorAction
137	}
138	threshold := normalizedTouchpadNavigationMinDelta(input.Config.TouchpadNavigationMinDelta)
139	return touchpadNavigationResult{
140		HasIndicator: true,
141		Indicator: entity.TouchpadNavigationGesture{
142			Action:           action,
143			Progress:         clampFloat(math.Abs(r.cumulativeDX)/threshold, 0, 1),
144			ThresholdReached: triggered,
145			Active:           false,
146		},
147	}
148}
149
150func (r *touchpadNavigationRecognizer) indicatorAction(input touchpadNavigationInput) (entity.TouchpadNavigationAction, bool) {
151	return r.resolvedDirection(input)
152}
153
154func (r *touchpadNavigationRecognizer) cancelAsTooVertical() touchpadNavigationResult {
155	var action entity.TouchpadNavigationAction
156	hasAction := r.indicatorShown
157	if hasAction {
158		action = r.lastIndicatorAction
159	}
160	r.reset()
161	r.verticalCanceled = true
162	if !hasAction {
163		return touchpadNavigationResult{}
164	}
165	return touchpadNavigationResult{
166		HasIndicator: true,
167		Indicator: entity.TouchpadNavigationGesture{
168			Action: action,
169			Active: false,
170		},
171	}
172}
173
174func (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 == entity.TouchpadNavigationForward {
180		return cef2gtk.NavigationSwipeForward, true
181	}
182	return cef2gtk.NavigationSwipeBack, true
183}
184
185func (r *touchpadNavigationRecognizer) resolvedDirection(input touchpadNavigationInput) (entity.TouchpadNavigationAction, bool) {
186	if r.cumulativeDX > 0 && input.CanGoBack && r.startedAtNavigationEdge(entity.TouchpadNavigationBack) {
187		return entity.TouchpadNavigationBack, true
188	}
189	if r.cumulativeDX < 0 && input.CanGoForward && r.startedAtNavigationEdge(entity.TouchpadNavigationForward) {
190		return entity.TouchpadNavigationForward, true
191	}
192	return entity.TouchpadNavigationBack, false
193}
194
195func (r *touchpadNavigationRecognizer) setGestureStart(input touchpadNavigationInput) {
196	r.gestureStartX = input.Event.X
197	r.gestureViewWidth = input.ViewWidth
198	r.hasGestureStart = true
199}
200
201func (r *touchpadNavigationRecognizer) startedAtNavigationEdge(action entity.TouchpadNavigationAction) bool {
202	if r.gestureViewWidth <= 0 {
203		return false
204	}
205	edgeDistance := math.Min(r.gestureViewWidth*touchpadNavigationEdgeFraction, touchpadNavigationMaxEdgeDistance)
206	if action == entity.TouchpadNavigationForward {
207		return r.gestureStartX >= r.gestureViewWidth-edgeDistance
208	}
209	return r.gestureStartX <= edgeDistance
210}
211
212func (r *touchpadNavigationRecognizer) isTooVertical(cfg RuntimeInputConfig) bool {
213	absDX, absDY := math.Abs(r.cumulativeDX), math.Abs(r.cumulativeDY)
214	return absDX == 0 || absDY >= absDX*normalizedTouchpadNavigationRatio(cfg.TouchpadNavigationMaxVerticalRatio)
215}
216
217func (r *touchpadNavigationRecognizer) reset() {
218	r.cumulativeDX = 0
219	r.cumulativeDY = 0
220	r.gestureStartX = 0
221	r.gestureViewWidth = 0
222	r.hasGestureStart = false
223	r.thresholdReached = false
224	r.verticalCanceled = false
225	r.indicatorShown = false
226	r.lastIndicatorAction = entity.TouchpadNavigationBack
227}
228
229func isTouchpadNavigationPreciseEvent(event cef2gtk.ScrollEvent) bool {
230	return event.UnitKnown && event.Unit == gdk.ScrollUnitSurfaceValue
231}
232
233func normalizedTouchpadNavigationMinDelta(value float64) float64 {
234	if math.IsNaN(value) || math.IsInf(value, 0) || value <= 0 {
235		return defaultTouchpadNavigationCommitDistance
236	}
237	return value
238}
239
240func normalizedTouchpadNavigationRatio(value float64) float64 {
241	if math.IsNaN(value) || math.IsInf(value, 0) || value <= 0 {
242		return defaultTouchpadNavigationVerticalRatio
243	}
244	return value
245}
246
247func clampFloat(value, minValue, maxValue float64) float64 {
248	if value < minValue {
249		return minValue
250	}
251	if value > maxValue {
252		return maxValue
253	}
254	return value
255}

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