From a35118d5228eab171523f30e7c1ca5182421032c Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 7 Jun 2019 21:41:09 +0200 Subject: [PATCH] ui: add package input for merged input To avoid passing a queue type for each kind of input (pointer, key), introduce package input for mapping a handler key to all input events. Future input sources can be added without changes to programs, and as an added bonus, event ordering is preserved across input sources. Signed-off-by: Elias Naur --- ui/app/window.go | 6 +- ui/gesture/gestures.go | 13 +++- ui/input/input.go | 20 ++++++ ui/{key/queue.go => input/key.go} | 63 +++++++--------- ui/{pointer/queue.go => input/pointer.go} | 88 ++++++++++------------- ui/input/queue.go | 53 ++++++++++++++ ui/key/key.go | 20 ++---- ui/layout/list.go | 3 +- ui/pointer/pointer.go | 27 ++++--- ui/text/editor.go | 9 +-- 10 files changed, 172 insertions(+), 130 deletions(-) create mode 100644 ui/input/input.go rename ui/{key/queue.go => input/key.go} (59%) rename ui/{pointer/queue.go => input/pointer.go} (74%) create mode 100644 ui/input/queue.go diff --git a/ui/app/window.go b/ui/app/window.go index 88a7e52f..d013bfcb 100644 --- a/ui/app/window.go +++ b/ui/app/window.go @@ -11,9 +11,9 @@ import ( "gioui.org/ui" "gioui.org/ui/app/internal/gpu" + "gioui.org/ui/input" "gioui.org/ui/internal/ops" "gioui.org/ui/key" - "gioui.org/ui/pointer" ) type WindowOptions struct { @@ -238,9 +238,7 @@ func (w *Window) event(e Event) { needAck := false needRedraw := false switch e := e.(type) { - case pointer.Event: - needRedraw = true - case key.Event: + case input.Event: needRedraw = true case *Command: needAck = true diff --git a/ui/gesture/gestures.go b/ui/gesture/gestures.go index edc57266..fa88feba 100644 --- a/ui/gesture/gestures.go +++ b/ui/gesture/gestures.go @@ -9,6 +9,7 @@ import ( "gioui.org/ui" "gioui.org/ui/f32" + "gioui.org/ui/input" "gioui.org/ui/pointer" ) @@ -80,9 +81,13 @@ func (c *Click) Add(ops *ui.Ops) { op.Add(ops) } -func (c *Click) Update(q pointer.Events) []ClickEvent { +func (c *Click) Update(q input.Events) []ClickEvent { var events []ClickEvent for _, e := range q.For(c) { + e, ok := e.(pointer.Event) + if !ok { + continue + } switch e.Type { case pointer.Release: if c.State == StatePressed { @@ -124,13 +129,17 @@ func (s *Scroll) Dragging() bool { return s.dragging } -func (s *Scroll) Update(cfg *ui.Config, q pointer.Events, axis Axis) int { +func (s *Scroll) Update(cfg *ui.Config, q input.Events, axis Axis) int { if s.axis != axis { s.axis = axis return 0 } total := 0 for _, e := range q.For(s) { + e, ok := e.(pointer.Event) + if !ok { + continue + } switch e.Type { case pointer.Press: if s.dragging || e.Source != pointer.Touch { diff --git a/ui/input/input.go b/ui/input/input.go new file mode 100644 index 00000000..0458c64f --- /dev/null +++ b/ui/input/input.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package input exposes a unified interface to input sources. Subpackages +// such as pointer and key provide the interfaces for specific input types. +package input + +// Events maps an event handler key to the events +// available to the handler. +type Events interface { + For(k Key) []Event +} + +// Key is the stable identifier for an event handler. For a handler h, the +// key is typically &h. +type Key interface{} + +// Event is the marker interface for input events. +type Event interface { + ImplementsInputEvent() +} diff --git a/ui/key/queue.go b/ui/input/key.go similarity index 59% rename from ui/key/queue.go rename to ui/input/key.go index f363395b..bc5e199b 100644 --- a/ui/key/queue.go +++ b/ui/input/key.go @@ -1,22 +1,22 @@ // SPDX-License-Identifier: Unlicense OR MIT -package key +package input import ( "gioui.org/ui" "gioui.org/ui/internal/ops" + "gioui.org/ui/key" ) -type Queue struct { +type keyQueue struct { focus Key - handlers map[Key]*handler + handlers map[Key]*keyHandler reader ui.OpsReader - state TextInputState + state key.TextInputState } -type handler struct { +type keyHandler struct { active bool - events []Event } type listenerPriority uint8 @@ -30,20 +30,19 @@ const ( // InputState returns the last text input state as // determined in Frame. -func (q *Queue) InputState() TextInputState { +func (q *keyQueue) InputState() key.TextInputState { return q.state } -func (q *Queue) Frame(root *ui.Ops) { +func (q *keyQueue) Frame(root *ui.Ops, events handlerEvents) { if q.handlers == nil { - q.handlers = make(map[Key]*handler) + q.handlers = make(map[Key]*keyHandler) } for _, h := range q.handlers { h.active = false - h.events = h.events[:0] } q.reader.Reset(root) - focus, pri, hide := q.resolveFocus() + focus, pri, hide := q.resolveFocus(events) for k, h := range q.handlers { if !h.active { delete(q.handlers, k) @@ -55,46 +54,33 @@ func (q *Queue) Frame(root *ui.Ops) { changed := focus != nil && focus != q.focus if focus != q.focus { if q.focus != nil { - if h, ok := q.handlers[q.focus]; ok { - h.events = append(h.events, Focus{Focus: false}) - } + events[q.focus] = append(events[q.focus], key.Focus{Focus: false}) } q.focus = focus if q.focus != nil { - // A new focus always exists in the handler map. - h := q.handlers[q.focus] - h.events = append(h.events, Focus{Focus: true}) + events[q.focus] = append(events[q.focus], key.Focus{Focus: true}) } } switch { case pri == priNewFocus: - q.state = TextInputOpen + q.state = key.TextInputOpen case hide: - q.state = TextInputClosed + q.state = key.TextInputClosed case changed: - q.state = TextInputFocus + q.state = key.TextInputFocus default: - q.state = TextInputKeep + q.state = key.TextInputKeep } } -func (q *Queue) Push(e Event) { +func (q *keyQueue) Push(e Event, events handlerEvents) { if q.focus == nil { return } - h := q.handlers[q.focus] - h.events = append(h.events, e) + events[q.focus] = append(events[q.focus], e) } -func (q *Queue) For(k Key) []Event { - h := q.handlers[k] - if h == nil { - return nil - } - return h.events -} - -func (q *Queue) resolveFocus() (Key, listenerPriority, bool) { +func (q *keyQueue) resolveFocus(events handlerEvents) (Key, listenerPriority, bool) { var k Key var pri listenerPriority var hide bool @@ -106,7 +92,7 @@ loop: } switch ops.OpType(encOp.Data[0]) { case ops.TypeKeyHandler: - var op OpHandler + var op key.OpHandler op.Decode(encOp.Data, encOp.Refs) var newPri listenerPriority switch { @@ -122,17 +108,16 @@ loop: } h, ok := q.handlers[op.Key] if !ok { - h = &handler{ - // Reset the handler on (each) first appearance. - events: []Event{Focus{Focus: false}}, - } + h = new(keyHandler) q.handlers[op.Key] = h + // Reset the handler on (each) first appearance. + events[op.Key] = []Event{key.Focus{Focus: false}} } h.active = true case ops.TypeHideInput: hide = true case ops.TypePush: - newK, newPri, h := q.resolveFocus() + newK, newPri, h := q.resolveFocus(events) hide = hide || h if newPri >= pri { k, pri = newK, newPri diff --git a/ui/pointer/queue.go b/ui/input/pointer.go similarity index 74% rename from ui/pointer/queue.go rename to ui/input/pointer.go index fa2f3c8c..f08028a3 100644 --- a/ui/pointer/queue.go +++ b/ui/input/pointer.go @@ -1,16 +1,17 @@ // SPDX-License-Identifier: Unlicense OR MIT -package pointer +package input import ( "gioui.org/ui" "gioui.org/ui/f32" "gioui.org/ui/internal/ops" + "gioui.org/ui/pointer" ) -type Queue struct { +type pointerQueue struct { hitTree []hitNode - handlers map[Key]*handler + handlers map[Key]*pointerHandler pointers []pointerInfo reader ui.OpsReader scratch []Key @@ -25,22 +26,21 @@ type hitNode struct { } type pointerInfo struct { - id ID + id pointer.ID pressed bool handlers []Key } -type handler struct { +type pointerHandler struct { area areaIntersection active bool transform ui.Transform - events []Event wantsGrab bool } type area struct { trans ui.Transform - area OpArea + area pointer.OpArea } type areaIntersection []area @@ -51,7 +51,7 @@ type areaStack struct { backing []area } -func (q *Queue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int) { +func (q *pointerQueue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int, events handlerEvents) { for { encOp, ok := r.Decode() if !ok { @@ -60,7 +60,7 @@ func (q *Queue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int) { switch ops.OpType(encOp.Data[0]) { case ops.TypePush: q.areas.push() - q.collectHandlers(r, t, layer) + q.collectHandlers(r, t, layer, events) case ops.TypePop: q.areas.pop() return @@ -68,24 +68,22 @@ func (q *Queue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int) { layer++ q.hitTree = append(q.hitTree, hitNode{level: layer}) case ops.TypeArea: - var op OpArea - op.decode(encOp.Data) + var op pointer.OpArea + op.Decode(encOp.Data) q.areas.add(t, op) case ops.TypeTransform: var op ui.OpTransform op.Decode(encOp.Data) t = t.Mul(op.Transform) case ops.TypePointerHandler: - var op OpHandler + var op pointer.OpHandler op.Decode(encOp.Data, encOp.Refs) q.hitTree = append(q.hitTree, hitNode{level: layer, key: op.Key}) h, ok := q.handlers[op.Key] if !ok { - h = &handler{ - // Reset the handler on (each) first appearance. - events: []Event{Event{Type: Cancel}}, - } + h = new(pointerHandler) q.handlers[op.Key] = h + events[op.Key] = []Event{pointer.Event{Type: pointer.Cancel}} } h.active = true h.area = q.areas.intersection() @@ -95,7 +93,7 @@ func (q *Queue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int) { } } -func (q *Queue) opHit(handlers *[]Key, pos f32.Point) { +func (q *pointerQueue) opHit(handlers *[]Key, pos f32.Point) { level := 1 << 30 opaque := false for i := len(q.hitTree) - 1; i >= 0; i-- { @@ -114,31 +112,30 @@ func (q *Queue) opHit(handlers *[]Key, pos f32.Point) { continue } res := h.area.hit(pos) - opaque = opaque || res == hitOpaque - if res != hitNone { + opaque = opaque || res == pointer.HitOpaque + if res != pointer.HitNone { *handlers = append(*handlers, n.key) } } } } -func (q *Queue) init() { +func (q *pointerQueue) init() { if q.handlers == nil { - q.handlers = make(map[Key]*handler) + q.handlers = make(map[Key]*pointerHandler) } } -func (q *Queue) Frame(root *ui.Ops) { +func (q *pointerQueue) Frame(root *ui.Ops, events handlerEvents) { q.init() for _, h := range q.handlers { // Reset handler. h.active = false - h.events = h.events[:0] } q.hitTree = q.hitTree[:0] q.areas.reset() q.reader.Reset(root) - q.collectHandlers(&q.reader, ui.Transform{}, 0) + q.collectHandlers(&q.reader, ui.Transform{}, 0, events) for k, h := range q.handlers { if !h.active { q.dropHandler(k) @@ -146,18 +143,7 @@ func (q *Queue) Frame(root *ui.Ops) { } } -func (q *Queue) For(k Key) []Event { - if k == nil { - panic("nil handler") - } - h := q.handlers[k] - if h == nil { - return nil - } - return h.events -} - -func (q *Queue) dropHandler(k Key) { +func (q *pointerQueue) dropHandler(k Key) { delete(q.handlers, k) for i := range q.pointers { p := &q.pointers[i] @@ -169,9 +155,9 @@ func (q *Queue) dropHandler(k Key) { } } -func (q *Queue) Push(e Event) { +func (q *pointerQueue) Push(e pointer.Event, events handlerEvents) { q.init() - if e.Type == Cancel { + if e.Type == pointer.Cancel { q.pointers = q.pointers[:0] for k := range q.handlers { q.dropHandler(k) @@ -190,7 +176,7 @@ func (q *Queue) Push(e Event) { pidx = len(q.pointers) - 1 } p := &q.pointers[pidx] - if !p.pressed && (e.Type == Move || e.Type == Press) { + if !p.pressed && (e.Type == pointer.Move || e.Type == pointer.Press) { p.handlers, q.scratch = q.scratch[:0], p.handlers q.opHit(&p.handlers, e.Position) // Drop handlers no longer hit. @@ -203,7 +189,7 @@ func (q *Queue) Push(e Event) { } q.dropHandler(h) } - if e.Type == Press { + if e.Type == pointer.Press { p.pressed = true } } @@ -223,7 +209,7 @@ func (q *Queue) Push(e Event) { q.dropHandler(k) } } - if e.Type == Release { + if e.Type == pointer.Release { q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) } for i, k := range p.handlers { @@ -231,14 +217,14 @@ func (q *Queue) Push(e Event) { e := e switch { case p.pressed && len(p.handlers) == 1: - e.Priority = Grabbed + e.Priority = pointer.Grabbed case i == 0: - e.Priority = Foremost + e.Priority = pointer.Foremost } - e.Hit = h.area.hit(e.Position) != hitNone + e.Hit = h.area.hit(e.Position) != pointer.HitNone e.Position = h.transform.InvTransform(e.Position) - h.events = append(h.events, e) - if e.Type == Release { + events[k] = append(events[k], e) + if e.Type == pointer.Release { // Release grab when the number of grabs reaches zero. grabs := 0 for _, p := range q.pointers { @@ -253,19 +239,19 @@ func (q *Queue) Push(e Event) { } } -func (a areaIntersection) hit(p f32.Point) hitResult { - res := hitNone +func (a areaIntersection) hit(p f32.Point) pointer.HitResult { + res := pointer.HitNone for _, area := range a { tp := area.trans.InvTransform(p) - res = area.area.hit(tp) - if res == hitNone { + res = area.area.Hit(tp) + if res == pointer.HitNone { break } } return res } -func (s *areaStack) add(t ui.Transform, a OpArea) { +func (s *areaStack) add(t ui.Transform, a pointer.OpArea) { s.areas = append(s.areas, area{t, a}) } diff --git a/ui/input/queue.go b/ui/input/queue.go new file mode 100644 index 00000000..eacd275f --- /dev/null +++ b/ui/input/queue.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package input + +import ( + "gioui.org/ui" + "gioui.org/ui/key" + "gioui.org/ui/pointer" +) + +// Queue is an Events implementation that merges events from +// all available input sources. +type Queue struct { + pqueue pointerQueue + kqueue keyQueue + + handlers handlerEvents +} + +type handlerEvents map[Key][]Event + +func (q *Queue) For(k Key) []Event { + return q.handlers[k] +} + +func (q *Queue) Frame(ops *ui.Ops) { + q.init() + for k := range q.handlers { + delete(q.handlers, k) + } + q.pqueue.Frame(ops, q.handlers) + q.kqueue.Frame(ops, q.handlers) +} + +func (q *Queue) Add(e Event) { + q.init() + switch e := e.(type) { + case pointer.Event: + q.pqueue.Push(e, q.handlers) + case key.Edit, key.Chord, key.Focus: + q.kqueue.Push(e, q.handlers) + } +} + +func (q *Queue) InputState() key.TextInputState { + return q.kqueue.InputState() +} + +func (q *Queue) init() { + if q.handlers == nil { + q.handlers = make(handlerEvents) + } +} diff --git a/ui/key/key.go b/ui/key/key.go index d8eca72a..3464c3ff 100644 --- a/ui/key/key.go +++ b/ui/key/key.go @@ -16,14 +16,6 @@ type OpHideInput struct{} type Key interface{} -type Events interface { - For(k Key) []Event -} - -type Event interface { - isKeyEvent() -} - type Focus struct { Focus bool } @@ -93,9 +85,9 @@ func (h OpHideInput) Add(o *ui.Ops) { o.Write(data) } -func (Edit) ImplementsEvent() {} -func (Chord) ImplementsEvent() {} -func (Focus) ImplementsEvent() {} -func (Edit) isKeyEvent() {} -func (Chord) isKeyEvent() {} -func (Focus) isKeyEvent() {} +func (Edit) ImplementsEvent() {} +func (Chord) ImplementsEvent() {} +func (Focus) ImplementsEvent() {} +func (Edit) ImplementsInputEvent() {} +func (Chord) ImplementsInputEvent() {} +func (Focus) ImplementsInputEvent() {} diff --git a/ui/layout/list.go b/ui/layout/list.go index 7e9987e6..de5aad6b 100644 --- a/ui/layout/list.go +++ b/ui/layout/list.go @@ -8,6 +8,7 @@ import ( "gioui.org/ui" "gioui.org/ui/draw" "gioui.org/ui/gesture" + "gioui.org/ui/input" "gioui.org/ui/pointer" ) @@ -64,7 +65,7 @@ func (l *List) Dragging() bool { return l.scroll.Dragging() } -func (l *List) Update(c *ui.Config, q pointer.Events) { +func (l *List) Update(c *ui.Config, q input.Events) { l.Distance = 0 d := l.scroll.Update(c, q, gesture.Axis(l.Axis)) l.scrollDir = d diff --git a/ui/pointer/pointer.go b/ui/pointer/pointer.go index f0988f74..4960e4b8 100644 --- a/ui/pointer/pointer.go +++ b/ui/pointer/pointer.go @@ -37,16 +37,12 @@ type OpHandler struct { type Key interface{} -type Events interface { - For(k Key) []Event -} - -type hitResult uint8 +type HitResult uint8 const ( - hitNone hitResult = iota - hitTransparent - hitOpaque + HitNone HitResult = iota + HitTransparent + HitOpaque ) type ID uint16 @@ -103,7 +99,7 @@ func (op OpArea) Add(o *ui.Ops) { o.Write(data) } -func (op *OpArea) decode(d []byte) { +func (op *OpArea) Decode(d []byte) { if ops.OpType(d[0]) != ops.TypeArea { panic("invalid op") } @@ -118,10 +114,10 @@ func (op *OpArea) decode(d []byte) { } } -func (op *OpArea) hit(pos f32.Point) hitResult { - res := hitOpaque +func (op *OpArea) Hit(pos f32.Point) HitResult { + res := HitOpaque if op.Transparent { - res = hitTransparent + res = HitTransparent } switch op.kind { case areaRect: @@ -129,7 +125,7 @@ func (op *OpArea) hit(pos f32.Point) hitResult { 0 <= pos.Y && pos.Y < float32(op.size.Y) { return res } else { - return hitNone + return HitNone } case areaEllipse: rx := float32(op.size.X) / 2 @@ -141,7 +137,7 @@ func (op *OpArea) hit(pos f32.Point) hitResult { if xh*xh*ry2+yk*yk*rx2 <= rx2*ry2 { return res } else { - return hitNone + return HitNone } default: panic("invalid area kind") @@ -206,4 +202,5 @@ func (s Source) String() string { } } -func (Event) ImplementsEvent() {} +func (Event) ImplementsEvent() {} +func (Event) ImplementsInputEvent() {} diff --git a/ui/text/editor.go b/ui/text/editor.go index a2ddbc7d..0087b14f 100644 --- a/ui/text/editor.go +++ b/ui/text/editor.go @@ -11,6 +11,7 @@ import ( "gioui.org/ui" "gioui.org/ui/draw" "gioui.org/ui/gesture" + "gioui.org/ui/input" "gioui.org/ui/key" "gioui.org/ui/layout" "gioui.org/ui/pointer" @@ -53,7 +54,7 @@ const ( maxBlinkDuration = 10 * time.Second ) -func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) { +func (e *Editor) Update(c *ui.Config, q input.Events) { if e.cfg == nil || c.PxPerDp != e.cfg.PxPerDp || c.PxPerSp != e.cfg.PxPerSp { e.invalidate() } @@ -68,7 +69,7 @@ func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) { axis = gesture.Vertical smin, smax = sbounds.Min.Y, sbounds.Max.Y } - sdist := e.scroller.Update(c, pq, axis) + sdist := e.scroller.Update(c, q, axis) var soff int if e.SingleLine { e.scrollOff.X += sdist @@ -78,7 +79,7 @@ func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) { soff = e.scrollOff.Y } scrollTo := false - for _, evt := range e.clicker.Update(pq) { + for _, evt := range e.clicker.Update(q) { switch { case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse, evt.Type == gesture.TypeClick && evt.Source == pointer.Touch: @@ -92,7 +93,7 @@ func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) { } } stop := (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) - for _, ke := range kq.For(e) { + for _, ke := range q.For(e) { e.blinkStart = c.Now switch ke := ke.(type) { case key.Focus: