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