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
+2 -2
View File
@@ -315,7 +315,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
if e.Priority < pointer.Grabbed { if e.Priority < pointer.Grabbed {
slop := cfg.Dp(touchSlop) slop := cfg.Dp(touchSlop)
if dist := dist; dist >= slop || -slop >= dist { if dist := dist; dist >= slop || -slop >= dist {
q.Queue(pointer.GrabCmd{Tag: s, ID: e.PointerID}) q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
} }
} else { } else {
s.last = v s.last = v
@@ -389,7 +389,7 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) []pointer.Even
diff := e.Position.Sub(d.start) diff := e.Position.Sub(d.start)
slop := cfg.Dp(touchSlop) slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
q.Queue(pointer.GrabCmd{Tag: d, ID: e.PointerID}) q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
} }
} }
case pointer.Release, pointer.Cancel: case pointer.Release, pointer.Cancel:
+7 -17
View File
@@ -17,8 +17,8 @@ func TestClipboardDuplicateEvent(t *testing.T) {
ops, router, handler := new(op.Ops), new(Router), make([]int, 2) ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
// Both must receive the event once // Both must receive the event once
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[1]}) router.Source().Execute(clipboard.ReadCmd{Tag: &handler[1]})
router.Frame(ops) router.Frame(ops)
event := transfer.DataEvent{ event := transfer.DataEvent{
@@ -41,7 +41,7 @@ func TestClipboardDuplicateEvent(t *testing.T) {
assertClipboardEvent(t, router.Events(&handler[1]), false) assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset() ops.Reset()
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops) router.Frame(ops)
// No ClipboardEvent sent // No ClipboardEvent sent
@@ -56,7 +56,7 @@ func TestQueueProcessReadClipboard(t *testing.T) {
ops.Reset() ops.Reset()
// Request read // Request read
router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) router.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops) router.Frame(ops)
assertClipboardReadCmd(t, router, 1) assertClipboardReadCmd(t, router, 1)
@@ -94,28 +94,18 @@ func TestQueueProcessReadClipboard(t *testing.T) {
} }
func TestQueueProcessWriteClipboard(t *testing.T) { func TestQueueProcessWriteClipboard(t *testing.T) {
ops, router := new(op.Ops), new(Router) router := new(Router)
ops.Reset()
const mime = "application/text" 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") assertClipboardWriteCmd(t, router, mime, "Write 1")
ops.Reset()
// No WriteCmd
router.Frame(ops)
assertClipboardWriteCmd(t, router, "", "") 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) assertClipboardReadCmd(t, router, 0)
assertClipboardWriteCmd(t, router, mime, "Write 2") assertClipboardWriteCmd(t, router, mime, "Write 2")
ops.Reset()
} }
func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { 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) ops := new(op.Ops)
r := new(Router) r := new(Router)
r.Source().Queue(key.SoftKeyboardCmd{Show: true}) r.Source().Execute(key.SoftKeyboardCmd{Show: true})
event.InputOp(ops, &handlers[0]) event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{Tag: &handlers[2]})
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
// The last one must be focused: // The last one must be focused:
@@ -51,6 +50,7 @@ func TestKeyMultiples(t *testing.T) {
r.Frame(ops) r.Frame(ops)
r.Source().Execute(key.FocusCmd{Tag: &handlers[2]})
assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), true) assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), true)
assertFocus(t, r, &handlers[2]) assertFocus(t, r, &handlers[2])
@@ -63,12 +63,12 @@ func TestKeyStacked(t *testing.T) {
r := new(Router) r := new(Router)
event.InputOp(ops, &handlers[0]) event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{}) r.Source().Execute(key.FocusCmd{})
r.Source().Queue(key.SoftKeyboardCmd{Show: false}) r.Source().Execute(key.SoftKeyboardCmd{Show: false})
event.InputOp(ops, &handlers[1]) 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]) event.InputOp(ops, &handlers[2])
r.Source().Queue(key.SoftKeyboardCmd{Show: true}) r.Source().Execute(key.SoftKeyboardCmd{Show: true})
event.InputOp(ops, &handlers[3]) event.InputOp(ops, &handlers[3])
for i := range handlers { for i := range handlers {
@@ -88,7 +88,7 @@ func TestKeySoftKeyboardNoFocus(t *testing.T) {
// It's possible to open the keyboard // It's possible to open the keyboard
// without any active focus: // without any active focus:
r.Source().Queue(key.SoftKeyboardCmd{Show: true}) r.Source().Execute(key.SoftKeyboardCmd{Show: true})
r.Frame(ops) r.Frame(ops)
@@ -103,8 +103,8 @@ func TestKeyRemoveFocus(t *testing.T) {
// New InputOp with Focus and Keyboard: // New InputOp with Focus and Keyboard:
event.InputOp(ops, &handlers[0]) event.InputOp(ops, &handlers[0])
r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
r.Source().Queue(key.SoftKeyboardCmd{Show: true}) r.Source().Execute(key.SoftKeyboardCmd{Show: true})
// New InputOp without any focus: // New InputOp without any focus:
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
@@ -136,7 +136,7 @@ func TestKeyRemoveFocus(t *testing.T) {
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
// Remove focus by focusing on a tag that don't exist. // 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) r.Frame(ops)
@@ -149,26 +149,26 @@ func TestKeyRemoveFocus(t *testing.T) {
event.InputOp(ops, &handlers[0]) event.InputOp(ops, &handlers[0])
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0], key.FocusFilter{})) assertKeyEventUnexpected(t, r.Events(&handlers[0], key.FocusFilter{}))
assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{}))
assertFocus(t, r, nil) assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose) assertKeyboard(t, r, TextInputClose)
r.Frame(ops)
ops.Reset() ops.Reset()
// Set focus to InputOp which already // Set focus to InputOp which already
// exists in the previous frame: // 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]) 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. // Remove focus.
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
r.Source().Queue(key.FocusCmd{}) r.Source().Execute(key.FocusCmd{})
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{}))
assertFocus(t, r, nil) assertFocus(t, r, nil)
@@ -181,9 +181,9 @@ func TestKeyFocusedInvisible(t *testing.T) {
r := new(Router) r := new(Router)
// Set new InputOp with focus: // 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]) event.InputOp(ops, &handlers[0])
r.Source().Queue(key.SoftKeyboardCmd{Show: true}) r.Source().Execute(key.SoftKeyboardCmd{Show: true})
// Set new InputOp without focus: // Set new InputOp without focus:
event.InputOp(ops, &handlers[1]) event.InputOp(ops, &handlers[1])
@@ -403,7 +403,7 @@ func TestKeyRouting(t *testing.T) {
r2.Events(&handlers[3], key.FocusFilter{}) r2.Events(&handlers[3], key.FocusFilter{})
r2.Events(&handlers[4], fa...) 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.Frame(ops)
r2.Queue(A, B) r2.Queue(A, B)
+4 -4
View File
@@ -99,10 +99,10 @@ func TestPointerGrab(t *testing.T) {
Position: f32.Pt(50, 50), Position: f32.Pt(50, 50),
}, },
) )
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press) assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press) assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Press) assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Press)
r.Source().Execute(pointer.GrabCmd{Tag: handler1})
r.Frame(&ops) r.Frame(&ops)
r.Queue( r.Queue(
pointer.Event{ pointer.Event{
@@ -136,9 +136,9 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
Position: f32.Pt(50, 50), Position: f32.Pt(50, 50),
}, },
) )
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press) assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press) assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
r.Source().Execute(pointer.GrabCmd{Tag: handler1})
r.Frame(&ops) r.Frame(&ops)
r.Queue( r.Queue(
pointer.Event{ pointer.Event{
@@ -941,7 +941,7 @@ func TestTransfer(t *testing.T) {
// Offer valid type and data. // Offer valid type and data.
ofr := &offer{data: "hello"} 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) r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
evs := r.Events(tgt, transfer.TargetFilter{Type: "file"}) evs := r.Events(tgt, transfer.TargetFilter{Type: "file"})
@@ -989,9 +989,9 @@ func TestTransfer(t *testing.T) {
}, },
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Events(src, transfer.SourceFilter{Type: "file"}) r.Events(src, transfer.SourceFilter{Type: "file"})
r.Events(tgt, transfer.TargetFilter{Type: "file"}) r.Events(tgt, transfer.TargetFilter{Type: "file"})
r.Source().Execute(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Frame(ops) r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
// Ignore DataEvent and verify that the next frame closes it as unused. // 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 cqueue clipboardQueue
// states is the list of pending state changes resulting from // states is the list of pending state changes resulting from
// incoming events. The first element is the current state, // incoming events. The first element, if present, contains the state
// if any. // and events for the current frame.
changes []stateChange changes []stateChange
reader ops.Reader reader ops.Reader
@@ -54,6 +54,10 @@ type Router struct {
// transfers is the pending transfer.DataEvent.Open functions. // transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser 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 is for garbage-free construction of ephemeral filters.
scratchFilter filter scratchFilter filter
} }
@@ -109,7 +113,9 @@ type SemanticID uint
type handler struct { type handler struct {
// active tracks whether the handler was active in the current // active tracks whether the handler was active in the current
// frame. Router deletes state belonging to inactive handlers during Frame. // 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 pointer pointerHandler
key keyHandler key keyHandler
// filter the handler has asked for through event handling // filter the handler has asked for through event handling
@@ -129,6 +135,8 @@ type filter struct {
// stateChange represents the new state and outgoing events // stateChange represents the new state and outgoing events
// resulting from an incoming event. // resulting from an incoming event.
type stateChange struct { type stateChange struct {
// event, if set, is the trigger for the change.
event event.Event
state inputState state inputState
events []taggedEvent events []taggedEvent
} }
@@ -152,13 +160,12 @@ func (q *Router) Source() Source {
return Source{r: q} return Source{r: q}
} }
// Queue a command to be executed after the current frame // Execute a command.
// has completed. func (s Source) Execute(c Command) {
func (s Source) Queue(c Command) {
if !s.Enabled() { if !s.Enabled() {
return return
} }
s.r.queue(c) s.r.execute(c)
} }
// Enabled reports whether the source is enabled. Only enabled // 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) h.nextFilter.Merge(q.scratchFilter)
if q.deferring {
return events
}
// Accumulate events from state changes until there are no more // Accumulate events from state changes until there are no more
// matching events. // matching events.
matchedIdx := 0 matchedIdx := 0
@@ -234,6 +244,20 @@ func (q *Router) collapseState(idx int) {
// operation list. The text input state, wakeup time and whether // operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved. // there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) { 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 { for _, rc := range q.transfers {
if rc != nil { if rc != nil {
rc.Close() rc.Close()
@@ -241,11 +265,7 @@ func (q *Router) Frame(frame *op.Ops) {
} }
q.transfers = nil q.transfers = nil
q.wakeup = false q.wakeup = false
// Collapse state and clear events. q.deferring = false
if n := len(q.changes); n > 1 {
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
for _, h := range q.handlers { for _, h := range q.handlers {
h.filter, h.nextFilter = h.nextFilter, h.filter h.filter, h.nextFilter = h.nextFilter, h.filter
h.nextFilter.Reset() h.nextFilter.Reset()
@@ -263,12 +283,17 @@ func (q *Router) Frame(frame *op.Ops) {
delete(q.handlers, k) delete(q.handlers, k)
} else { } else {
h.active = false h.active = false
h.old = true
} }
} }
q.executeCommands() q.executeCommands()
q.changePointerState(q.pointer.queue.Frame(q.handlers, q.lastState().pointerState)) q.Queue(remaining...)
kstate := q.key.queue.Frame(q.handlers, q.lastState().keyState) st := q.lastState()
q.changeKeyState(kstate, nil) 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. // Collapse state and events.
q.collapseState(len(q.changes) - 1) q.collapseState(len(q.changes) - 1)
@@ -324,9 +349,11 @@ func (q *Router) processEvent(e event.Event) bool {
state := q.lastState() state := q.lastState()
switch e := e.(type) { switch e := e.(type) {
case pointer.Event: 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: case key.Event:
return q.addEvents(q.queueKeyEvent(state.keyState, e)) return q.changeState(e, state, q.queueKeyEvent(state.keyState, e))
case key.SnippetEvent: case key.SnippetEvent:
// Expand existing, overlapping snippet. // Expand existing, overlapping snippet.
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) { 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 { if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e}) 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: case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent var evts []taggedEvent
if f := state.focus; f != nil { if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e}) evts = append(evts, taggedEvent{tag: f, event: e})
} }
return q.addEvents(evts) return q.changeState(e, state, evts)
case transfer.DataEvent: 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: default:
panic("unknown event type") panic("unknown event type")
} }
} }
func (q *Router) queue(f Command) { func (q *Router) execute(c Command) {
q.commands = append(q.commands, f) // 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 { func (q *Router) state() inputState {
@@ -373,58 +436,47 @@ func (q *Router) lastState() inputState {
return 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() { func (q *Router) executeCommands() {
for _, req := range q.commands { for _, c := range q.commands {
state := q.lastState() _, ch := q.executeCommand(c)
switch req := req.(type) { q.changeState(nil, ch.state, ch.events)
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))
}
} }
q.commands = nil q.commands = nil
} }
func (q *Router) addEvents(evts []taggedEvent) bool { // executeCommand the command and return the resulting state change along with the
return q.changeState(q.lastState(), evts) // 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. // Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts { for i := range evts {
e := &evts[i] e := &evts[i]
@@ -439,16 +491,18 @@ func (q *Router) changeState(state inputState, evts []taggedEvent) bool {
e.event = de e.event = de
} }
} }
n := len(q.changes) // Initialize the first change to contain the current state
// We must add a new state change if // and events that are bound for the current frame.
// if len(q.changes) == 0 {
// - there is no first state change, or q.changes = append(q.changes, stateChange{})
// - 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) { if e != nil && len(evts) > 0 {
q.changes = append(q.changes, stateChange{state: state, events: evts}) // 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 { } else {
// Otherwise, merge with previous change. // Otherwise, merge with previous change.
prev := &q.changes[n-1] prev := &q.changes[len(q.changes)-1]
prev.state = state prev.state = state
prev.events = append(prev.events, evts...) 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 { func (q *Router) MoveFocus(dir key.FocusDirection) bool {
ks, evts := q.key.queue.MoveFocus(q.handlers, q.lastState().keyState, dir) state := q.lastState()
return q.changeKeyState(ks, evts) 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 // 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 kh := &q.handlers[focus].key
area := q.key.queue.AreaFor(kh) 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, Kind: pointer.Scroll,
Source: pointer.Touch, Source: pointer.Touch,
Scroll: f32internal.FPt(dist), Scroll: f32internal.FPt(dist),
@@ -593,16 +649,19 @@ func (q *Router) ClickFocus() {
} }
area := q.key.queue.AreaFor(kh) area := q.key.queue.AreaFor(kh)
e.Kind = pointer.Press 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 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 // TextInputState returns the input state from the most recent
// call to Frame. // call to Frame.
func (q *Router) TextInputState() TextInputState { func (q *Router) TextInputState() TextInputState {
kstate, s := q.state().InputState() state := q.state()
q.changeKeyState(kstate, nil) kstate, s := state.InputState()
state.keyState = kstate
q.changeState(nil, state, nil)
return s return s
} }
@@ -713,7 +772,7 @@ func (q *Router) collect() {
pc.inputOp(tag, &s.pointer) pc.inputOp(tag, &s.pointer)
a := pc.currentArea() a := pc.currentArea()
b := pc.currentAreaBounds() b := pc.currentAreaBounds()
if s.filter.focusable { if s.filter.key.focusable {
kq.inputOp(tag, &s.key, t, a, b) kq.inputOp(tag, &s.key, t, a, b)
} }
+2 -2
View File
@@ -80,7 +80,7 @@ func (b *Clickable) Pressed() bool {
// Focus requests the input focus for the element. // Focus requests the input focus for the element.
func (b *Clickable) Focus(gtx layout.Context) { func (b *Clickable) Focus(gtx layout.Context) {
gtx.Queue(key.FocusCmd{Tag: &b.keyTag}) gtx.Execute(key.FocusCmd{Tag: &b.keyTag})
} }
// Focused reports whether b has focus. // Focused reports whether b has focus.
@@ -149,7 +149,7 @@ func (b *Clickable) Update(gtx layout.Context) []Click {
} }
case gesture.KindPress: case gesture.KindPress:
if e.Source == pointer.Mouse { if e.Source == pointer.Mouse {
gtx.Queue(key.FocusCmd{Tag: &b.keyTag}) gtx.Execute(key.FocusCmd{Tag: &b.keyTag})
} }
b.history = append(b.history, Press{ b.history = append(b.history, Press{
Position: e.Position, Position: e.Position,
+1 -1
View File
@@ -76,7 +76,7 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
// Offer the data ready for a drop. Must be called after being Requested. // Offer the data ready for a drop. Must be called after being Requested.
// The mime must be one in the requested list. // The mime must be one in the requested list.
func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) { func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) {
gtx.Queue(transfer.OfferCmd{Tag: &d.handle, Type: mime, Data: data}) gtx.Execute(transfer.OfferCmd{Tag: &d.handle, Type: mime, Data: data})
} }
// Pos returns the drag position relative to its initial click position. // Pos returns the drag position relative to its initial click position.
+2 -3
View File
@@ -51,17 +51,16 @@ func TestDraggable(t *testing.T) {
}, },
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
drag.Offer(gtx, "file", ofr)
drag.Update(gtx) drag.Update(gtx)
r.Events(drag, transfer.TargetFilter{Type: drag.Type}) r.Events(drag, transfer.TargetFilter{Type: drag.Type})
drag.Offer(gtx, "file", ofr)
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
evs := r.Events(drag, transfer.TargetFilter{Type: drag.Type}) evs := r.Events(drag, transfer.TargetFilter{Type: drag.Type})
if len(evs) != 2 { if len(evs) != 2 {
t.Fatalf("expected 2 event, got %d", len(evs)) t.Fatalf("expected 3 event, got %d", len(evs))
} }
ev := evs[0].(transfer.DataEvent) ev := evs[0].(transfer.DataEvent)
ev.Open = nil
if got, want := ev.Type, "file"; got != want { if got, want := ev.Type, "file"; got != want {
t.Errorf("expected %v; got %v", got, want) t.Errorf("expected %v; got %v", got, want)
} }
+6 -6
View File
@@ -475,13 +475,13 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
// half is in Editor.processKey() under clipboard.Event. // half is in Editor.processKey() under clipboard.Event.
case "V": case "V":
if !e.ReadOnly { if !e.ReadOnly {
gtx.Queue(clipboard.ReadCmd{Tag: &e.eventKey}) gtx.Execute(clipboard.ReadCmd{Tag: &e.eventKey})
} }
// Copy or Cut selection -- ignored if nothing selected. // Copy or Cut selection -- ignored if nothing selected.
case "C", "X": case "C", "X":
e.scratch = e.text.SelectedText(e.scratch) e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" { if text := string(e.scratch); text != "" {
gtx.Queue(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
if k.Name == "X" && !e.ReadOnly { if k.Name == "X" && !e.ReadOnly {
e.Delete(1) e.Delete(1)
} }
@@ -556,8 +556,8 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
// Focus requests the input focus for the Editor. // Focus requests the input focus for the Editor.
func (e *Editor) Focus(gtx layout.Context) { func (e *Editor) Focus(gtx layout.Context) {
gtx.Queue(key.FocusCmd{Tag: &e.eventKey}) gtx.Execute(key.FocusCmd{Tag: &e.eventKey})
gtx.Queue(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
} }
// Focused returns whether the editor is focused or not. // Focused returns whether the editor is focused or not.
@@ -601,7 +601,7 @@ func (e *Editor) Update(gtx layout.Context) {
} }
if newSel != e.ime.selection { if newSel != e.ime.selection {
e.ime.selection = newSel e.ime.selection = newSel
gtx.Queue(key.SelectionCmd{Tag: &e.eventKey, Range: newSel.rng, Caret: newSel.caret}) gtx.Execute(key.SelectionCmd{Tag: &e.eventKey, Range: newSel.rng, Caret: newSel.caret})
} }
e.updateSnippet(gtx, e.ime.start, e.ime.end) e.updateSnippet(gtx, e.ime.start, e.ime.end)
@@ -658,7 +658,7 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
return return
} }
e.ime.snippet = newSnip e.ime.snippet = newSnip
gtx.Queue(key.SnippetCmd{Tag: &e.eventKey, Snippet: newSnip}) gtx.Execute(key.SnippetCmd{Tag: &e.eventKey, Snippet: newSnip})
} }
func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions { func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
+1 -1
View File
@@ -51,7 +51,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
switch ev.Kind { switch ev.Kind {
case gesture.KindPress: case gesture.KindPress:
if ev.Source == pointer.Mouse { if ev.Source == pointer.Mouse {
gtx.Queue(key.FocusCmd{Tag: &state.tag}) gtx.Execute(key.FocusCmd{Tag: &state.tag})
} }
case gesture.KindClick: case gesture.KindClick:
if state.key != e.Value { if state.key != e.Value {
+3 -3
View File
@@ -100,8 +100,8 @@ func (l *Selectable) initialize() {
// Focus requests the input focus for the label. // Focus requests the input focus for the label.
func (l *Selectable) Focus(gtx layout.Context) { func (l *Selectable) Focus(gtx layout.Context) {
gtx.Queue(key.FocusCmd{Tag: l}) gtx.Execute(key.FocusCmd{Tag: l})
gtx.Queue(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
} }
// Focused returns whether the label is focused or not. // Focused returns whether the label is focused or not.
@@ -348,7 +348,7 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case "C", "X": case "C", "X":
e.scratch = e.text.SelectedText(e.scratch) e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" { if text := string(e.scratch); text != "" {
gtx.Queue(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
} }
// Select all // Select all
case "A": case "A":