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  }