From 9dfada745c663a4770adf5f6a1b040f27c95eb28 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Wed, 15 Nov 2023 19:14:14 -0600 Subject: [PATCH] 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 --- app/window.go | 2 +- io/input/clipboard.go | 43 +++--- io/input/clipboard_test.go | 14 +- io/input/key.go | 118 +++++++-------- io/input/key_test.go | 4 +- io/input/pointer.go | 153 +++++++++++--------- io/input/pointer_test.go | 1 + io/input/router.go | 285 +++++++++++++++++++++++++++---------- 8 files changed, 390 insertions(+), 230 deletions(-) diff --git a/app/window.go b/app/window.go index aa99df69..3d348b1f 100644 --- a/app/window.go +++ b/app/window.go @@ -319,7 +319,7 @@ func (w *Window) processFrame(d driver) { if mime, txt, ok := q.WriteClipboard(); ok { d.WriteClipboard(mime, txt) } - if q.ReadClipboard() { + if q.ClipboardRequested() { d.ReadClipboard() } oldState := w.imeState diff --git a/io/input/clipboard.go b/io/input/clipboard.go index d7ef98d5..3f40f6d9 100644 --- a/io/input/clipboard.go +++ b/io/input/clipboard.go @@ -9,8 +9,12 @@ import ( "gioui.org/io/event" ) +// clipboardState contains the state for clipboard event routing. +type clipboardState struct { + receivers []event.Tag +} + type clipboardQueue struct { - receivers map[event.Tag]struct{} // request avoid read clipboard every frame while waiting. requested bool mime string @@ -28,22 +32,21 @@ func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) return q.mime, content, true } -// ReadClipboard reports if any new handler is waiting +// ClipboardRequested reports if any new handler is waiting // to read the clipboard. -func (q *clipboardQueue) ReadClipboard() bool { - if len(q.receivers) == 0 || q.requested { - return false - } - q.requested = true - return true +func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool { + req := len(state.receivers) > 0 && q.requested + q.requested = false + return req } -func (q *clipboardQueue) Push(evts []taggedEvent, e event.Event) []taggedEvent { - for r := range q.receivers { +func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) { + var evts []taggedEvent + for _, r := range state.receivers { evts = append(evts, taggedEvent{tag: r, event: e}) - delete(q.receivers, r) } - return evts + state.receivers = nil + return state, evts } func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { @@ -56,12 +59,14 @@ func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { q.text = content } -func (q *clipboardQueue) ProcessReadClipboard(tag event.Tag) { - if q.receivers == nil { - q.receivers = make(map[event.Tag]struct{}) - } - if _, ok := q.receivers[tag]; !ok { - q.receivers[tag] = struct{}{} - q.requested = false +func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState { + for _, k := range state.receivers { + if k == tag { + return state + } } + n := len(state.receivers) + state.receivers = append(state.receivers[:n:n], tag) + q.requested = true + return state } diff --git a/io/input/clipboard_test.go b/io/input/clipboard_test.go index fea3973f..400ccec2 100644 --- a/io/input/clipboard_test.go +++ b/io/input/clipboard_test.go @@ -28,9 +28,9 @@ func TestClipboardDuplicateEvent(t *testing.T) { }, } router.Queue(event) - assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true) assertClipboardEvent(t, router.Events(&handler[1], transfer.TargetFilter{Type: "application/text"}), true) + assertClipboardReadCmd(t, router, 0) ops.Reset() // No ReadCmd @@ -80,8 +80,8 @@ func TestQueueProcessReadClipboard(t *testing.T) { }, } router.Queue(event) - assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true) + assertClipboardReadCmd(t, router, 0) ops.Reset() // No ReadCmd @@ -137,20 +137,20 @@ func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { func assertClipboardReadCmd(t *testing.T, router *Router, expected int) { t.Helper() - if len(router.cqueue.receivers) != expected { - t.Error("unexpected number of receivers") + if got := len(router.lastState().receivers); got != expected { + t.Errorf("unexpected %d receivers, got %d", expected, got) } - if router.cqueue.ReadClipboard() != (expected > 0) { + if router.ClipboardRequested() != (expected > 0) { t.Error("missing requests") } } func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) { t.Helper() - if len(router.cqueue.receivers) != expected { + if len(router.lastState().receivers) != expected { t.Error("receivers removed") } - if router.cqueue.ReadClipboard() != false { + if router.ClipboardRequested() != false { t.Error("duplicated requests") } } diff --git a/io/input/key.go b/io/input/key.go index 5acd9a05..c3b73e0b 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -24,13 +24,17 @@ type EditorState struct { type TextInputState uint8 type keyQueue struct { - focus event.Tag order []event.Tag dirOrder []dirFocusEntry handlers map[event.Tag]*keyHandler - state TextInputState hint key.InputHint - content EditorState +} + +// keyState is the input state related to key events. +type keyState struct { + focus event.Tag + state TextInputState + content EditorState } type keyHandler struct { @@ -67,21 +71,18 @@ func (q *keyQueue) inputHint(op key.InputHintOp) { h.hint = op.Hint } -// InputState returns the last text input state as -// determined in Frame. -func (q *keyQueue) InputState() TextInputState { - state := q.state - q.state = TextInputKeep - return state +// InputState returns the input state and returns a state +// reset to [TextInputKeep]. +func (s keyState) InputState() (keyState, TextInputState) { + state := s.state + s.state = TextInputKeep + return s, state } // InputHint returns the input hint from the focused handler and whether it was // changed since the last call. -func (q *keyQueue) InputHint() (key.InputHint, bool) { - if q.focus == nil { - return q.hint, false - } - focused, ok := q.handlers[q.focus] +func (q *keyQueue) InputHint(state keyState) (key.InputHint, bool) { + focused, ok := q.handlers[state.focus] if !ok { return q.hint, false } @@ -108,13 +109,13 @@ func (q *keyQueue) ResetEvent(k event.Tag) (event.Event, bool) { return key.FocusEvent{Focus: false}, true } -func (q *keyQueue) Frame() { +func (q *keyQueue) Frame(state keyState) keyState { for k, h := range q.handlers { if !h.visible || !h.focusable { - if q.focus == k { + if state.focus == k { // Remove focus from the handler that is no longer focusable. - q.focus = nil - q.state = TextInputClose + state.focus = nil + state.state = TextInputClose } if !h.visible && !h.focusable { delete(q.handlers, k) @@ -126,6 +127,7 @@ func (q *keyQueue) Frame() { h.active = false } q.updateFocusLayout() + return state } // updateFocusLayout partitions input handlers handlers into rows @@ -168,13 +170,13 @@ func (q *keyQueue) updateFocusLayout() { } // MoveFocus attempts to move the focus in the direction of dir. -func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []taggedEvent { +func (q *keyQueue) MoveFocus(state keyState, dir key.FocusDirection) (keyState, []taggedEvent) { if len(q.dirOrder) == 0 { - return nil + return state, nil } order := 0 - if q.focus != nil { - order = q.handlers[q.focus].dirOrder + if state.focus != nil { + order = q.handlers[state.focus].dirOrder } focus := q.dirOrder[order] switch dir { @@ -186,8 +188,8 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge if dir == key.FocusBackward { order = -1 } - if q.focus != nil { - order = q.handlers[q.focus].order + if state.focus != nil { + order = q.handlers[state.focus].order if dir == key.FocusForward { order++ } else { @@ -195,10 +197,10 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge } } order = (order + len(q.order)) % len(q.order) - return q.Focus(evts, q.order[order]) + return q.Focus(state, q.order[order]) case key.FocusRight, key.FocusLeft: next := order - if q.focus != nil { + if state.focus != nil { next = order + 1 if dir == key.FocusLeft { next = order - 1 @@ -207,7 +209,7 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge if 0 <= next && next < len(q.dirOrder) { newFocus := q.dirOrder[next] if newFocus.row == focus.row { - return q.Focus(evts, newFocus.tag) + return q.Focus(state, newFocus.tag) } } case key.FocusUp, key.FocusDown: @@ -216,7 +218,7 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge delta = -1 } nextRow := 0 - if q.focus != nil { + if state.focus != nil { nextRow = focus.row + delta } var closest event.Tag @@ -243,10 +245,10 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge order += delta } if closest != nil { - return q.Focus(evts, closest) + return q.Focus(state, closest) } } - return nil + return state, nil } func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle { @@ -281,35 +283,37 @@ func keyFilterMatch(f key.Filter, e key.Event) bool { return true } -func (q *keyQueue) Focus(evts []taggedEvent, focus event.Tag) []taggedEvent { +func (q *keyQueue) Focus(state keyState, focus event.Tag) (keyState, []taggedEvent) { if focus != nil { if _, exists := q.handlers[focus]; !exists { focus = nil } } - if focus == q.focus { - return evts + if focus == state.focus { + return state, nil } - q.content = EditorState{} - if q.focus != nil { - evts = append(evts, taggedEvent{tag: q.focus, event: key.FocusEvent{Focus: false}}) + state.content = EditorState{} + var evts []taggedEvent + if state.focus != nil { + evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}}) } - q.focus = focus - if q.focus != nil { - evts = append(evts, taggedEvent{tag: q.focus, event: key.FocusEvent{Focus: true}}) + state.focus = focus + if state.focus != nil { + evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}}) } - if q.focus == nil || q.state == TextInputKeep { - q.state = TextInputClose + if state.focus == nil || state.state == TextInputKeep { + state.state = TextInputClose } - return evts + return state, evts } -func (q *keyQueue) softKeyboard(show bool) { +func (s keyState) softKeyboard(show bool) keyState { if show { - q.state = TextInputOpen + s.state = TextInputOpen } else { - q.state = TextInputClose + s.state = TextInputClose } + return s } func (q *keyQueue) filter(tag event.Tag, f key.Filter) { @@ -349,26 +353,28 @@ func (q *keyQueue) inputOp(tag event.Tag, t f32.Affine2D, area int, bounds image h.trans = t } -func (q *keyQueue) setSelection(req key.SelectionCmd) { - if req.Tag != q.focus { - return +func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState { + if req.Tag != state.focus { + return state } - q.content.Selection.Range = req.Range - q.content.Selection.Caret = req.Caret + state.content.Selection.Range = req.Range + state.content.Selection.Caret = req.Caret + return state } -func (q *keyQueue) editorState() EditorState { - s := q.content - if f := q.focus; f != nil { +func (q *keyQueue) editorState(state keyState) EditorState { + s := state.content + if f := state.focus; f != nil { s.Selection.Transform = q.handlers[f].trans } return s } -func (q *keyQueue) setSnippet(req key.SnippetCmd) { - if req.Tag == q.focus { - q.content.Snippet = req.Snippet +func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState { + if req.Tag == state.focus { + state.content.Snippet = req.Snippet } + return state } func (t TextInputState) String() string { diff --git a/io/input/key_test.go b/io/input/key_test.go index e4a707aa..3cfcd9ca 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -464,14 +464,14 @@ func assertKeyEventUnexpected(t *testing.T, events []event.Event) { func assertFocus(t *testing.T, router *Router, expected event.Tag) { t.Helper() - if got := router.key.queue.focus; got != expected { + if got := router.lastState().focus; got != expected { t.Errorf("expected %v to be focused, got %v", expected, got) } } func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { t.Helper() - if got := router.key.queue.state; got != expected { + if got := router.lastState().state; got != expected { t.Errorf("expected %v keyboard, got %v", expected, got) } } diff --git a/io/input/pointer.go b/io/input/pointer.go index 722e883e..1fd9f924 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -17,12 +17,9 @@ import ( ) type pointerQueue struct { - hitTree []hitNode - areas []areaNode - cursor pointer.Cursor - handlers map[event.Tag]*pointerHandler - pointers []pointerInfo - transfers []io.ReadCloser // pending data transfers + hitTree []hitNode + areas []areaNode + handlers map[event.Tag]*pointerHandler semantic struct { idsAssigned bool @@ -43,6 +40,12 @@ type hitNode struct { pass bool } +// pointerState is the input state related to pointer events. +type pointerState struct { + cursor pointer.Cursor + pointers []pointerInfo +} + type pointerInfo struct { id pointer.ID pressed bool @@ -264,8 +267,9 @@ func (c *pointerCollector) actionInputOp(act system.Action) { area.action = act } -func (q *pointerQueue) grab(evts []taggedEvent, req pointer.GrabCmd) []taggedEvent { - for _, p := range q.pointers { +func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) { + var evts []taggedEvent + for _, p := range state.pointers { if !p.pressed || p.id != req.ID { continue } @@ -276,12 +280,12 @@ func (q *pointerQueue) grab(evts []taggedEvent, req pointer.GrabCmd) []taggedEve tag: tag, event: pointer.Event{Kind: pointer.Cancel}, }) - q.dropHandler(tag) + state = dropHandler(state, tag) } } break } - return evts + return state, evts } func (c *pointerCollector) inputOp(tag event.Tag) { @@ -357,29 +361,25 @@ func (q *pointerQueue) targetFilter(tag event.Tag, f transfer.TargetFilter) { h.targetMimes = append(h.targetMimes, f.Type) } -func (q *pointerQueue) offerData(evts []taggedEvent, req transfer.OfferCmd) []taggedEvent { - transferIdx := len(q.transfers) - q.transfers = append(q.transfers, req.Data) - for i := range q.pointers { - p := q.pointers[i] +func (q *pointerQueue) offerData(state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) { + var evts []taggedEvent + for i, p := range state.pointers { if p.dataSource != req.Tag { continue } - if p.dataTarget == nil { - q.pointers[i], evts = q.deliverTransferCancelEvent(p, evts) - break + if p.dataTarget != nil { + evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{ + Type: req.Type, + Open: func() io.ReadCloser { + return req.Data + }, + }}) } - evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{ - Type: req.Type, - Open: func() io.ReadCloser { - q.transfers[transferIdx] = nil - return req.Data - }, - }}) - q.pointers[i], evts = q.deliverTransferCancelEvent(p, evts) + state.pointers = append([]pointerInfo{}, state.pointers...) + state.pointers[i], evts = q.deliverTransferCancelEvent(p, evts) break } - return evts + return state, evts } func (c *pointerCollector) reset() { @@ -595,18 +595,12 @@ func (q *pointerQueue) reset() { delete(q.semantic.contentIDs, k) } } - for _, rc := range q.transfers { - if rc != nil { - rc.Close() - } - } - q.transfers = nil } -func (q *pointerQueue) Frame(evts []taggedEvent) []taggedEvent { +func (q *pointerQueue) Frame(state pointerState) (pointerState, []taggedEvent) { for k, h := range q.handlers { if !h.active { - q.dropHandler(k) + state = dropHandler(state, k) delete(q.handlers, k) continue } @@ -622,38 +616,51 @@ func (q *pointerQueue) Frame(evts []taggedEvent) []taggedEvent { area.semantic.valid = area.semantic.content.gestures != 0 } } - for i := range q.pointers { - p := q.pointers[i] - q.pointers[i], evts = q.deliverEnterLeaveEvents(p, evts, p.last) + var evts []taggedEvent + for i, p := range state.pointers { + changed := false + p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(state.cursor, p, evts, p.last) + if changed { + state.pointers = append([]pointerInfo{}, state.pointers...) + state.pointers[i] = p + } } - return evts + return state, evts } -func (q *pointerQueue) dropHandler(tag event.Tag) { - for i := range q.pointers { - p := &q.pointers[i] - for i := len(p.handlers) - 1; i >= 0; i-- { - if p.handlers[i] == tag { - p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) +func dropHandler(state pointerState, tag event.Tag) pointerState { + pointers := state.pointers + state.pointers = nil + for _, p := range pointers { + handlers := p.handlers + p.handlers = nil + for _, h := range handlers { + if h != tag { + p.handlers = append(p.handlers, h) } } - for i := len(p.entered) - 1; i >= 0; i-- { - if p.entered[i] == tag { - p.entered = append(p.entered[:i], p.entered[i+1:]...) + entered := p.entered + p.entered = nil + for _, h := range entered { + if h != tag { + p.entered = append(p.entered, h) } } + state.pointers = append(state.pointers, p) } + return state } // pointerOf returns the pointerInfo index corresponding to the pointer in e. -func (q *pointerQueue) pointerOf(e pointer.Event) int { - for i, p := range q.pointers { +func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) { + for i, p := range s.pointers { if p.id == e.PointerID { - return i + return s, i } } - q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) - return len(q.pointers) - 1 + n := len(s.pointers) + s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID}) + return s, len(s.pointers) - 1 } // Deliver is like Push, but delivers an event to a particular area. @@ -710,7 +717,8 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) { return semanticContent{}, -1 } -func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent { +func (q *pointerQueue) Push(state pointerState, e pointer.Event) (pointerState, []taggedEvent) { + var evts []taggedEvent if e.Kind == pointer.Cancel { for k := range q.handlers { evts = append(evts, taggedEvent{ @@ -718,25 +726,22 @@ func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent { tag: k, }) } - q.pointers = q.pointers[:0] - for k := range q.handlers { - q.dropHandler(k) - } - return evts + state.pointers = nil + return state, evts } - pidx := q.pointerOf(e) - p := q.pointers[pidx] + state, pidx := state.pointerOf(e) + p := state.pointers[pidx] switch e.Kind { case pointer.Press: - p, evts = q.deliverEnterLeaveEvents(p, evts, e) + p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e) p.pressed = true evts = q.deliverEvent(p, evts, e) case pointer.Move: if p.pressed { e.Kind = pointer.Drag } - p, evts = q.deliverEnterLeaveEvents(p, evts, e) + p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e) evts = q.deliverEvent(p, evts, e) if p.pressed { p, evts = q.deliverDragEvent(p, evts) @@ -744,23 +749,25 @@ func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent { case pointer.Release: evts = q.deliverEvent(p, evts, e) p.pressed = false - p, evts = q.deliverEnterLeaveEvents(p, evts, e) + p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e) p, evts = q.deliverDropEvent(p, evts) case pointer.Scroll: - p, evts = q.deliverEnterLeaveEvents(p, evts, e) + p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e) evts = q.deliverEvent(p, evts, e) default: panic("unsupported pointer event type") } - q.pointers[pidx] = p p.last = e if !p.pressed && len(p.entered) == 0 { // No longer need to track pointer. - q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) + state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...) + } else { + state.pointers = append([]pointerInfo{}, state.pointers...) + state.pointers[pidx] = p } - return evts + return state, evts } func (q *pointerQueue) deliverEvent(p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent { @@ -794,12 +801,13 @@ func (q *pointerQueue) deliverEvent(p pointerInfo, evts []taggedEvent, e pointer return evts } -func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent) { +func (q *pointerQueue) deliverEnterLeaveEvents(cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) { + changed := false var hits []event.Tag if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { // Consider non-mouse pointers leaving when they're released. } else { - hits, q.cursor = q.opHit(e.Position) + hits, cursor = q.opHit(e.Position) if p.pressed { // Filter out non-participating handlers, // except potential transfer targets when a transfer has been initiated. @@ -819,6 +827,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent } } } else { + changed = true p.handlers = hits } } @@ -827,6 +836,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent if _, found := searchTag(hits, k); found { continue } + changed = true h := q.handlers[k] e := e e.Kind = pointer.Leave @@ -842,6 +852,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent if _, found := searchTag(p.entered, k); found { continue } + changed = true e := e e.Kind = pointer.Enter @@ -851,7 +862,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent } } p.entered = hits - return p, evts + return p, evts, cursor, changed } func (q *pointerQueue) deliverDragEvent(p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index 1af7b936..dac2a495 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -1037,6 +1037,7 @@ func TestPassCursor(t *testing.T) { Position: f32.Pt(10, 10), Kind: pointer.Move, }) + r.Frame(&ops) if got := r.Cursor(); want != got { t.Errorf("got cursor %v, want %v", got, want) } diff --git a/io/input/router.go b/io/input/router.go index 3d481ce0..288e7512 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -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() {