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