io/input: implement lazy event routing

This change defers event routing from the time the event is queued until
the time Events is called. This allows a future change to execute
commands immediately and to react to event order changes during a frame.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-11-15 19:14:14 -06:00
parent 651094d692
commit 9dfada745c
8 changed files with 390 additions and 230 deletions
+211 -74
View File
@@ -5,6 +5,7 @@ package input
import (
"encoding/binary"
"image"
"io"
"strings"
"time"
@@ -35,7 +36,10 @@ type Router struct {
}
cqueue clipboardQueue
events []taggedEvent
// states is the list of pending state changes resulting from
// incoming events. The first element is the current state,
// if any.
changes []stateChange
reader ops.Reader
@@ -45,6 +49,9 @@ type Router struct {
// Changes queued for next call to Frame.
commands []Command
// transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser
}
// Source implements the interface between a Router and user interface widgets.
@@ -94,6 +101,21 @@ const (
// By convention, the zero value denotes the non-existent ID.
type SemanticID uint
// stateChange represents the new state and outgoing events
// resulting from an incoming event.
type stateChange struct {
state inputState
events []taggedEvent
}
// inputState represent a immutable snapshot of the state required
// to route events.
type inputState struct {
clipboardState
keyState
pointerState
}
// taggedEvent represents an event and its target handler.
type taggedEvent struct {
event event.Event
@@ -130,7 +152,8 @@ func (s Source) Events(k event.Tag, filters ...event.Filter) []event.Event {
}
func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
var evts []event.Event
var events []event.Event
// Record handler filters and add reset events.
for _, f := range filters {
switch f := f.(type) {
case key.Filter:
@@ -138,12 +161,12 @@ func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
case key.FocusFilter:
q.key.queue.focusable(k)
if reset, ok := q.key.queue.ResetEvent(k); ok {
evts = append(evts, reset)
events = append(events, reset)
}
case pointer.Filter:
q.pointer.queue.filterTag(k, f)
if reset, ok := q.pointer.queue.ResetEvent(k); ok {
evts = append(evts, reset)
events = append(events, reset)
}
case transfer.SourceFilter:
q.pointer.queue.sourceFilter(k, f)
@@ -151,39 +174,71 @@ func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
q.pointer.queue.targetFilter(k, f)
}
}
i := 0
for i < len(q.events) {
e := q.events[i]
if e.tag == k {
q.events = append(q.events[:i], q.events[i+1:]...)
if filtersMatches(filters, e.event) {
evts = append(evts, e.event)
// Accumulate events from state changes until there are no more
// matching events.
matchedIdx := 0
for i := range q.changes {
change := &q.changes[i]
j := 0
for j < len(change.events) {
evt := change.events[j]
if evt.tag != k || !filtersMatches(filters, evt.event) {
j++
continue
}
} else {
i++
events = append(events, evt.event)
change.events = append(change.events[:j], change.events[j+1:]...)
matchedIdx = i
}
}
return evts
// Fast forward state to last matched.
q.collapseState(matchedIdx)
return events
}
// collapseState in the interval [1;idx] into q.changes[0].
func (q *Router) collapseState(idx int) {
if idx == 0 {
return
}
first := &q.changes[0]
first.state = q.changes[idx].state
for i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) {
q.events = q.events[:0]
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
q.wakeup = false
// Collapse state and clear events.
if n := len(q.changes); n > 1 {
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
var ops *ops.Ops
if frame != nil {
ops = &frame.Internal
}
q.reader.Reset(ops)
q.collect()
evts := q.executeCommands(nil)
evts = q.pointer.queue.Frame(evts)
q.key.queue.Frame()
q.addEvents(evts)
q.executeCommands()
q.changePointerState(q.pointer.queue.Frame(q.lastState().pointerState))
kstate := q.key.queue.Frame(q.lastState().keyState)
q.changeKeyState(kstate, nil)
// Collapse state and events.
q.collapseState(len(q.changes) - 1)
if len(evts) > 0 {
if len(q.changes) > 0 && len(q.changes[0].events) > 0 {
q.wakeup = true
q.wakeupTime = time.Time{}
}
@@ -191,69 +246,147 @@ func (q *Router) Frame(frame *op.Ops) {
// Queue events and report whether at least one event matched a handler.
func (q *Router) Queue(events ...event.Event) bool {
var evts []taggedEvent
matched := false
for _, e := range events {
switch e := e.(type) {
case pointer.Event:
evts = q.pointer.queue.Push(evts, e)
case key.Event:
evts = q.queueKeyEvent(evts, e)
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := q.key.queue.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
if e.Start > r.Start {
e.Start = r.Start
}
if e.End < r.End {
e.End = r.End
}
}
if f := q.key.queue.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
if f := q.key.queue.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
case transfer.DataEvent:
evts = q.cqueue.Push(evts, e)
}
hadEvents := q.processEvent(e)
matched = matched || hadEvents
}
return matched
}
func (q *Router) processEvent(e event.Event) bool {
state := q.lastState()
switch e := e.(type) {
case pointer.Event:
return q.changePointerState(q.pointer.queue.Push(state.pointerState, e))
case key.Event:
return q.addEvents(q.queueKeyEvent(state.keyState, e))
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
if e.Start > r.Start {
e.Start = r.Start
}
if e.End < r.End {
e.End = r.End
}
}
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
return q.addEvents(evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
return q.addEvents(evts)
case transfer.DataEvent:
return q.changeClipboardState(q.cqueue.Push(state.clipboardState, e))
default:
panic("unknown event type")
}
q.addEvents(evts)
return len(evts) > 0
}
func (q *Router) queue(f Command) {
q.commands = append(q.commands, f)
}
func (q *Router) executeCommands(evts []taggedEvent) []taggedEvent {
func (q *Router) state() inputState {
if len(q.changes) > 0 {
return q.changes[0].state
}
return inputState{}
}
func (q *Router) lastState() inputState {
if n := len(q.changes); n > 0 {
return q.changes[n-1].state
}
return inputState{}
}
func (q *Router) changeClipboardState(cstate clipboardState, evts []taggedEvent) bool {
state := q.lastState()
state.clipboardState = cstate
return q.changeState(state, evts)
}
func (q *Router) changeKeyState(kstate keyState, evts []taggedEvent) bool {
state := q.lastState()
state.keyState = kstate
return q.changeState(state, evts)
}
func (q *Router) changePointerState(pstate pointerState, evts []taggedEvent) bool {
state := q.lastState()
state.pointerState = pstate
return q.changeState(state, evts)
}
func (q *Router) executeCommands() {
for _, req := range q.commands {
state := q.lastState()
switch req := req.(type) {
case key.SelectionCmd:
q.key.queue.setSelection(req)
kstate := q.key.queue.setSelection(state.keyState, req)
q.changeKeyState(kstate, nil)
case key.FocusCmd:
evts = q.key.queue.Focus(evts, req.Tag)
q.changeKeyState(q.key.queue.Focus(state.keyState, req.Tag))
case key.SoftKeyboardCmd:
q.key.queue.softKeyboard(req.Show)
kstate := state.keyState.softKeyboard(req.Show)
q.changeKeyState(kstate, nil)
case key.SnippetCmd:
q.key.queue.setSnippet(req)
kstate := q.key.queue.setSnippet(state.keyState, req)
q.changeKeyState(kstate, nil)
case transfer.OfferCmd:
evts = q.pointer.queue.offerData(evts, req)
q.changePointerState(q.pointer.queue.offerData(state.pointerState, req))
case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
q.cqueue.ProcessReadClipboard(req.Tag)
cstate := q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
q.changeClipboardState(cstate, nil)
case pointer.GrabCmd:
evts = q.pointer.queue.grab(evts, req)
q.changePointerState(q.pointer.queue.grab(state.pointerState, req))
}
}
q.commands = nil
return evts
}
func (q *Router) addEvents(evts []taggedEvent) {
q.events = append(q.events, evts...)
func (q *Router) addEvents(evts []taggedEvent) bool {
return q.changeState(q.lastState(), evts)
}
func (q *Router) changeState(state inputState, evts []taggedEvent) bool {
// Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts {
e := &evts[i]
if de, ok := e.event.(transfer.DataEvent); ok {
transferIdx := len(q.transfers)
data := de.Open()
q.transfers = append(q.transfers, data)
de.Open = func() io.ReadCloser {
q.transfers[transferIdx] = nil
return data
}
e.event = de
}
}
n := len(q.changes)
// We must add a new state change if
//
// - there is no first state change, or
// - the state change is not atomic from the perspective of the handlers.
if len(q.changes) == 0 || (len(evts) > 0 && len(q.changes[n-1].events) > 0) {
q.changes = append(q.changes, stateChange{state: state, events: evts})
} else {
// Otherwise, merge with previous change.
prev := &q.changes[n-1]
prev.state = state
prev.events = append(prev.events, evts...)
}
return len(evts) > 0
}
func rangeOverlaps(r1, r2 key.Range) bool {
@@ -270,9 +403,10 @@ func rangeNorm(r key.Range) key.Range {
return r
}
func (q *Router) queueKeyEvent(evts []taggedEvent, e key.Event) []taggedEvent {
func (q *Router) queueKeyEvent(state keyState, e key.Event) []taggedEvent {
kq := &q.key.queue
f := q.key.queue.focus
f := state.focus
var evts []taggedEvent
if f != nil && kq.Accepts(f, e) {
evts = append(evts, taggedEvent{tag: f, event: e})
return evts
@@ -305,15 +439,15 @@ func (q *Router) queueKeyEvent(evts []taggedEvent, e key.Event) []taggedEvent {
}
func (q *Router) MoveFocus(dir key.FocusDirection) bool {
evts := q.key.queue.MoveFocus(nil, dir)
q.addEvents(evts)
return len(evts) > 0
ks, evts := q.key.queue.MoveFocus(q.lastState().keyState, dir)
return q.changeKeyState(ks, evts)
}
// RevealFocus scrolls the current focus (if any) into viewport
// if there are scrollable parent handlers.
func (q *Router) RevealFocus(viewport image.Rectangle) {
focus := q.key.queue.focus
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
@@ -339,7 +473,8 @@ func (q *Router) RevealFocus(viewport image.Rectangle) {
// ScrollFocus scrolls the focused widget, if any, by dist.
func (q *Router) ScrollFocus(dist image.Point) {
focus := q.key.queue.focus
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
@@ -378,7 +513,7 @@ func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
}
func (q *Router) ClickFocus() {
focus := q.key.queue.focus
focus := q.lastState().focus
if focus == nil {
return
}
@@ -398,12 +533,14 @@ func (q *Router) ClickFocus() {
// TextInputState returns the input state from the most recent
// call to Frame.
func (q *Router) TextInputState() TextInputState {
return q.key.queue.InputState()
kstate, s := q.state().InputState()
q.changeKeyState(kstate, nil)
return s
}
// TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.key.queue.InputHint()
return q.key.queue.InputHint(q.state().keyState)
}
// WriteClipboard returns the most recent content to be copied
@@ -412,15 +549,15 @@ func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
return q.cqueue.WriteClipboard()
}
// ReadClipboard reports if any new handler is waiting
// ClipboardRequested reports if any new handler is waiting
// to read the clipboard.
func (q *Router) ReadClipboard() bool {
return q.cqueue.ReadClipboard()
func (q *Router) ClipboardRequested() bool {
return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
}
// Cursor returns the last cursor set.
func (q *Router) Cursor() pointer.Cursor {
return q.pointer.queue.cursor
return q.state().cursor
}
// SemanticAt returns the first semantic description under pos, if any.
@@ -439,7 +576,7 @@ func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
// EditorState returns the editor state for the focused handler, or the
// zero value if there is none.
func (q *Router) EditorState() EditorState {
return q.key.queue.editorState()
return q.key.queue.editorState(q.state().keyState)
}
func (q *Router) collect() {