diff --git a/shiny/example/gallery/main.go b/shiny/example/gallery/main.go index 4d6996b03..1a43bfe82 100644 --- a/shiny/example/gallery/main.go +++ b/shiny/example/gallery/main.go @@ -23,7 +23,6 @@ import ( "golang.org/x/exp/shiny/widget" "golang.org/x/exp/shiny/widget/node" "golang.org/x/exp/shiny/widget/theme" - "golang.org/x/mobile/event/mouse" ) var red = image.NewUniform(color.RGBA{0xff, 0x00, 0x00, 0xff}) @@ -39,9 +38,9 @@ func newCustom() *custom { return w } -func (w *custom) OnMouseEvent(e mouse.Event, origin image.Point) node.EventHandled { +func (w *custom) OnInputEvent(e interface{}, origin image.Point) node.EventHandled { // TODO: do something more interesting. - fmt.Println(e) + fmt.Printf("%T %v\n", e, e) return node.Handled } diff --git a/shiny/gesture/gesture.go b/shiny/gesture/gesture.go new file mode 100644 index 000000000..8078615b1 --- /dev/null +++ b/shiny/gesture/gesture.go @@ -0,0 +1,326 @@ +// 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 +} diff --git a/shiny/widget/node/node.go b/shiny/widget/node/node.go index 5fa5b051b..9b0828b72 100644 --- a/shiny/widget/node/node.go +++ b/shiny/widget/node/node.go @@ -44,13 +44,14 @@ package node // import "golang.org/x/exp/shiny/widget/node" import ( "image" + "golang.org/x/exp/shiny/gesture" "golang.org/x/exp/shiny/widget/theme" "golang.org/x/mobile/event/mouse" ) -// EventHandled is whether or not an input event, such as a key or mouse event, -// was handled by a widget. If it was not handled, the event is propagated -// along the widget tree. +// EventHandled is whether or not an input event (a key, mouse, touch or +// gesture event) was handled by a widget. If it was not handled, the event is +// propagated along the widget tree. type EventHandled bool const ( @@ -97,14 +98,14 @@ type Node interface { // smaller dst images? Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) - // OnMouseEvent handles a mouse event. + // OnInputEvent handles a key, mouse, touch or gesture event. // - // origin is the parent widget's origin with respect to the mouse event - // origin; this node's Embed.Rect.Add(origin) will be its position and size - // in mouse event coordinate space. - OnMouseEvent(e mouse.Event, origin image.Point) EventHandled + // origin is the parent widget's origin with respect to the event origin; + // this node's Embed.Rect.Add(origin) will be its position and size in + // event coordinate space. + OnInputEvent(e interface{}, origin image.Point) EventHandled - // TODO: OnXxxEvent methods. + // TODO: other OnXxxEvent methods? } // LeafEmbed is designed to be embedded in struct types for nodes with no @@ -123,7 +124,7 @@ func (m *LeafEmbed) Layout(t *theme.Theme) {} func (m *LeafEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {} -func (m *LeafEmbed) OnMouseEvent(e mouse.Event, origin image.Point) EventHandled { return NotHandled } +func (m *LeafEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled { return NotHandled } // ShellEmbed is designed to be embedded in struct types for nodes with at most // one child. @@ -160,9 +161,9 @@ func (m *ShellEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) } } -func (m *ShellEmbed) OnMouseEvent(e mouse.Event, origin image.Point) EventHandled { +func (m *ShellEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled { if c := m.FirstChild; c != nil { - return c.Wrapper.OnMouseEvent(e, origin.Add(m.Rect.Min)) + return c.Wrapper.OnInputEvent(e, origin.Add(m.Rect.Min)) } return NotHandled } @@ -203,16 +204,25 @@ func (m *ContainerEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Poi } } -func (m *ContainerEmbed) OnMouseEvent(e mouse.Event, origin image.Point) EventHandled { +func (m *ContainerEmbed) OnInputEvent(e interface{}, origin image.Point) EventHandled { origin = origin.Add(m.Rect.Min) - p := image.Point{ - X: int(e.X) - origin.X, - Y: int(e.Y) - origin.Y, + var p image.Point + switch e := e.(type) { + case gesture.Event: + p = image.Point{ + X: int(e.CurrentPos.X) - origin.X, + Y: int(e.CurrentPos.Y) - origin.Y, + } + case mouse.Event: + p = image.Point{ + X: int(e.X) - origin.X, + Y: int(e.Y) - origin.Y, + } } // Iterate backwards. Later children have priority over earlier children, // as later ones are usually drawn over earlier ones. for c := m.LastChild; c != nil; c = c.PrevSibling { - if p.In(c.Rect) && c.Wrapper.OnMouseEvent(e, origin) == Handled { + if p.In(c.Rect) && c.Wrapper.OnInputEvent(e, origin) == Handled { return Handled } } diff --git a/shiny/widget/widget.go b/shiny/widget/widget.go index ab2779bb4..4de82a58a 100644 --- a/shiny/widget/widget.go +++ b/shiny/widget/widget.go @@ -10,6 +10,7 @@ package widget // import "golang.org/x/exp/shiny/widget" import ( "image" + "golang.org/x/exp/shiny/gesture" "golang.org/x/exp/shiny/screen" "golang.org/x/exp/shiny/unit" "golang.org/x/exp/shiny/widget/node" @@ -50,7 +51,9 @@ type RunWindowOptions struct { NewWindowOptions screen.NewWindowOptions Theme theme.Theme - // TODO: some mechanism to process, filter and inject events. + // TODO: some mechanism to process, filter and inject events. Perhaps a + // screen.EventFilter interface, and note that the zero value in this + // RunWindowOptions implicitly includes the gesture.EventFilter? } // TODO: how does RunWindow's caller inject or process events (whether general @@ -86,15 +89,22 @@ func RunWindow(s screen.Screen, root node.Node, opts *RunWindowOptions) error { return err } defer w.Release() + gef := gesture.EventFilter{EventDeque: w} for { - switch e := w.NextEvent().(type) { + e := w.NextEvent() + + if e = gef.Filter(e); e == nil { + continue + } + + switch e := e.(type) { case lifecycle.Event: if e.To == lifecycle.StageDead { return nil } - case mouse.Event: - root.OnMouseEvent(e, image.Point{}) + case gesture.Event, mouse.Event: + root.OnInputEvent(e, image.Point{}) case paint.Event: if buf != nil {