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