gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/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  	"math"
    14  
    15  	"gioui.org/ui"
    16  	"gioui.org/ui/f32"
    17  	"gioui.org/ui/internal/fling"
    18  	"gioui.org/ui/pointer"
    19  )
    20  
    21  // Click detects click gestures in the form
    22  // of ClickEvents.
    23  type Click struct {
    24  	// state tracks the gesture state.
    25  	state ClickState
    26  }
    27  
    28  type ClickState uint8
    29  
    30  // ClickEvent represent a click action, either a
    31  // TypePress for the beginning of a click or a
    32  // TypeClick for a completed click.
    33  type ClickEvent struct {
    34  	Type     ClickType
    35  	Position f32.Point
    36  	Source   pointer.Source
    37  }
    38  
    39  type ClickType uint8
    40  
    41  // Scroll detects scroll gestures and reduces them to
    42  // scroll distances. Scroll recognizes mouse wheel
    43  // movements as well as drag and fling touch gestures.
    44  type Scroll struct {
    45  	dragging  bool
    46  	axis      Axis
    47  	estimator fling.Extrapolation
    48  	flinger   fling.Animation
    49  	pid       pointer.ID
    50  	grab      bool
    51  	last      int
    52  	// Leftover scroll.
    53  	scroll float32
    54  }
    55  
    56  type ScrollState uint8
    57  
    58  type Axis uint8
    59  
    60  const (
    61  	Horizontal Axis = iota
    62  	Vertical
    63  )
    64  
    65  const (
    66  	// StateNormal is the default click state.
    67  	StateNormal ClickState = iota
    68  	// StateFocused is reported when a pointer
    69  	// is hovering over the handler.
    70  	StateFocused
    71  	// StatePressed is then a pointer is pressed.
    72  	StatePressed
    73  )
    74  
    75  const (
    76  	// TypePress is reported for the first pointer
    77  	// press.
    78  	TypePress ClickType = iota
    79  	// TypeClick is reporoted when a click action
    80  	// is complete.
    81  	TypeClick
    82  )
    83  
    84  const (
    85  	// StateIdle is the default scroll state.
    86  	StateIdle ScrollState = iota
    87  	// StateDrag is reported during drag gestures.
    88  	StateDragging
    89  	// StateFlinging is reported when a fling is
    90  	// in progress.
    91  	StateFlinging
    92  )
    93  
    94  var touchSlop = ui.Dp(3)
    95  
    96  // Add the handler to the operation list to receive click events.
    97  func (c *Click) Add(ops *ui.Ops) {
    98  	op := pointer.InputOp{Key: c}
    99  	op.Add(ops)
   100  }
   101  
   102  // State reports the click state.
   103  func (c *Click) State() ClickState {
   104  	return c.state
   105  }
   106  
   107  // Events returns the next click event, if any.
   108  func (c *Click) Events(q ui.Queue) []ClickEvent {
   109  	var events []ClickEvent
   110  	for _, evt := range q.Events(c) {
   111  		e, ok := evt.(pointer.Event)
   112  		if !ok {
   113  			continue
   114  		}
   115  		switch e.Type {
   116  		case pointer.Release:
   117  			wasPressed := c.state == StatePressed
   118  			c.state = StateNormal
   119  			if wasPressed {
   120  				events = append(events, ClickEvent{Type: TypeClick, Position: e.Position, Source: e.Source})
   121  			}
   122  		case pointer.Cancel:
   123  			c.state = StateNormal
   124  		case pointer.Press:
   125  			if c.state == StatePressed || !e.Hit {
   126  				break
   127  			}
   128  			c.state = StatePressed
   129  			events = append(events, ClickEvent{Type: TypePress, Position: e.Position, Source: e.Source})
   130  		case pointer.Move:
   131  			if c.state == StatePressed && !e.Hit {
   132  				c.state = StateNormal
   133  			} else if c.state < StateFocused {
   134  				c.state = StateFocused
   135  			}
   136  		}
   137  	}
   138  	return events
   139  }
   140  
   141  // Add the handler to the operation list to receive scroll events.
   142  func (s *Scroll) Add(ops *ui.Ops) {
   143  	oph := pointer.InputOp{Key: s, Grab: s.grab}
   144  	oph.Add(ops)
   145  	if s.flinger.Active() {
   146  		ui.InvalidateOp{}.Add(ops)
   147  	}
   148  }
   149  
   150  // Stop any remaining fling movement.
   151  func (s *Scroll) Stop() {
   152  	s.flinger = fling.Animation{}
   153  }
   154  
   155  // Scroll detects the scrolling distance from the available events and
   156  // ongoing fling gestures.
   157  func (s *Scroll) Scroll(cfg ui.Config, q ui.Queue, axis Axis) int {
   158  	if s.axis != axis {
   159  		s.axis = axis
   160  		return 0
   161  	}
   162  	total := 0
   163  	for _, evt := range q.Events(s) {
   164  		e, ok := evt.(pointer.Event)
   165  		if !ok {
   166  			continue
   167  		}
   168  		switch e.Type {
   169  		case pointer.Press:
   170  			if s.dragging || e.Source != pointer.Touch {
   171  				break
   172  			}
   173  			s.Stop()
   174  			s.estimator = fling.Extrapolation{}
   175  			v := s.val(e.Position)
   176  			s.last = int(math.Round(float64(v)))
   177  			s.estimator.Sample(e.Time, v)
   178  			s.dragging = true
   179  			s.pid = e.PointerID
   180  		case pointer.Release:
   181  			if s.pid != e.PointerID {
   182  				break
   183  			}
   184  			fling := s.estimator.Estimate()
   185  			if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop {
   186  				s.flinger.Start(cfg, fling.Velocity)
   187  			}
   188  			fallthrough
   189  		case pointer.Cancel:
   190  			s.dragging = false
   191  			s.grab = false
   192  		case pointer.Move:
   193  			// Scroll
   194  			switch s.axis {
   195  			case Horizontal:
   196  				s.scroll += e.Scroll.X
   197  			case Vertical:
   198  				s.scroll += e.Scroll.Y
   199  			}
   200  			iscroll := int(math.Round(float64(s.scroll)))
   201  			s.scroll -= float32(iscroll)
   202  			total += iscroll
   203  			if !s.dragging || s.pid != e.PointerID {
   204  				continue
   205  			}
   206  			// Drag
   207  			val := s.val(e.Position)
   208  			s.estimator.Sample(e.Time, val)
   209  			v := int(math.Round(float64(val)))
   210  			dist := s.last - v
   211  			if e.Priority < pointer.Grabbed {
   212  				slop := cfg.Px(touchSlop)
   213  				if dist := dist; dist >= slop || -slop >= dist {
   214  					s.grab = true
   215  				}
   216  			} else {
   217  				s.last = v
   218  				total += dist
   219  			}
   220  		}
   221  	}
   222  	total += s.flinger.Tick(cfg.Now())
   223  	return total
   224  }
   225  
   226  func (s *Scroll) val(p f32.Point) float32 {
   227  	if s.axis == Horizontal {
   228  		return p.X
   229  	} else {
   230  		return p.Y
   231  	}
   232  }
   233  
   234  // State reports the scroll state.
   235  func (s *Scroll) State() ScrollState {
   236  	switch {
   237  	case s.flinger.Active():
   238  		return StateFlinging
   239  	case s.dragging:
   240  		return StateDragging
   241  	default:
   242  		return StateIdle
   243  	}
   244  }
   245  
   246  func (a Axis) String() string {
   247  	switch a {
   248  	case Horizontal:
   249  		return "Horizontal"
   250  	case Vertical:
   251  		return "Vertical"
   252  	default:
   253  		panic("invalid Axis")
   254  	}
   255  }
   256  
   257  func (ct ClickType) String() string {
   258  	switch ct {
   259  	case TypePress:
   260  		return "TypePress"
   261  	case TypeClick:
   262  		return "TypeClick"
   263  	default:
   264  		panic("invalid ClickType")
   265  	}
   266  }
   267  
   268  func (cs ClickState) String() string {
   269  	switch cs {
   270  	case StateNormal:
   271  		return "StateNormal"
   272  	case StateFocused:
   273  		return "StateFocused"
   274  	case StatePressed:
   275  		return "StatePressed"
   276  	default:
   277  		panic("invalid ClickState")
   278  	}
   279  }
   280  
   281  func (s ScrollState) String() string {
   282  	switch s {
   283  	case StateIdle:
   284  		return "StateIdle"
   285  	case StateDragging:
   286  		return "StateDragging"
   287  	case StateFlinging:
   288  		return "StateFlinging"
   289  	default:
   290  		panic("unreachable")
   291  	}
   292  }