gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/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  	"gioui.org/f32"
    19  	"gioui.org/internal/fling"
    20  	"gioui.org/io/event"
    21  	"gioui.org/io/input"
    22  	"gioui.org/io/key"
    23  	"gioui.org/io/pointer"
    24  	"gioui.org/op"
    25  	"gioui.org/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, scrollx, scrolly pointer.ScrollRange) int {
   275  	total := 0
   276  	f := pointer.Filter{
   277  		Target:  s,
   278  		Kinds:   pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
   279  		ScrollX: scrollx,
   280  		ScrollY: scrolly,
   281  	}
   282  	for {
   283  		evt, ok := q.Event(f)
   284  		if !ok {
   285  			break
   286  		}
   287  		e, ok := evt.(pointer.Event)
   288  		if !ok {
   289  			continue
   290  		}
   291  		switch e.Kind {
   292  		case pointer.Press:
   293  			if s.dragging {
   294  				break
   295  			}
   296  			// Only scroll on touch drags, or on Android where mice
   297  			// drags also scroll by convention.
   298  			if e.Source != pointer.Touch && runtime.GOOS != "android" {
   299  				break
   300  			}
   301  			s.Stop()
   302  			s.estimator = fling.Extrapolation{}
   303  			v := s.val(axis, e.Position)
   304  			s.last = int(math.Round(float64(v)))
   305  			s.estimator.Sample(e.Time, v)
   306  			s.dragging = true
   307  			s.pid = e.PointerID
   308  		case pointer.Release:
   309  			if s.pid != e.PointerID {
   310  				break
   311  			}
   312  			fling := s.estimator.Estimate()
   313  			if slop, d := float32(cfg.Dp(touchSlop)), fling.Distance; d < -slop || d > slop {
   314  				s.flinger.Start(cfg, t, fling.Velocity)
   315  			}
   316  			fallthrough
   317  		case pointer.Cancel:
   318  			s.dragging = false
   319  		case pointer.Scroll:
   320  			switch axis {
   321  			case Horizontal:
   322  				s.scroll += e.Scroll.X
   323  			case Vertical:
   324  				s.scroll += e.Scroll.Y
   325  			}
   326  			iscroll := int(s.scroll)
   327  			s.scroll -= float32(iscroll)
   328  			total += iscroll
   329  		case pointer.Drag:
   330  			if !s.dragging || s.pid != e.PointerID {
   331  				continue
   332  			}
   333  			val := s.val(axis, e.Position)
   334  			s.estimator.Sample(e.Time, val)
   335  			v := int(math.Round(float64(val)))
   336  			dist := s.last - v
   337  			if e.Priority < pointer.Grabbed {
   338  				slop := cfg.Dp(touchSlop)
   339  				if dist := dist; dist >= slop || -slop >= dist {
   340  					q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
   341  				}
   342  			} else {
   343  				s.last = v
   344  				total += dist
   345  			}
   346  		}
   347  	}
   348  	total += s.flinger.Tick(t)
   349  	if s.flinger.Active() {
   350  		q.Execute(op.InvalidateCmd{})
   351  	}
   352  	return total
   353  }
   354  
   355  func (s *Scroll) val(axis Axis, p f32.Point) float32 {
   356  	if axis == Horizontal {
   357  		return p.X
   358  	} else {
   359  		return p.Y
   360  	}
   361  }
   362  
   363  // State reports the scroll state.
   364  func (s *Scroll) State() ScrollState {
   365  	switch {
   366  	case s.flinger.Active():
   367  		return StateFlinging
   368  	case s.dragging:
   369  		return StateDragging
   370  	default:
   371  		return StateIdle
   372  	}
   373  }
   374  
   375  // Add the handler to the operation list to receive drag events.
   376  func (d *Drag) Add(ops *op.Ops) {
   377  	event.Op(ops, d)
   378  }
   379  
   380  // Update state and return the next drag event, if any.
   381  func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
   382  	for {
   383  		ev, ok := q.Event(pointer.Filter{
   384  			Target: d,
   385  			Kinds:  pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
   386  		})
   387  		if !ok {
   388  			break
   389  		}
   390  		e, ok := ev.(pointer.Event)
   391  		if !ok {
   392  			continue
   393  		}
   394  
   395  		switch e.Kind {
   396  		case pointer.Press:
   397  			if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
   398  				continue
   399  			}
   400  			d.pressed = true
   401  			if d.dragging {
   402  				continue
   403  			}
   404  			d.dragging = true
   405  			d.pid = e.PointerID
   406  			d.start = e.Position
   407  		case pointer.Drag:
   408  			if !d.dragging || e.PointerID != d.pid {
   409  				continue
   410  			}
   411  			switch axis {
   412  			case Horizontal:
   413  				e.Position.Y = d.start.Y
   414  			case Vertical:
   415  				e.Position.X = d.start.X
   416  			case Both:
   417  				// Do nothing
   418  			}
   419  			if e.Priority < pointer.Grabbed {
   420  				diff := e.Position.Sub(d.start)
   421  				slop := cfg.Dp(touchSlop)
   422  				if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
   423  					q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
   424  				}
   425  			}
   426  		case pointer.Release, pointer.Cancel:
   427  			d.pressed = false
   428  			if !d.dragging || e.PointerID != d.pid {
   429  				continue
   430  			}
   431  			d.dragging = false
   432  		}
   433  
   434  		return e, true
   435  	}
   436  
   437  	return pointer.Event{}, false
   438  }
   439  
   440  // Dragging reports whether it is currently in use.
   441  func (d *Drag) Dragging() bool { return d.dragging }
   442  
   443  // Pressed returns whether a pointer is pressing.
   444  func (d *Drag) Pressed() bool { return d.pressed }
   445  
   446  func (a Axis) String() string {
   447  	switch a {
   448  	case Horizontal:
   449  		return "Horizontal"
   450  	case Vertical:
   451  		return "Vertical"
   452  	default:
   453  		panic("invalid Axis")
   454  	}
   455  }
   456  
   457  func (ct ClickKind) String() string {
   458  	switch ct {
   459  	case KindPress:
   460  		return "KindPress"
   461  	case KindClick:
   462  		return "KindClick"
   463  	case KindCancel:
   464  		return "KindCancel"
   465  	default:
   466  		panic("invalid ClickKind")
   467  	}
   468  }
   469  
   470  func (s ScrollState) String() string {
   471  	switch s {
   472  	case StateIdle:
   473  		return "StateIdle"
   474  	case StateDragging:
   475  		return "StateDragging"
   476  	case StateFlinging:
   477  		return "StateFlinging"
   478  	default:
   479  		panic("unreachable")
   480  	}
   481  }