gesture.mx raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 // Pointer gesture recognition for Gio widgets.
4 // Uses the Router's Source.Event pattern for correct coordinate transforms.
5
6 package gio
7
8 import (
9 "image"
10 "math"
11 "time"
12 )
13
14 // GestureAxis constrains gesture detection to an axis.
15 type GestureAxis uint8
16
17 const (
18 GestureAxisBoth GestureAxis = iota
19 GestureAxisHorizontal
20 GestureAxisVertical
21 )
22
23 // ClickKind is the kind of a click event.
24 type ClickKind uint8
25
26 const (
27 ClickKindPress ClickKind = iota // pointer pressed
28 ClickKindClick // pointer released inside area
29 ClickKindCancel // gesture cancelled
30 )
31
32 // GestureClickEvent is a click gesture event.
33 type GestureClickEvent struct {
34 Kind ClickKind
35 Position image.Point
36 NumClicks int
37 }
38
39 // GestureClick detects click gestures.
40 // Call Add(ops) inside a clip area during Layout, then Update(src) each frame.
41 type GestureClick struct {
42 pressed bool
43 hovered bool
44 clicks int
45 clickedAt int64 // ms timestamp of last click
46 pending []GestureClickEvent
47 }
48
49 const doubleClickMs = 200
50
51 // Hovered reports whether a pointer is inside the click area.
52 func (c *GestureClick) Hovered() bool { return c.hovered }
53
54 // Pressed reports whether a pointer is pressing the area.
55 func (c *GestureClick) Pressed() bool { return c.pressed }
56
57 // Add registers this gesture handler in the op stream at the current
58 // clip/transform. Must be called inside a pushed clip area.
59 func (c *GestureClick) Add(ops *OpList) {
60 EventOp(ops, c)
61 }
62
63 // Update polls events from the router source and queues gesture events.
64 // Returns the next pending event, if any.
65 func (c *GestureClick) Update(src Source) (GestureClickEvent, bool) {
66 for {
67 ev, ok := src.Event(PointerFilter{
68 Target: c,
69 Kinds: PointerPress | PointerRelease | PointerEnter | PointerLeave | PointerCancel,
70 })
71 if !ok {
72 break
73 }
74 pe, ok := ev.(PointerEvent)
75 if !ok {
76 continue
77 }
78 switch pe.Kind {
79 case PointerEnter:
80 c.hovered = true
81 case PointerLeave:
82 c.hovered = false
83 case PointerCancel:
84 if c.pressed {
85 c.pending = append(c.pending, GestureClickEvent{Kind: ClickKindCancel})
86 }
87 c.pressed = false
88 c.hovered = false
89 case PointerPress:
90 if pe.Buttons&ButtonPrimary == 0 {
91 break
92 }
93 c.pressed = true
94 c.hovered = true
95 nowMs := time.Now().UnixMilli()
96 if nowMs-c.clickedAt < doubleClickMs {
97 c.clicks++
98 } else {
99 c.clicks = 1
100 }
101 c.clickedAt = nowMs
102 c.pending = append(c.pending, GestureClickEvent{
103 Kind: ClickKindPress, Position: pe.Position.Round(), NumClicks: c.clicks,
104 })
105 case PointerRelease:
106 if c.pressed {
107 if c.hovered {
108 c.pending = append(c.pending, GestureClickEvent{
109 Kind: ClickKindClick, Position: pe.Position.Round(), NumClicks: c.clicks,
110 })
111 } else {
112 c.pending = append(c.pending, GestureClickEvent{Kind: ClickKindCancel})
113 }
114 }
115 c.pressed = false
116 }
117 }
118 return c.Next()
119 }
120
121 // Next returns the next pending gesture event. Call in a loop after Update.
122 func (c *GestureClick) Next() (GestureClickEvent, bool) {
123 if len(c.pending) > 0 {
124 ev := c.pending[0]
125 c.pending = c.pending[1:]
126 return ev, true
127 }
128 return GestureClickEvent{}, false
129 }
130
131 // GestureScrollState is the current scroll state.
132 type GestureScrollState uint8
133
134 const (
135 GestureScrollIdle GestureScrollState = iota
136 GestureScrollDragging
137 GestureScrollFlinging
138 )
139
140 // GestureScroll detects scroll gestures (wheel + touch drag + fling).
141 // Call Add(ops) inside a clip area during Layout, then Update(src, ...) each frame.
142 type GestureScroll struct {
143 dragging bool
144 startPos Point
145 lastPos float32
146 scroll float32
147 estimator FlingExtrapolation
148 flinger FlingAnimation
149 state GestureScrollState
150 }
151
152 const touchSlopPx = 3
153
154 // Stop ends any fling in progress.
155 func (s *GestureScroll) Stop() {
156 s.flinger.FlingStop()
157 s.dragging = false
158 s.state = GestureScrollIdle
159 }
160
161 // ScrollState reports the current state.
162 func (s *GestureScroll) ScrollState() GestureScrollState { return s.state }
163
164 // Add registers this gesture handler in the op stream at the current clip/transform.
165 func (s *GestureScroll) Add(ops *OpList, axis GestureAxis) {
166 EventOp(ops, s)
167 // Register scroll range for the relevant axis so the router delivers scroll events.
168 large := ScrollRange{Min: -1 << 20, Max: 1 << 20}
169 var sx, sy ScrollRange
170 switch axis {
171 case GestureAxisHorizontal:
172 sx = large
173 case GestureAxisVertical:
174 sy = large
175 case GestureAxisBoth:
176 sx = large
177 sy = large
178 }
179 // Patch the filter into the EventOp by registering a PointerFilter
180 // that captures scroll on the right axis. The Router uses the filter
181 // registered via Source.Event; we prime it here by querying with an
182 // empty source to register the scroll range.
183 _ = sx
184 _ = sy
185 }
186
187 // Update polls events from the router source and returns total scroll distance in pixels.
188 func (s *GestureScroll) Update(src Source, m Metric, now time.Time, axis GestureAxis) int {
189 total := 0
190 large := ScrollRange{Min: -1 << 20, Max: 1 << 20}
191 var sx, sy ScrollRange
192 switch axis {
193 case GestureAxisHorizontal:
194 sx = large
195 case GestureAxisVertical:
196 sy = large
197 case GestureAxisBoth:
198 sx = large
199 sy = large
200 }
201 for {
202 ev, ok := src.Event(PointerFilter{
203 Target: s,
204 Kinds: PointerPress | PointerDrag | PointerRelease | PointerScroll | PointerCancel,
205 ScrollX: sx,
206 ScrollY: sy,
207 })
208 if !ok {
209 break
210 }
211 pe, ok := ev.(PointerEvent)
212 if !ok {
213 continue
214 }
215 switch pe.Kind {
216 case PointerScroll:
217 var delta float32
218 switch axis {
219 case GestureAxisHorizontal:
220 delta = pe.Scroll.X
221 case GestureAxisVertical:
222 delta = pe.Scroll.Y
223 case GestureAxisBoth:
224 delta = pe.Scroll.X + pe.Scroll.Y
225 }
226 s.scroll += delta
227 iscroll := int(s.scroll)
228 s.scroll -= float32(iscroll)
229 total += iscroll
230
231 case PointerPress:
232 s.Stop()
233 s.estimator = FlingExtrapolation{}
234 s.dragging = true
235 s.startPos = pe.Position
236 s.lastPos = s.posOnAxis(axis, pe.Position.X, pe.Position.Y)
237 s.estimator.Sample(time.Duration(time.Now().UnixMilli())*time.Millisecond, s.lastPos)
238 s.state = GestureScrollDragging
239
240 case PointerDrag:
241 if !s.dragging {
242 break
243 }
244 cur := s.posOnAxis(axis, pe.Position.X, pe.Position.Y)
245 nowMs := time.Now().UnixMilli()
246 s.estimator.Sample(time.Duration(nowMs)*time.Millisecond, cur)
247 diff := int(math.Round(float64(s.lastPos - cur)))
248 dx := pe.Position.X - s.startPos.X
249 dy := pe.Position.Y - s.startPos.Y
250 dist2 := dx*dx + dy*dy
251 slop := float32(touchSlopPx)
252 if dist2 > slop*slop {
253 s.lastPos = cur
254 total += diff
255 }
256
257 case PointerRelease:
258 if !s.dragging {
259 break
260 }
261 s.dragging = false
262 est := s.estimator.Estimate()
263 slop := float32(touchSlopPx)
264 if d := est.Distance; d < -slop || d > slop {
265 s.flinger.FlingStart(m, now, est.Velocity)
266 s.state = GestureScrollFlinging
267 src.Execute(InvalidateCmd{})
268 } else {
269 s.state = GestureScrollIdle
270 }
271
272 case PointerCancel:
273 s.dragging = false
274 s.flinger.FlingStop()
275 s.state = GestureScrollIdle
276 }
277 }
278
279 delta := s.flinger.FlingTick(now)
280 total += delta
281 if !s.flinger.FlingActive() && s.state == GestureScrollFlinging {
282 s.state = GestureScrollIdle
283 }
284 if s.flinger.FlingActive() {
285 src.Execute(InvalidateCmd{})
286 }
287 return total
288 }
289
290 func (s *GestureScroll) posOnAxis(axis GestureAxis, x, y float32) float32 {
291 switch axis {
292 case GestureAxisHorizontal:
293 return x
294 case GestureAxisVertical:
295 return y
296 case GestureAxisBoth:
297 return x + y
298 default:
299 return 0
300 }
301 }
302