touchpad_navigation_indicator.go RawHistoryFolder

1package component
2
3import (
4	"github.com/bnema/dumber/internal/domain/entity"
5	"github.com/bnema/puregotk/v4/glib"
6	"github.com/bnema/puregotk/v4/gtk"
7)
8
9const (
10	touchpadNavigationIndicatorHideMs       = 220
11	touchpadNavigationIndicatorSpacing      = 8
12	touchpadNavigationIndicatorHeaderGap    = 10
13	touchpadNavigationIndicatorMargin       = 32
14	touchpadNavigationIndicatorWidth        = 156
15	touchpadNavigationIndicatorBarHeight    = 5
16	touchpadNavigationIndicatorIconMinChars = 2
17	touchpadNavigationIndicatorIconXAlign   = 0.5
18)
19
20// TouchpadNavigationIndicator is a lightweight overlay that makes CEF
21// two-finger history navigation deliberate: it appears during the swipe,
22// fills toward the commit threshold, and briefly shows the triggered state.
23type TouchpadNavigationIndicator struct {
24	container *gtk.Box
25	icon      *gtk.Label
26	label     *gtk.Label
27	bar       *gtk.ProgressBar
28
29	hideTimer uint
30}
31
32func NewTouchpadNavigationIndicator() *TouchpadNavigationIndicator {
33	container := gtk.NewBox(gtk.OrientationVerticalValue, touchpadNavigationIndicatorSpacing)
34	if container == nil {
35		return nil
36	}
37	container.AddCssClass("touchpad-navigation-indicator")
38	container.SetValign(gtk.AlignCenterValue)
39	container.SetHalign(gtk.AlignStartValue)
40	container.SetHexpand(false)
41	container.SetVexpand(false)
42	container.SetMarginStart(touchpadNavigationIndicatorMargin)
43	container.SetMarginEnd(touchpadNavigationIndicatorMargin)
44	container.SetCanTarget(false)
45	container.SetCanFocus(false)
46	container.SetVisible(false)
47
48	icon, label, ok := appendTouchpadNavigationHeader(container)
49	if !ok {
50		container.Unref()
51		return nil
52	}
53
54	bar := gtk.NewProgressBar()
55	if bar == nil {
56		container.Unref()
57		return nil
58	}
59	bar.AddCssClass("touchpad-navigation-progress")
60	bar.SetSizeRequest(touchpadNavigationIndicatorWidth, touchpadNavigationIndicatorBarHeight)
61	bar.SetFraction(0)
62	bar.SetCanTarget(false)
63	bar.SetCanFocus(false)
64	container.Append(&bar.Widget)
65
66	return &TouchpadNavigationIndicator{
67		container: container,
68		icon:      icon,
69		label:     label,
70		bar:       bar,
71	}
72}
73
74func appendTouchpadNavigationHeader(container *gtk.Box) (*gtk.Label, *gtk.Label, bool) {
75	header := gtk.NewBox(gtk.OrientationHorizontalValue, touchpadNavigationIndicatorHeaderGap)
76	if header == nil {
77		return nil, nil, false
78	}
79	header.AddCssClass("touchpad-navigation-header")
80	header.SetCanTarget(false)
81	header.SetCanFocus(false)
82	container.Append(&header.Widget)
83
84	iconText := "←"
85	icon := gtk.NewLabel(&iconText)
86	if icon == nil {
87		return nil, nil, false
88	}
89	icon.AddCssClass("touchpad-navigation-icon")
90	icon.SetWidthChars(touchpadNavigationIndicatorIconMinChars)
91	icon.SetXalign(touchpadNavigationIndicatorIconXAlign)
92	icon.SetCanTarget(false)
93	icon.SetCanFocus(false)
94	header.Append(&icon.Widget)
95
96	text := "Slide Back"
97	label := gtk.NewLabel(&text)
98	if label == nil {
99		return nil, nil, false
100	}
101	label.AddCssClass("touchpad-navigation-label")
102	label.SetXalign(0)
103	label.SetCanTarget(false)
104	label.SetCanFocus(false)
105	header.Append(&label.Widget)
106
107	return icon, label, true
108}
109
110func (i *TouchpadNavigationIndicator) Widget() *gtk.Widget {
111	if i == nil || i.container == nil {
112		return nil
113	}
114	return &i.container.Widget
115}
116
117func (i *TouchpadNavigationIndicator) ShowGesture(gesture entity.TouchpadNavigationGesture) {
118	if i == nil || i.container == nil || i.icon == nil || i.label == nil || i.bar == nil {
119		return
120	}
121	i.cancelHideTimer()
122
123	i.container.RemoveCssClass("hiding")
124	i.container.RemoveCssClass("back")
125	i.container.RemoveCssClass("forward")
126	if gesture.Action == entity.TouchpadNavigationForward {
127		i.container.AddCssClass("forward")
128		i.container.SetHalign(gtk.AlignEndValue)
129		i.bar.SetInverted(true)
130	} else {
131		i.container.AddCssClass("back")
132		i.container.SetHalign(gtk.AlignStartValue)
133		i.bar.SetInverted(false)
134	}
135
136	if gesture.ThresholdReached {
137		i.container.AddCssClass("threshold-reached")
138	} else {
139		i.container.RemoveCssClass("threshold-reached")
140	}
141
142	i.icon.SetLabel(touchpadNavigationIndicatorIcon(gesture))
143	i.label.SetLabel(touchpadNavigationIndicatorLabel(gesture))
144	i.bar.SetFraction(clampIndicatorProgress(gesture.Progress))
145	i.container.SetVisible(true)
146
147	if !gesture.Active {
148		i.scheduleHide(gesture.ThresholdReached)
149	}
150}
151
152func (i *TouchpadNavigationIndicator) Destroy() {
153	if i == nil {
154		return
155	}
156	i.cancelHideTimer()
157	if i.container != nil {
158		i.container.SetVisible(false)
159		i.container.Unparent()
160		i.container.Unref()
161	}
162	i.container = nil
163	i.icon = nil
164	i.label = nil
165	i.bar = nil
166}
167
168func (i *TouchpadNavigationIndicator) scheduleHide(triggered bool) {
169	delay := uint(0)
170	if triggered {
171		delay = touchpadNavigationIndicatorHideMs
172	}
173	if delay == 0 {
174		i.hide()
175		return
176	}
177	if i.container != nil {
178		i.container.AddCssClass("hiding")
179	}
180	cb := glib.SourceFunc(func(_ uintptr) bool {
181		i.hideTimer = 0
182		i.hide()
183		return false
184	})
185	i.hideTimer = glib.TimeoutAdd(delay, &cb, 0)
186}
187
188func (i *TouchpadNavigationIndicator) hide() {
189	i.cancelHideTimer()
190	if i.container != nil {
191		i.container.RemoveCssClass("hiding")
192		i.container.RemoveCssClass("threshold-reached")
193		i.container.RemoveCssClass("back")
194		i.container.RemoveCssClass("forward")
195		i.container.SetVisible(false)
196	}
197	if i.bar != nil {
198		i.bar.SetFraction(0)
199	}
200}
201
202func (i *TouchpadNavigationIndicator) cancelHideTimer() {
203	if i.hideTimer != 0 {
204		glib.SourceRemove(i.hideTimer)
205		i.hideTimer = 0
206	}
207}
208
209func touchpadNavigationIndicatorIcon(gesture entity.TouchpadNavigationGesture) string {
210	if gesture.Action == entity.TouchpadNavigationForward {
211		return "→"
212	}
213	return "←"
214}
215
216func touchpadNavigationIndicatorLabel(gesture entity.TouchpadNavigationGesture) string {
217	suffix := "Back"
218	if gesture.Action == entity.TouchpadNavigationForward {
219		suffix = "Forward"
220	}
221	if gesture.ThresholdReached {
222		return "Release for " + suffix
223	}
224	return "Slide " + suffix
225}
226
227func clampIndicatorProgress(progress float64) float64 {
228	if progress < 0 {
229		return 0
230	}
231	if progress > 1 {
232		return 1
233	}
234	return progress
235}

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