v0.33.1 9577fee

touchpad_navigation_test.go RawHistoryFolder

1package cef
2
3import (
4	"testing"
5
6	"github.com/bnema/dumber/internal/domain/entity"
7	cef2gtk "github.com/bnema/purego-cef2gtk"
8	"github.com/bnema/puregotk/v4/gdk"
9	"github.com/stretchr/testify/require"
10)
11
12func TestTouchpadNavigationRecognizer_ReportsProgressAndNavigatesOnReleaseAfterThreshold(t *testing.T) {
13	recognizer := newTouchpadNavigationRecognizer()
14	cfg := RuntimeInputConfig{
15		TouchpadNavigationEnabled:          true,
16		TouchpadNavigationMinDelta:         200,
17		TouchpadNavigationMaxVerticalRatio: 0.5,
18	}
19
20	begin := recognizer.Handle(touchpadNavigationInput{
21		Event:        touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
22		Config:       cfg,
23		CanGoBack:    true,
24		CanGoForward: true,
25		ViewWidth:    1000,
26	})
27	require.False(t, begin.HasIndicator)
28	require.False(t, begin.HasAction)
29
30	ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
31		Event:        touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0),
32		Config:       cfg,
33		CanGoBack:    true,
34		CanGoForward: true,
35	})
36	require.False(t, ordinaryScroll.HasIndicator, "ordinary horizontal scrolling below the activation distance should not show navigation UI")
37	require.False(t, ordinaryScroll.HasAction)
38
39	progress := recognizer.Handle(touchpadNavigationInput{
40		Event:        touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -40, 0),
41		Config:       cfg,
42		CanGoBack:    true,
43		CanGoForward: true,
44	})
45	require.True(t, progress.HasIndicator)
46	require.Equal(t, entity.TouchpadNavigationBack, progress.Indicator.Action)
47	require.True(t, progress.Indicator.Active)
48	require.InDelta(t, 0.6, progress.Indicator.Progress, 0.001)
49	require.False(t, progress.Indicator.ThresholdReached)
50	require.False(t, progress.HasAction)
51
52	armed := recognizer.Handle(touchpadNavigationInput{
53		Event:        touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -100, 0),
54		Config:       cfg,
55		CanGoBack:    true,
56		CanGoForward: true,
57	})
58	require.True(t, armed.HasIndicator)
59	require.Equal(t, entity.TouchpadNavigationBack, armed.Indicator.Action)
60	require.True(t, armed.Indicator.Active)
61	require.InDelta(t, 1.0, armed.Indicator.Progress, 0.001)
62	require.True(t, armed.Indicator.ThresholdReached)
63	require.False(t, armed.HasAction, "navigation waits for gesture release so the indicator is visible first")
64
65	end := recognizer.Handle(touchpadNavigationInput{
66		Event:        touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
67		Config:       cfg,
68		CanGoBack:    true,
69		CanGoForward: true,
70	})
71	require.True(t, end.HasIndicator)
72	require.False(t, end.Indicator.Active)
73	require.True(t, end.HasAction)
74	require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
75}
76
77func 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, entity.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
120func 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
154func TestTouchpadNavigationRecognizer_DoesNotShowIndicatorBelowActivationDistance(t *testing.T) {
155	recognizer := newTouchpadNavigationRecognizer()
156	cfg := RuntimeInputConfig{
157		TouchpadNavigationEnabled:          true,
158		TouchpadNavigationMinDelta:         200,
159		TouchpadNavigationMaxVerticalRatio: 0.5,
160	}
161
162	_ = recognizer.Handle(touchpadNavigationInput{
163		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
164		Config:    cfg,
165		CanGoBack: true,
166		ViewWidth: 1000,
167	})
168	ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
169		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -80, 0),
170		Config:    cfg,
171		CanGoBack: true,
172	})
173	require.False(t, ordinaryScroll.HasIndicator)
174	require.False(t, ordinaryScroll.HasAction)
175
176	end := recognizer.Handle(touchpadNavigationInput{
177		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
178		Config:    cfg,
179		CanGoBack: true,
180	})
181	require.False(t, end.HasAction)
182	require.False(t, end.HasIndicator)
183}
184
185func TestTouchpadNavigationRecognizer_AllowsIntentionalEdgeSwipeWithViewWidth(t *testing.T) {
186	recognizer := newTouchpadNavigationRecognizer()
187	cfg := RuntimeInputConfig{
188		TouchpadNavigationEnabled:          true,
189		TouchpadNavigationMinDelta:         200,
190		TouchpadNavigationMaxVerticalRatio: 0.5,
191	}
192
193	_ = recognizer.Handle(touchpadNavigationInput{
194		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 24, 0, 0),
195		Config:    cfg,
196		CanGoBack: true,
197		ViewWidth: 1000,
198	})
199	armed := recognizer.Handle(touchpadNavigationInput{
200		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 24, -240, 0),
201		Config:    cfg,
202		CanGoBack: true,
203	})
204	require.True(t, armed.HasIndicator)
205	require.True(t, armed.Indicator.ThresholdReached)
206
207	end := recognizer.Handle(touchpadNavigationInput{
208		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 24, 0, 0),
209		Config:    cfg,
210		CanGoBack: true,
211	})
212	require.True(t, end.HasAction)
213	require.Equal(t, cef2gtk.NavigationSwipeBack, end.Action)
214}
215
216func TestTouchpadNavigationRecognizer_DoesNotTreatInContentHorizontalScrollAsNavigation(t *testing.T) {
217	recognizer := newTouchpadNavigationRecognizer()
218	cfg := RuntimeInputConfig{
219		TouchpadNavigationEnabled:          true,
220		TouchpadNavigationMinDelta:         200,
221		TouchpadNavigationMaxVerticalRatio: 0.5,
222	}
223
224	_ = recognizer.Handle(touchpadNavigationInput{
225		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseBegin, 500, 0, 0),
226		Config:    cfg,
227		CanGoBack: true,
228		ViewWidth: 1000,
229	})
230	ordinaryScroll := recognizer.Handle(touchpadNavigationInput{
231		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseUpdate, 500, -240, 0),
232		Config:    cfg,
233		CanGoBack: true,
234		ViewWidth: 1000,
235	})
236	require.False(t, ordinaryScroll.HasIndicator)
237	require.False(t, ordinaryScroll.HasAction)
238
239	end := recognizer.Handle(touchpadNavigationInput{
240		Event:     touchpadNavigationScrollEventAt(cef2gtk.ScrollPhaseEnd, 500, 0, 0),
241		Config:    cfg,
242		CanGoBack: true,
243		ViewWidth: 1000,
244	})
245	require.False(t, end.HasIndicator)
246	require.False(t, end.HasAction)
247}
248
249func 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
300func 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
334func 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
365func 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
396func TestTouchpadNavigationRecognizer_HidesIndicatorWithoutNavigatingWhenReleasedAfterActivationBelowThreshold(t *testing.T) {
397	recognizer := newTouchpadNavigationRecognizer()
398	cfg := RuntimeInputConfig{
399		TouchpadNavigationEnabled:          true,
400		TouchpadNavigationMinDelta:         200,
401		TouchpadNavigationMaxVerticalRatio: 0.5,
402	}
403
404	_ = recognizer.Handle(touchpadNavigationInput{
405		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
406		Config:    cfg,
407		CanGoBack: true,
408		ViewWidth: 1000,
409	})
410	progress := recognizer.Handle(touchpadNavigationInput{
411		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -120, 0),
412		Config:    cfg,
413		CanGoBack: true,
414	})
415	require.True(t, progress.HasIndicator)
416	require.InDelta(t, 0.6, progress.Indicator.Progress, 0.001)
417	require.False(t, progress.Indicator.ThresholdReached)
418
419	end := recognizer.Handle(touchpadNavigationInput{
420		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
421		Config:    cfg,
422		CanGoBack: true,
423	})
424	require.False(t, end.HasAction)
425	require.True(t, end.HasIndicator)
426	require.False(t, end.Indicator.Active)
427}
428
429func 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, entity.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
479func TestTouchpadNavigationRecognizer_CancelsVerticalGestures(t *testing.T) {
480	recognizer := newTouchpadNavigationRecognizer()
481	cfg := RuntimeInputConfig{
482		TouchpadNavigationEnabled:          true,
483		TouchpadNavigationMinDelta:         200,
484		TouchpadNavigationMaxVerticalRatio: 0.5,
485	}
486
487	_ = recognizer.Handle(touchpadNavigationInput{
488		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
489		Config:    cfg,
490		CanGoBack: true,
491		ViewWidth: 1000,
492	})
493	vertical := recognizer.Handle(touchpadNavigationInput{
494		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -60, 80),
495		Config:    cfg,
496		CanGoBack: true,
497	})
498	require.False(t, vertical.HasIndicator)
499	require.False(t, vertical.HasAction)
500
501	continued := recognizer.Handle(touchpadNavigationInput{
502		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -400, 0),
503		Config:    cfg,
504		CanGoBack: true,
505	})
506	require.False(t, continued.HasIndicator)
507	require.False(t, continued.HasAction)
508
509	end := recognizer.Handle(touchpadNavigationInput{
510		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
511		Config:    cfg,
512		CanGoBack: true,
513	})
514	require.False(t, end.HasAction)
515}
516
517func TestTouchpadNavigationRecognizer_HidesVisibleIndicatorOnVerticalCancelAndResets(t *testing.T) {
518	recognizer := newTouchpadNavigationRecognizer()
519	cfg := RuntimeInputConfig{
520		TouchpadNavigationEnabled:          true,
521		TouchpadNavigationMinDelta:         200,
522		TouchpadNavigationMaxVerticalRatio: 0.5,
523	}
524
525	_ = recognizer.Handle(touchpadNavigationInput{
526		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
527		Config:    cfg,
528		CanGoBack: true,
529		ViewWidth: 1000,
530	})
531	shown := recognizer.Handle(touchpadNavigationInput{
532		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -120, 0),
533		Config:    cfg,
534		CanGoBack: true,
535	})
536	require.True(t, shown.HasIndicator)
537	require.True(t, shown.Indicator.Active)
538
539	canceled := recognizer.Handle(touchpadNavigationInput{
540		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, 0, 80),
541		Config:    cfg,
542		CanGoBack: true,
543	})
544	require.True(t, canceled.HasIndicator)
545	require.False(t, canceled.Indicator.Active)
546	require.False(t, canceled.HasAction)
547
548	end := recognizer.Handle(touchpadNavigationInput{
549		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
550		Config:    cfg,
551		CanGoBack: true,
552	})
553	require.False(t, end.HasIndicator)
554	require.False(t, end.HasAction)
555
556	_ = recognizer.Handle(touchpadNavigationInput{
557		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseBegin, 0, 0),
558		Config:    cfg,
559		CanGoBack: true,
560		ViewWidth: 1000,
561	})
562	valid := recognizer.Handle(touchpadNavigationInput{
563		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseUpdate, -240, 0),
564		Config:    cfg,
565		CanGoBack: true,
566	})
567	require.True(t, valid.HasIndicator)
568	validEnd := recognizer.Handle(touchpadNavigationInput{
569		Event:     touchpadNavigationScrollEvent(cef2gtk.ScrollPhaseEnd, 0, 0),
570		Config:    cfg,
571		CanGoBack: true,
572	})
573	require.True(t, validEnd.HasAction)
574	require.Equal(t, cef2gtk.NavigationSwipeBack, validEnd.Action)
575}
576
577func touchpadNavigationScrollEvent(phase cef2gtk.ScrollPhase, dx, dy float64) cef2gtk.ScrollEvent {
578	return touchpadNavigationScrollEventAt(phase, 0, dx, dy)
579}
580
581// ViewWidth is provided separately on touchpadNavigationInput; the recognizer
582// caches it from the initial Begin event for subsequent Update and End phases.
583func touchpadNavigationScrollEventAt(phase cef2gtk.ScrollPhase, x, dx, dy float64) cef2gtk.ScrollEvent {
584	return cef2gtk.ScrollEvent{
585		Phase:     phase,
586		X:         x,
587		DX:        dx,
588		DY:        dy,
589		Unit:      gdk.ScrollUnitSurfaceValue,
590		UnitKnown: true,
591	}
592}

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