~eliasnaur/gio

a35118d5228eab171523f30e7c1ca5182421032c — Elias Naur 2 years ago 28dd257
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>
10 files changed, 172 insertions(+), 130 deletions(-)

M ui/app/window.go
M ui/gesture/gestures.go
A ui/input/input.go
R ui/{key/queue.go => input/key.go}
R ui/{pointer/queue.go => input/pointer.go}
A ui/input/queue.go
M ui/key/key.go
M ui/layout/list.go
M ui/pointer/pointer.go
M ui/text/editor.go
M ui/app/window.go => ui/app/window.go +2 -4
@@ 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

M ui/gesture/gestures.go => ui/gesture/gestures.go +11 -2
@@ 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 {

A ui/input/input.go => ui/input/input.go +20 -0
@@ 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()
}

R ui/key/queue.go => ui/input/key.go +24 -39
@@ 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)
}

func (q *Queue) For(k Key) []Event {
	h := q.handlers[k]
	if h == nil {
		return nil
	}
	return h.events
	events[q.focus] = append(events[q.focus], e)
}

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

R ui/pointer/queue.go => ui/input/pointer.go +37 -51
@@ 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})
}


A ui/input/queue.go => ui/input/queue.go +53 -0
@@ 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)
	}
}

M ui/key/key.go => ui/key/key.go +6 -14
@@ 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() {}

M ui/layout/list.go => ui/layout/list.go +2 -1
@@ 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

M ui/pointer/pointer.go => ui/pointer/pointer.go +12 -15
@@ 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() {}

M ui/text/editor.go => ui/text/editor.go +5 -4
@@ 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: