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 }