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 }