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}