mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 09:55:40 +00:00
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:
+211
-74
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user