| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package gesture provides gesture events such as long presses and drags. |
| // These are higher level than underlying mouse and touch events. |
| package gesture |
| |
| import ( |
| "fmt" |
| "time" |
| |
| "golang.org/x/exp/shiny/screen" |
| "golang.org/x/mobile/event/mouse" |
| ) |
| |
| // TODO: handle touch events, not just mouse events. |
| // |
| // TODO: multi-button / multi-touch gestures such as pinch, rotate and tilt? |
| |
| const ( |
| // TODO: use a resolution-independent unit such as DIPs or Millimetres? |
| dragThreshold = 10 // Pixels. |
| |
| doublePressThreshold = 300 * time.Millisecond |
| longPressThreshold = 500 * time.Millisecond |
| ) |
| |
| // Type describes the type of a touch event. |
| type Type uint8 |
| |
| const ( |
| // TypeStart and TypeEnd are the start and end of a gesture. A gesture |
| // spans multiple events. |
| TypeStart Type = 0 |
| TypeEnd Type = 1 |
| |
| // TypeIsXxx is when the gesture is recognized as a long press, double |
| // press or drag. For example, a mouse button press won't generate a |
| // TypeIsLongPress immediately, but if a threshold duration passes without |
| // the corresponding mouse button release, a TypeIsLongPress event is sent. |
| // |
| // Once a TypeIsXxx event is sent, the corresponding Event.Xxx bool field |
| // is set for this and subsequent events. For example, a TypeTap event by |
| // itself doesn't say whether or not it is a single tap or the first tap of |
| // a double tap. If the app needs to distinguish these two sorts of taps, |
| // it can wait until a TypeEnd or TypeIsDoublePress event is seen. If a |
| // TypeEnd is seen before TypeIsDoublePress, or equivalently, if the |
| // TypeEnd event's DoublePress field is false, the gesture is a single tap. |
| // |
| // These attributes aren't exclusive. A long press drag is perfectly valid. |
| // |
| // The uncommon "double press" instead of "double tap" terminology is |
| // because, in this package, taps are associated with button releases, not |
| // button presses. Note also that "double" really means "at least two". |
| TypeIsLongPress Type = 10 |
| TypeIsDoublePress Type = 11 |
| TypeIsDrag Type = 12 |
| |
| // TypeTap and TypeDrag are tap and drag events. |
| // |
| // For 'flinging' drags, to simulate inertia, look to the Velocity field of |
| // the TypeEnd event. |
| // |
| // TODO: implement velocity. |
| TypeTap Type = 20 |
| TypeDrag Type = 21 |
| |
| // All internal types are >= typeInternal. |
| typeInternal Type = 100 |
| |
| // The typeXxxSchedule and typeXxxResolve constants are used for the two |
| // step process for sending an event after a timeout, in a separate |
| // goroutine. There are two steps so that the spawned goroutine is |
| // guaranteed to execute only after any other EventDeque.SendFirst calls |
| // are made for the one underlying mouse or touch event. |
| |
| typeDoublePressSchedule Type = 100 |
| typeDoublePressResolve Type = 101 |
| |
| typeLongPressSchedule Type = 110 |
| typeLongPressResolve Type = 111 |
| ) |
| |
| func (t Type) String() string { |
| switch t { |
| case TypeStart: |
| return "Start" |
| case TypeEnd: |
| return "End" |
| case TypeIsLongPress: |
| return "IsLongPress" |
| case TypeIsDoublePress: |
| return "IsDoublePress" |
| case TypeIsDrag: |
| return "IsDrag" |
| case TypeTap: |
| return "Tap" |
| case TypeDrag: |
| return "Drag" |
| default: |
| return fmt.Sprintf("gesture.Type(%d)", t) |
| } |
| } |
| |
| // Point is a mouse or touch location, in pixels. |
| type Point struct { |
| X, Y float32 |
| } |
| |
| // Event is a gesture event. |
| type Event struct { |
| // Type is the gesture type. |
| Type Type |
| |
| // Drag, LongPress and DoublePress are set when the gesture is recognized as |
| // a drag, etc. |
| // |
| // Note that these status fields can be lost during a gesture's events over |
| // time: LongPress can be set for the first press of a double press, but |
| // unset on the second press. |
| Drag bool |
| LongPress bool |
| DoublePress bool |
| |
| // InitialPos is the initial position of the button press or touch that |
| // started this gesture. |
| InitialPos Point |
| |
| // CurrentPos is the current position of the button or touch event. |
| CurrentPos Point |
| |
| // TODO: a "Velocity Point" field. See |
| // - frameworks/native/libs/input/VelocityTracker.cpp in AOSP, or |
| // - https://chromium.googlesource.com/chromium/src/+/master/ui/events/gesture_detection/velocity_tracker.cc in Chromium, |
| // for some velocity tracking implementations. |
| |
| // Time is the event's time. |
| Time time.Time |
| |
| // TODO: include the mouse Button and key Modifiers? |
| } |
| |
| type internalEvent struct { |
| eventFilter *EventFilter |
| |
| typ Type |
| x, y float32 |
| |
| // pressCounter is the EventFilter.pressCounter value at the time this |
| // internal event was scheduled to be delivered after a timeout. It detects |
| // whether there have been other button presses and releases during that |
| // timeout, and hence whether this internalEvent is obsolete. |
| pressCounter uint32 |
| } |
| |
| // EventFilter generates gesture events from lower level mouse and touch |
| // events. |
| type EventFilter struct { |
| EventDeque screen.EventDeque |
| |
| inProgress bool |
| drag bool |
| longPress bool |
| doublePress bool |
| |
| // initialPos is the initial position of the button press or touch that |
| // started this gesture. |
| initialPos Point |
| |
| // pressButton is the initial button that started this gesture. If |
| // button.None, no gesture is in progress. |
| pressButton mouse.Button |
| |
| // pressCounter is incremented on every button press and release. |
| pressCounter uint32 |
| } |
| |
| func (f *EventFilter) sendFirst(t Type, x, y float32, now time.Time) { |
| if t >= typeInternal { |
| f.EventDeque.SendFirst(internalEvent{ |
| eventFilter: f, |
| typ: t, |
| x: x, |
| y: y, |
| pressCounter: f.pressCounter, |
| }) |
| return |
| } |
| f.EventDeque.SendFirst(Event{ |
| Type: t, |
| Drag: f.drag, |
| LongPress: f.longPress, |
| DoublePress: f.doublePress, |
| InitialPos: f.initialPos, |
| CurrentPos: Point{ |
| X: x, |
| Y: y, |
| }, |
| // TODO: Velocity. |
| Time: now, |
| }) |
| } |
| |
| func (f *EventFilter) sendAfter(e internalEvent, sleep time.Duration) { |
| time.Sleep(sleep) |
| f.EventDeque.SendFirst(e) |
| } |
| |
| func (f *EventFilter) end(x, y float32, now time.Time) { |
| f.sendFirst(TypeEnd, x, y, now) |
| f.inProgress = false |
| f.drag = false |
| f.longPress = false |
| f.doublePress = false |
| f.initialPos = Point{} |
| f.pressButton = mouse.ButtonNone |
| } |
| |
| // Filter filters the event. It can return e, a different event, or nil to |
| // consume the event. It can also trigger side effects such as pushing new |
| // events onto its EventDeque. |
| func (f *EventFilter) Filter(e interface{}) interface{} { |
| switch e := e.(type) { |
| case internalEvent: |
| if e.eventFilter != f { |
| break |
| } |
| |
| now := time.Now() |
| |
| switch e.typ { |
| case typeDoublePressSchedule: |
| e.typ = typeDoublePressResolve |
| go f.sendAfter(e, doublePressThreshold) |
| |
| case typeDoublePressResolve: |
| if e.pressCounter == f.pressCounter { |
| // It's a single press only. |
| f.end(e.x, e.y, now) |
| } |
| |
| case typeLongPressSchedule: |
| e.typ = typeLongPressResolve |
| go f.sendAfter(e, longPressThreshold) |
| |
| case typeLongPressResolve: |
| if e.pressCounter == f.pressCounter && !f.drag { |
| f.longPress = true |
| f.sendFirst(TypeIsLongPress, e.x, e.y, now) |
| } |
| } |
| return nil |
| |
| case mouse.Event: |
| now := time.Now() |
| |
| switch e.Direction { |
| case mouse.DirNone: |
| if f.pressButton == mouse.ButtonNone { |
| break |
| } |
| startDrag := false |
| if !f.drag && |
| (abs(e.X-f.initialPos.X) > dragThreshold || abs(e.Y-f.initialPos.Y) > dragThreshold) { |
| f.drag = true |
| startDrag = true |
| } |
| if f.drag { |
| f.sendFirst(TypeDrag, e.X, e.Y, now) |
| } |
| if startDrag { |
| f.sendFirst(TypeIsDrag, e.X, e.Y, now) |
| } |
| |
| case mouse.DirPress: |
| if f.pressButton != mouse.ButtonNone { |
| break |
| } |
| |
| oldInProgress := f.inProgress |
| oldDoublePress := f.doublePress |
| |
| f.drag = false |
| f.longPress = false |
| f.doublePress = f.inProgress |
| f.initialPos = Point{e.X, e.Y} |
| f.pressButton = e.Button |
| f.pressCounter++ |
| |
| f.inProgress = true |
| |
| f.sendFirst(typeLongPressSchedule, e.X, e.Y, now) |
| if !oldDoublePress && f.doublePress { |
| f.sendFirst(TypeIsDoublePress, e.X, e.Y, now) |
| } |
| if !oldInProgress { |
| f.sendFirst(TypeStart, e.X, e.Y, now) |
| } |
| |
| case mouse.DirRelease: |
| if f.pressButton != e.Button { |
| break |
| } |
| f.pressButton = mouse.ButtonNone |
| f.pressCounter++ |
| |
| if f.drag { |
| f.end(e.X, e.Y, now) |
| break |
| } |
| f.sendFirst(typeDoublePressSchedule, e.X, e.Y, now) |
| f.sendFirst(TypeTap, e.X, e.Y, now) |
| } |
| } |
| return e |
| } |
| |
| func abs(x float32) float32 { |
| if x < 0 { |
| return -x |
| } else if x == 0 { |
| return 0 // Handle floating point negative zero. |
| } |
| return x |
| } |