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  }