gesture.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 /*
4 Package gesture implements common pointer gestures.
5
6 Gestures accept low level pointer Events from an event
7 Queue and detect higher level actions such as clicks
8 and scrolling.
9 */
10 package gesture
11
12 import (
13 "image"
14 "math"
15 "runtime"
16 "time"
17
18 "github.com/p9c/p9/pkg/gel/gio/f32"
19 "github.com/p9c/p9/pkg/gel/gio/io/event"
20 "github.com/p9c/p9/pkg/gel/gio/io/key"
21 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
22 "github.com/p9c/p9/pkg/gel/gio/op"
23 "github.com/p9c/p9/pkg/gel/gio/unit"
24
25 "github.com/p9c/p9/pkg/gel/gio/internal/fling"
26 )
27
28 // The duration is somewhat arbitrary.
29 const doubleClickDuration = 300 * time.Millisecond
30
31 // Click detects click gestures in the form
32 // of ClickEvents.
33 type Click struct {
34 // clickedAt is the timestamp at which
35 // the last click occurred.
36 clickedAt time.Duration
37 // clicks is incremented if successive clicks
38 // are performed within a fixed duration.
39 clicks int
40 // pressed tracks whether the pointer is pressed.
41 pressed bool
42 // entered tracks whether the pointer is inside the gesture.
43 entered bool
44 // pid is the pointer.ID.
45 pid pointer.ID
46 Button pointer.Buttons
47 }
48
49 type ClickState uint8
50
51 // ClickEvent represent a click action, either a
52 // TypePress for the beginning of a click or a
53 // TypeClick for a completed click.
54 type ClickEvent struct {
55 Type ClickType
56 Position f32.Point
57 Source pointer.Source
58 Modifiers key.Modifiers
59 // NumClicks records successive clicks occurring
60 // within a short duration of each other.
61 NumClicks int
62 Button pointer.Buttons
63 }
64
65 type ClickType uint8
66
67 // Drag detects drag gestures in the form of pointer.Drag events.
68 type Drag struct {
69 dragging bool
70 pid pointer.ID
71 start f32.Point
72 grab bool
73 }
74
75 // Scroll detects scroll gestures and reduces them to
76 // scroll distances. Scroll recognizes mouse wheel
77 // movements as well as drag and fling touch gestures.
78 type Scroll struct {
79 dragging bool
80 axis Axis
81 estimator fling.Extrapolation
82 flinger fling.Animation
83 pid pointer.ID
84 grab bool
85 last int
86 // Leftover scroll.
87 scroll float32
88 }
89
90 type ScrollState uint8
91
92 type Axis uint8
93
94 const (
95 Horizontal Axis = iota
96 Vertical
97 Both
98 )
99
100 const (
101 // TypePress is reported for the first pointer
102 // press.
103 TypePress ClickType = iota
104 // TypeClick is reported when a click action
105 // is complete.
106 TypeClick
107 // TypeCancel is reported when the gesture is
108 // cancelled.
109 TypeCancel
110 )
111
112 const (
113 // StateIdle is the default scroll state.
114 StateIdle ScrollState = iota
115 // StateDrag is reported during drag gestures.
116 StateDragging
117 // StateFlinging is reported when a fling is
118 // in progress.
119 StateFlinging
120 )
121
122 var touchSlop = unit.Dp(3)
123
124 // Add the handler to the operation list to receive click events.
125 func (c *Click) Add(ops *op.Ops) {
126 op := pointer.InputOp{
127 Tag: c,
128 Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
129 }
130 op.Add(ops)
131 }
132
133 // Hovered returns whether a pointer is inside the area.
134 func (c *Click) Hovered() bool {
135 return c.entered
136 }
137
138 // Pressed returns whether a pointer is pressing.
139 func (c *Click) Pressed() bool {
140 return c.pressed
141 }
142
143 // Events returns the next click event, if any.
144 func (c *Click) Events(q event.Queue) []ClickEvent {
145 var events []ClickEvent
146 for _, evt := range q.Events(c) {
147 // I.S(evt)
148 e, ok := evt.(pointer.Event)
149 if !ok {
150 continue
151 }
152 switch e.Type {
153 case pointer.Release:
154 if !c.pressed || c.pid != e.PointerID {
155 break
156 }
157 c.pressed = false
158 if c.entered {
159 if e.Time-c.clickedAt < doubleClickDuration ||
160 (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) {
161 c.clicks++
162 } else {
163 c.clicks = 1
164 }
165 c.clickedAt = e.Time
166 events = append(events, ClickEvent{
167 Type: TypeClick, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers,
168 Button: e.Buttons, NumClicks: c.clicks,
169 })
170 } else {
171 events = append(events, ClickEvent{Type: TypeCancel})
172 }
173 case pointer.Cancel:
174 wasPressed := c.pressed
175 c.pressed = false
176 c.entered = false
177 if wasPressed {
178 events = append(events, ClickEvent{Type: TypeCancel})
179 }
180 case pointer.Press:
181 if c.pressed {
182 break
183 }
184 // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
185 // break
186 // }
187 if !c.entered {
188 c.pid = e.PointerID
189 }
190 if c.pid != e.PointerID {
191 break
192 }
193 c.pressed = true
194 events = append(events, ClickEvent{
195 Type: TypePress, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers, Button: e.Buttons,
196 })
197 case pointer.Leave:
198 if !c.pressed {
199 c.pid = e.PointerID
200 }
201 if c.pid == e.PointerID {
202 c.entered = false
203 }
204 case pointer.Enter:
205 if !c.pressed {
206 c.pid = e.PointerID
207 }
208 if c.pid == e.PointerID {
209 c.entered = true
210 }
211 }
212 }
213 return events
214 }
215
216 func (ClickEvent) ImplementsEvent() {}
217
218 // Add the handler to the operation list to receive scroll events.
219 func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
220 oph := pointer.InputOp{
221 Tag: s,
222 Grab: s.grab,
223 Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
224 ScrollBounds: bounds,
225 }
226 oph.Add(ops)
227 if s.flinger.Active() {
228 op.InvalidateOp{}.Add(ops)
229 }
230 }
231
232 // Stop any remaining fling movement.
233 func (s *Scroll) Stop() {
234 s.flinger = fling.Animation{}
235 }
236
237 // Scroll detects the scrolling distance from the available events and
238 // ongoing fling gestures.
239 func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
240 if s.axis != axis {
241 s.axis = axis
242 return 0
243 }
244 total := 0
245 for _, evt := range q.Events(s) {
246 e, ok := evt.(pointer.Event)
247 if !ok {
248 continue
249 }
250 switch e.Type {
251 case pointer.Press:
252 if s.dragging {
253 break
254 }
255 // Only scroll on touch drags, or on Android where mice
256 // drags also scroll by convention.
257 if e.Source != pointer.Touch && runtime.GOOS != "android" {
258 break
259 }
260 s.Stop()
261 s.estimator = fling.Extrapolation{}
262 v := s.val(e.Position)
263 s.last = int(math.Round(float64(v)))
264 s.estimator.Sample(e.Time, v)
265 s.dragging = true
266 s.pid = e.PointerID
267 case pointer.Release:
268 if s.pid != e.PointerID {
269 break
270 }
271 fling := s.estimator.Estimate()
272 if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop {
273 s.flinger.Start(cfg, t, fling.Velocity)
274 }
275 fallthrough
276 case pointer.Cancel:
277 s.dragging = false
278 s.grab = false
279 case pointer.Scroll:
280 switch s.axis {
281 case Horizontal:
282 s.scroll += e.Scroll.X
283 case Vertical:
284 s.scroll += e.Scroll.Y
285 }
286 iscroll := int(s.scroll)
287 s.scroll -= float32(iscroll)
288 total += iscroll
289 case pointer.Drag:
290 if !s.dragging || s.pid != e.PointerID {
291 continue
292 }
293 val := s.val(e.Position)
294 s.estimator.Sample(e.Time, val)
295 v := int(math.Round(float64(val)))
296 dist := s.last - v
297 if e.Priority < pointer.Grabbed {
298 slop := cfg.Px(touchSlop)
299 if dist := dist; dist >= slop || -slop >= dist {
300 s.grab = true
301 }
302 } else {
303 s.last = v
304 total += dist
305 }
306 }
307 }
308 total += s.flinger.Tick(t)
309 return total
310 }
311
312 func (s *Scroll) val(p f32.Point) float32 {
313 if s.axis == Horizontal {
314 return p.X
315 } else {
316 return p.Y
317 }
318 }
319
320 // State reports the scroll state.
321 func (s *Scroll) State() ScrollState {
322 switch {
323 case s.flinger.Active():
324 return StateFlinging
325 case s.dragging:
326 return StateDragging
327 default:
328 return StateIdle
329 }
330 }
331
332 // Add the handler to the operation list to receive drag events.
333 func (d *Drag) Add(ops *op.Ops) {
334 op := pointer.InputOp{
335 Tag: d,
336 Grab: d.grab,
337 Types: pointer.Press | pointer.Drag | pointer.Release,
338 }
339 op.Add(ops)
340 }
341
342 // Events returns the next drag events, if any.
343 func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
344 var events []pointer.Event
345 for _, e := range q.Events(d) {
346 e, ok := e.(pointer.Event)
347 if !ok {
348 continue
349 }
350
351 switch e.Type {
352 case pointer.Press:
353 if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
354 continue
355 }
356 if d.dragging {
357 continue
358 }
359 d.dragging = true
360 d.pid = e.PointerID
361 d.start = e.Position
362 case pointer.Drag:
363 if !d.dragging || e.PointerID != d.pid {
364 continue
365 }
366 switch axis {
367 case Horizontal:
368 e.Position.Y = d.start.Y
369 case Vertical:
370 e.Position.X = d.start.X
371 case Both:
372 // Do nothing
373 }
374 if e.Priority < pointer.Grabbed {
375 diff := e.Position.Sub(d.start)
376 slop := cfg.Px(touchSlop)
377 if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
378 d.grab = true
379 }
380 }
381 case pointer.Release, pointer.Cancel:
382 if !d.dragging || e.PointerID != d.pid {
383 continue
384 }
385 d.dragging = false
386 d.grab = false
387 }
388
389 events = append(events, e)
390 }
391
392 return events
393 }
394
395 // Dragging reports whether it's currently in use.
396 func (d *Drag) Dragging() bool { return d.dragging }
397
398 func (a Axis) String() string {
399 switch a {
400 case Horizontal:
401 return "Horizontal"
402 case Vertical:
403 return "Vertical"
404 default:
405 panic("invalid Axis")
406 }
407 }
408
409 func (ct ClickType) String() string {
410 switch ct {
411 case TypePress:
412 return "TypePress"
413 case TypeClick:
414 return "TypeClick"
415 case TypeCancel:
416 return "TypeCancel"
417 default:
418 panic("invalid ClickType")
419 }
420 }
421
422 func (s ScrollState) String() string {
423 switch s {
424 case StateIdle:
425 return "StateIdle"
426 case StateDragging:
427 return "StateDragging"
428 case StateFlinging:
429 return "StateFlinging"
430 default:
431 panic("unreachable")
432 }
433 }
434