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  }