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