io/input: [API] execute commands immediately

Change the semantics of commands to execute immediately. In cases where
execution of a command introduces a inconsistency, freeze event routing
and defer the command as well as queued events to the next frame.

Rename Source.Queue to Source.Execute to better fit the new command
semantics.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-11-20 16:25:05 -06:00
parent 67b58a6006
commit fc208248b7
11 changed files with 190 additions and 142 deletions
+7 -17
View File
@@ -17,8 +17,8 @@ func TestClipboardDuplicateEvent(t *testing.T) {
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
// Both must receive the event once
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[1]})
router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Source().Execute(clipboard.ReadCmd{Tag: &handler[1]})
router.Frame(ops)
event := transfer.DataEvent{
@@ -41,7 +41,7 @@ func TestClipboardDuplicateEvent(t *testing.T) {
assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset()
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops)
// No ClipboardEvent sent
@@ -56,7 +56,7 @@ func TestQueueProcessReadClipboard(t *testing.T) {
ops.Reset()
// Request read
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops)
assertClipboardReadCmd(t, router, 1)
@@ -94,28 +94,18 @@ func TestQueueProcessReadClipboard(t *testing.T) {
}
func TestQueueProcessWriteClipboard(t *testing.T) {
ops, router := new(op.Ops), new(Router)
ops.Reset()
router := new(Router)
const mime = "application/text"
router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))})
router.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))})
router.Frame(ops)
assertClipboardWriteCmd(t, router, mime, "Write 1")
ops.Reset()
// No WriteCmd
router.Frame(ops)
assertClipboardWriteCmd(t, router, "", "")
ops.Reset()
router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))})
router.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))})
router.Frame(ops)
assertClipboardReadCmd(t, router, 0)
assertClipboardWriteCmd(t, router, mime, "Write 2")
ops.Reset()
}
func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
+20 -20
View File
@@ -37,9 +37,8 @@ func TestKeyMultiples(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{Tag: &handlers[2]})
event.InputOp(ops, &handlers[1])
// The last one must be focused:
@@ -51,6 +50,7 @@ func TestKeyMultiples(t *testing.T) {
r.Frame(ops)
r.Source().Execute(key.FocusCmd{Tag: &handlers[2]})
assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), true)
assertFocus(t, r, &handlers[2])
@@ -63,12 +63,12 @@ func TestKeyStacked(t *testing.T) {
r := new(Router)
event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{})
r.Source().Queue(key.SoftKeyboardCmd{Show: false})
r.Source().Execute(key.FocusCmd{})
r.Source().Execute(key.SoftKeyboardCmd{Show: false})
event.InputOp(ops, &handlers[1])
r.Source().Queue(key.FocusCmd{Tag: &handlers[1]})
r.Source().Execute(key.FocusCmd{Tag: &handlers[1]})
event.InputOp(ops, &handlers[2])
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
event.InputOp(ops, &handlers[3])
for i := range handlers {
@@ -88,7 +88,7 @@ func TestKeySoftKeyboardNoFocus(t *testing.T) {
// It's possible to open the keyboard
// without any active focus:
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
r.Frame(ops)
@@ -103,8 +103,8 @@ func TestKeyRemoveFocus(t *testing.T) {
// New InputOp with Focus and Keyboard:
event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{Tag: &handlers[0]})
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
// New InputOp without any focus:
event.InputOp(ops, &handlers[1])
@@ -136,7 +136,7 @@ func TestKeyRemoveFocus(t *testing.T) {
event.InputOp(ops, &handlers[1])
// Remove focus by focusing on a tag that don't exist.
r.Source().Queue(key.FocusCmd{Tag: new(int)})
r.Source().Execute(key.FocusCmd{Tag: new(int)})
r.Frame(ops)
@@ -149,26 +149,26 @@ func TestKeyRemoveFocus(t *testing.T) {
event.InputOp(ops, &handlers[0])
event.InputOp(ops, &handlers[1])
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0], key.FocusFilter{}))
assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{}))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
r.Frame(ops)
ops.Reset()
// Set focus to InputOp which already
// exists in the previous frame:
r.Source().Queue(key.FocusCmd{Tag: &handlers[0]})
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
event.InputOp(ops, &handlers[0])
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
assertFocus(t, r, &handlers[0])
ops.Reset()
// Remove focus.
event.InputOp(ops, &handlers[1])
r.Source().Queue(key.FocusCmd{})
r.Frame(ops)
r.Source().Execute(key.FocusCmd{})
assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{}))
assertFocus(t, r, nil)
@@ -181,9 +181,9 @@ func TestKeyFocusedInvisible(t *testing.T) {
r := new(Router)
// Set new InputOp with focus:
r.Source().Queue(key.FocusCmd{Tag: &handlers[0]})
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
event.InputOp(ops, &handlers[0])
r.Source().Queue(key.SoftKeyboardCmd{Show: true})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
// Set new InputOp without focus:
event.InputOp(ops, &handlers[1])
@@ -403,7 +403,7 @@ func TestKeyRouting(t *testing.T) {
r2.Events(&handlers[3], key.FocusFilter{})
r2.Events(&handlers[4], fa...)
r2.Source().Queue(key.FocusCmd{Tag: &handlers[3]})
r2.Source().Execute(key.FocusCmd{Tag: &handlers[3]})
r2.Frame(ops)
r2.Queue(A, B)
+4 -4
View File
@@ -99,10 +99,10 @@ func TestPointerGrab(t *testing.T) {
Position: f32.Pt(50, 50),
},
)
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Press)
r.Source().Execute(pointer.GrabCmd{Tag: handler1})
r.Frame(&ops)
r.Queue(
pointer.Event{
@@ -136,9 +136,9 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
Position: f32.Pt(50, 50),
},
)
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
r.Source().Execute(pointer.GrabCmd{Tag: handler1})
r.Frame(&ops)
r.Queue(
pointer.Event{
@@ -941,7 +941,7 @@ func TestTransfer(t *testing.T) {
// Offer valid type and data.
ofr := &offer{data: "hello"}
r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Source().Execute(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
evs := r.Events(tgt, transfer.TargetFilter{Type: "file"})
@@ -989,9 +989,9 @@ func TestTransfer(t *testing.T) {
},
)
ofr := &offer{data: "hello"}
r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Events(src, transfer.SourceFilter{Type: "file"})
r.Events(tgt, transfer.TargetFilter{Type: "file"})
r.Source().Execute(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
// Ignore DataEvent and verify that the next frame closes it as unused.
+142 -83
View File
@@ -38,8 +38,8 @@ type Router struct {
cqueue clipboardQueue
// states is the list of pending state changes resulting from
// incoming events. The first element is the current state,
// if any.
// incoming events. The first element, if present, contains the state
// and events for the current frame.
changes []stateChange
reader ops.Reader
@@ -54,6 +54,10 @@ type Router struct {
// transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser
// deferring is set if command execution and event delivery is deferred
// to the next frame.
deferring bool
// scratchFilter is for garbage-free construction of ephemeral filters.
scratchFilter filter
}
@@ -109,7 +113,9 @@ type SemanticID uint
type handler struct {
// active tracks whether the handler was active in the current
// frame. Router deletes state belonging to inactive handlers during Frame.
active bool
active bool
// old is true iff the handler was aded in a previous frame.
old bool
pointer pointerHandler
key keyHandler
// filter the handler has asked for through event handling
@@ -129,6 +135,8 @@ type filter struct {
// stateChange represents the new state and outgoing events
// resulting from an incoming event.
type stateChange struct {
// event, if set, is the trigger for the change.
event event.Event
state inputState
events []taggedEvent
}
@@ -152,13 +160,12 @@ func (q *Router) Source() Source {
return Source{r: q}
}
// Queue a command to be executed after the current frame
// has completed.
func (s Source) Queue(c Command) {
// Execute a command.
func (s Source) Execute(c Command) {
if !s.Enabled() {
return
}
s.r.queue(c)
s.r.execute(c)
}
// Enabled reports whether the source is enabled. Only enabled
@@ -195,6 +202,9 @@ func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
}
}
h.nextFilter.Merge(q.scratchFilter)
if q.deferring {
return events
}
// Accumulate events from state changes until there are no more
// matching events.
matchedIdx := 0
@@ -234,6 +244,20 @@ func (q *Router) collapseState(idx int) {
// 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) {
var remaining []event.Event
if n := len(q.changes); n > 0 {
if q.deferring {
// Collect events for replay.
for _, ch := range q.changes[1:] {
remaining = append(remaining, ch.event)
}
q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state})
} else {
// Collapse state.
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
}
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
@@ -241,11 +265,7 @@ func (q *Router) Frame(frame *op.Ops) {
}
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})
}
q.deferring = false
for _, h := range q.handlers {
h.filter, h.nextFilter = h.nextFilter, h.filter
h.nextFilter.Reset()
@@ -263,12 +283,17 @@ func (q *Router) Frame(frame *op.Ops) {
delete(q.handlers, k)
} else {
h.active = false
h.old = true
}
}
q.executeCommands()
q.changePointerState(q.pointer.queue.Frame(q.handlers, q.lastState().pointerState))
kstate := q.key.queue.Frame(q.handlers, q.lastState().keyState)
q.changeKeyState(kstate, nil)
q.Queue(remaining...)
st := q.lastState()
pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState)
st.pointerState = pst
st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState)
q.changeState(nil, st, evts)
// Collapse state and events.
q.collapseState(len(q.changes) - 1)
@@ -324,9 +349,11 @@ 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(q.handlers, state.pointerState, e))
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
state.pointerState = pstate
return q.changeState(e, state, evts)
case key.Event:
return q.addEvents(q.queueKeyEvent(state.keyState, e))
return q.changeState(e, state, q.queueKeyEvent(state.keyState, e))
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
@@ -341,22 +368,58 @@ func (q *Router) processEvent(e event.Event) bool {
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
return q.addEvents(evts)
return q.changeState(e, state, 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)
return q.changeState(e, state, evts)
case transfer.DataEvent:
return q.changeClipboardState(q.cqueue.Push(state.clipboardState, e))
cstate, evts := q.cqueue.Push(state.clipboardState, e)
state.clipboardState = cstate
return q.changeState(e, state, evts)
default:
panic("unknown event type")
}
}
func (q *Router) queue(f Command) {
q.commands = append(q.commands, f)
func (q *Router) execute(c Command) {
// The command can be executed immediately if:
//
// - event delivery is not frozen, and
// - the influencing tag and event receivers were all seen
// in the previous frame, and
// - no event receiver has completed their event handling.
if !q.deferring {
tag, ch := q.executeCommand(c)
immediate := true
if tag != nil {
h, ok := q.handlers[tag]
immediate = immediate && ok && h.old
}
for _, e := range ch.events {
h, ok := q.handlers[e.tag]
immediate = immediate && ok && h.old && !h.nextFilter.Matches(e.event)
}
if immediate {
// Hold on to the remaining events for state replay.
var evts []event.Event
for _, ch := range q.changes {
if ch.event != nil {
evts = append(evts, ch.event)
}
}
if len(q.changes) > 1 {
q.changes = q.changes[:1]
}
q.changeState(nil, ch.state, ch.events)
q.Queue(evts...)
return
}
}
q.deferring = true
q.commands = append(q.commands, c)
}
func (q *Router) state() inputState {
@@ -373,58 +436,47 @@ func (q *Router) lastState() inputState {
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:
kstate := q.key.queue.setSelection(state.keyState, req)
q.changeKeyState(kstate, nil)
case key.FocusCmd:
q.changeKeyState(q.key.queue.Focus(q.handlers, state.keyState, req.Tag))
case key.SoftKeyboardCmd:
kstate := state.keyState.softKeyboard(req.Show)
q.changeKeyState(kstate, nil)
case key.SnippetCmd:
kstate := q.key.queue.setSnippet(state.keyState, req)
q.changeKeyState(kstate, nil)
case transfer.OfferCmd:
q.changePointerState(q.pointer.queue.offerData(q.handlers, state.pointerState, req))
case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
cstate := q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
q.changeClipboardState(cstate, nil)
case pointer.GrabCmd:
q.changePointerState(q.pointer.queue.grab(state.pointerState, req))
}
for _, c := range q.commands {
_, ch := q.executeCommand(c)
q.changeState(nil, ch.state, ch.events)
}
q.commands = nil
}
func (q *Router) addEvents(evts []taggedEvent) bool {
return q.changeState(q.lastState(), evts)
// executeCommand the command and return the resulting state change along with the
// tag the state change depended on, if any.
func (q *Router) executeCommand(c Command) (event.Tag, stateChange) {
state := q.state()
var evts []taggedEvent
var tag event.Tag
switch req := c.(type) {
case key.SelectionCmd:
tag = req.Tag
state.keyState = q.key.queue.setSelection(state.keyState, req)
case key.FocusCmd:
tag = req.Tag
state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag)
case key.SoftKeyboardCmd:
state.keyState = state.keyState.softKeyboard(req.Show)
case key.SnippetCmd:
tag = req.Tag
state.keyState = q.key.queue.setSnippet(state.keyState, req)
case transfer.OfferCmd:
tag = req.Tag
state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req)
case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
case pointer.GrabCmd:
tag = req.Tag
state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req)
}
return tag, stateChange{state: state, events: evts}
}
func (q *Router) changeState(state inputState, evts []taggedEvent) bool {
func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) bool {
// Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts {
e := &evts[i]
@@ -439,16 +491,18 @@ func (q *Router) changeState(state inputState, evts []taggedEvent) bool {
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})
// Initialize the first change to contain the current state
// and events that are bound for the current frame.
if len(q.changes) == 0 {
q.changes = append(q.changes, stateChange{})
}
if e != nil && len(evts) > 0 {
// An event triggered events bound for user receivers. Add a state change to be
// able to redo the change in case of a command execution.
q.changes = append(q.changes, stateChange{event: e, state: state, events: evts})
} else {
// Otherwise, merge with previous change.
prev := &q.changes[n-1]
prev := &q.changes[len(q.changes)-1]
prev.state = state
prev.events = append(prev.events, evts...)
}
@@ -504,8 +558,10 @@ func (q *Router) queueKeyEvent(state keyState, e key.Event) []taggedEvent {
}
func (q *Router) MoveFocus(dir key.FocusDirection) bool {
ks, evts := q.key.queue.MoveFocus(q.handlers, q.lastState().keyState, dir)
return q.changeKeyState(ks, evts)
state := q.lastState()
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
state.keyState = kstate
return q.changeState(nil, state, evts)
}
// RevealFocus scrolls the current focus (if any) into viewport
@@ -546,7 +602,7 @@ func (q *Router) ScrollFocus(dist image.Point) {
}
kh := &q.handlers[focus].key
area := q.key.queue.AreaFor(kh)
q.addEvents(q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Touch,
Scroll: f32internal.FPt(dist),
@@ -593,16 +649,19 @@ func (q *Router) ClickFocus() {
}
area := q.key.queue.AreaFor(kh)
e.Kind = pointer.Press
q.addEvents(q.pointer.queue.Deliver(q.handlers, area, e))
state := q.lastState()
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
e.Kind = pointer.Release
q.addEvents(q.pointer.queue.Deliver(q.handlers, area, e))
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
}
// TextInputState returns the input state from the most recent
// call to Frame.
func (q *Router) TextInputState() TextInputState {
kstate, s := q.state().InputState()
q.changeKeyState(kstate, nil)
state := q.state()
kstate, s := state.InputState()
state.keyState = kstate
q.changeState(nil, state, nil)
return s
}
@@ -713,7 +772,7 @@ func (q *Router) collect() {
pc.inputOp(tag, &s.pointer)
a := pc.currentArea()
b := pc.currentAreaBounds()
if s.filter.focusable {
if s.filter.key.focusable {
kq.inputOp(tag, &s.key, t, a, b)
}