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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-06-07 21:41:09 +02:00
parent 28dd25736f
commit a35118d522
10 changed files with 172 additions and 130 deletions
+2 -4
View File
@@ -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
+11 -2
View File
@@ -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 {
+20
View File
@@ -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()
}
+24 -39
View File
@@ -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
+37 -51
View File
@@ -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})
}
+53
View File
@@ -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)
}
}
+6 -14
View File
@@ -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() {}
+2 -1
View File
@@ -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
+12 -15
View File
@@ -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() {}
+5 -4
View File
@@ -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: