blob: 8078615b120cdadbbe9992ab9b6fffcbfb25a491 [file] [log] [blame]
// 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
}