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