From ed0d5d5767fa3565710931cbb36c3a86f5bb58bb Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sun, 26 Nov 2023 17:54:51 -0600 Subject: [PATCH] all: [API] deliver key events to the first matching filter Replace the key.Filter.Target field with a Focus field that matches only of the specified tag has focus. This has the advantage of simpler event delivery and for lower latency in delivering key events to new handlers. For example, consider a UI where a button is activated by a key press, which is turn displays a dialog with another button activated by the same key. This change allows two button press(+releases) in the same frame to arrive at the intended targets: one key press(+release) for each button. Signed-off-by: Elias Naur --- io/input/key.go | 43 +++++-------- io/input/pointer_test.go | 3 - io/input/router.go | 127 ++++++++++++++++++++------------------- io/key/key.go | 12 ++-- widget/button.go | 15 ++--- widget/editor.go | 78 +++++++++++------------- widget/editor_test.go | 23 +++++++ widget/enum.go | 15 ++--- widget/selectable.go | 37 +++++------- 9 files changed, 169 insertions(+), 184 deletions(-) diff --git a/io/input/key.go b/io/input/key.go index 30a37326..8841863b 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -49,10 +49,7 @@ type keyHandler struct { trans f32.Affine2D } -type keyFilter struct { - focusable bool - filters []key.Filter -} +type keyFilter []key.Filter type dirFocusEntry struct { tag event.Tag @@ -112,7 +109,7 @@ func (k *keyHandler) ResetEvent() (event.Event, bool) { func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState { if state.focus != nil { - if h, ok := handlers[state.focus]; !ok || !h.filter.key.focusable || !h.key.visible { + if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible { // Remove focus from the handler that is no longer focusable. state.focus = nil state.state = TextInputClose @@ -253,21 +250,19 @@ func (q *keyQueue) AreaFor(k *keyHandler) int { return q.dirOrder[order].area } -func (k *keyFilter) Matches(e event.Event) bool { - switch e := e.(type) { - case key.Event: - for _, f := range k.filters { - if keyFilterMatch(f, e) { - return true - } +func (k *keyFilter) Matches(focus event.Tag, e key.Event) bool { + for _, f := range *k { + if keyFilterMatch(focus, f, e) { + return true } - case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: - return k.focusable } return false } -func keyFilterMatch(f key.Filter, e key.Event) bool { +func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event) bool { + if f.Focus != nil && f.Focus != focus { + return false + } if f.Name != e.Name { return false } @@ -308,23 +303,17 @@ func (s keyState) softKeyboard(show bool) keyState { return s } -func (k *keyFilter) Add(f event.Filter) { - switch f := f.(type) { - case key.FocusFilter: - k.focusable = true - case key.Filter: - for _, f2 := range k.filters { - if f == f2 { - return - } +func (k *keyFilter) Add(f key.Filter) { + for _, f2 := range *k { + if f == f2 { + return } - k.filters = append(k.filters, f) } + *k = append(*k, f) } func (k *keyFilter) Merge(k2 keyFilter) { - k.focusable = k.focusable || k2.focusable - k.filters = append(k.filters, k2.filters...) + *k = append(*k, k2...) } func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) { diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index b9bccc4c..309f5e6c 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -69,9 +69,6 @@ func events(r *Router, h event.Tag, filters ...event.Filter) []event.Event { // Hack to facilitate transition to per-filter tags. for i, f := range filters { switch f := f.(type) { - case key.Filter: - f.Target = h - filters[i] = f case key.FocusFilter: f.Target = h filters[i] = f diff --git a/io/input/router.go b/io/input/router.go index 209ed3c3..300b0f4b 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -33,6 +33,12 @@ type Router struct { } key struct { queue keyQueue + // The following fields have the same purpose as the fields in + // type handler, but for key.Events. + filter keyFilter + nextFilter keyFilter + processedFilter keyFilter + scratchFilter keyFilter } cqueue clipboardQueue // states is the list of pending state changes resulting from @@ -120,8 +126,8 @@ type handler struct { // filter is the union of a set of [io/event.Filters]. type filter struct { - pointer pointerFilter - key keyFilter + pointer pointerFilter + focusable bool } // taggedFilter is a filter for a particular tag. @@ -183,11 +189,13 @@ func (s Source) Event(filters ...event.Filter) (event.Event, bool) { func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { // Merge filters into scratch filters. q.scratchFilters = q.scratchFilters[:0] + q.key.scratchFilter = q.key.scratchFilter[:0] for _, f := range filters { var t event.Tag switch f := f.(type) { case key.Filter: - t = f.Target + q.key.scratchFilter = append(q.key.scratchFilter, f) + continue case transfer.SourceFilter: t = f.Target case transfer.TargetFilter: @@ -210,8 +218,17 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { } if filter == nil { n := len(q.scratchFilters) - q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t}) - filter = &q.scratchFilters[n].filter + if n < cap(q.scratchFilters) { + // Re-use previously allocated filter. + q.scratchFilters = q.scratchFilters[:n+1] + tf := &q.scratchFilters[n] + tf.tag = t + filter = &tf.filter + filter.Reset() + } else { + q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t}) + filter = &q.scratchFilters[n].filter + } } filter.Add(f) } @@ -220,6 +237,8 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { h.filter.Merge(tf.filter) h.nextFilter.Merge(tf.filter) } + q.key.filter = append(q.key.filter, q.key.scratchFilter...) + q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...) // Deliver reset event, if any. for _, f := range filters { switch f := f.(type) { @@ -245,14 +264,24 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { for i := range q.changes { change := &q.changes[i] for j, evt := range change.events { - for _, tf := range q.scratchFilters { - if evt.tag == tf.tag && tf.filter.Matches(evt.event) { - change.events = append(change.events[:j], change.events[j+1:]...) - // Fast forward state to last matched. - q.collapseState(i) - return evt.event, true + match := false + switch e := evt.event.(type) { + case key.Event: + match = q.key.scratchFilter.Matches(change.state.keyState.focus, e) + default: + for _, tf := range q.scratchFilters { + if evt.tag == tf.tag && tf.filter.Matches(evt.event) { + match = true + break + } } } + if match { + change.events = append(change.events[:j], change.events[j+1:]...) + // Fast forward state to last matched. + q.collapseState(i) + return evt.event, true + } } } } @@ -260,6 +289,7 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { h := q.stateFor(tf.tag) h.processedFilter.Merge(tf.filter) } + q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...) return nil, false } @@ -308,6 +338,8 @@ func (q *Router) Frame(frame *op.Ops) { h.pointer.Reset() h.key.Reset() } + q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter + q.key.nextFilter = q.key.nextFilter[:0] var ops *ops.Ops if frame != nil { ops = &frame.Internal @@ -348,21 +380,10 @@ func (q *Router) Queue(events ...event.Event) bool { return matched } -func (f *filter) Reset() { - *f = filter{ - key: keyFilter{ - // Re-use filter slice storage. - filters: f.key.filters[:0], - }, - } -} - func (f *filter) Add(flt event.Filter) { switch flt := flt.(type) { - case key.Filter: - f.key.Add(flt) case key.FocusFilter: - f.key.Add(flt) + f.focusable = true case pointer.Filter: f.pointer.Add(flt) case transfer.SourceFilter, transfer.TargetFilter: @@ -372,12 +393,26 @@ func (f *filter) Add(flt event.Filter) { // Merge f2 into f. func (f *filter) Merge(f2 filter) { - f.key.Merge(f2.key) + f.focusable = f.focusable || f2.focusable f.pointer.Merge(f2.pointer) } func (f *filter) Matches(e event.Event) bool { - return f.key.Matches(e) || f.pointer.Matches(e) + switch e.(type) { + case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: + return f.focusable + default: + return f.pointer.Matches(e) + } +} + +func (f *filter) Reset() { + *f = filter{ + pointer: pointerFilter{ + sourceMimes: f.pointer.sourceMimes[:0], + targetMimes: f.pointer.targetMimes[:0], + }, + } } func (q *Router) processEvent(e event.Event) bool { @@ -388,7 +423,11 @@ func (q *Router) processEvent(e event.Event) bool { state.pointerState = pstate return q.changeState(e, state, evts) case key.Event: - return q.changeState(e, state, q.queueKeyEvent(state.keyState, e)) + var evts []taggedEvent + if q.key.filter.Matches(state.keyState.focus, e) { + evts = append(evts, taggedEvent{event: e}) + } + return q.changeState(e, state, evts) case key.SnippetEvent: // Expand existing, overlapping snippet. if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) { @@ -549,40 +588,6 @@ func rangeNorm(r key.Range) key.Range { return r } -func (q *Router) queueKeyEvent(state keyState, e key.Event) []taggedEvent { - f := state.focus - var evts []taggedEvent - if f != nil && q.handlers[f].filter.key.Matches(e) { - evts = append(evts, taggedEvent{tag: f, event: e}) - return evts - } - pq := &q.pointer.queue - idx := len(pq.hitTree) - 1 - focused := f != nil - if focused { - // If there is a focused tag, traverse its ancestry through the - // hit tree to search for handlers. - for ; pq.hitTree[idx].tag != f; idx-- { - } - } - for idx != -1 { - n := &pq.hitTree[idx] - if focused { - idx = n.next - } else { - idx-- - } - if n.tag == nil { - continue - } - if q.handlers[n.tag].filter.key.Matches(e) { - evts = append(evts, taggedEvent{tag: n.tag, event: e}) - break - } - } - return evts -} - func (q *Router) MoveFocus(dir key.FocusDirection) bool { state := q.lastState() kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir) @@ -795,7 +800,7 @@ func (q *Router) collect() { pc.inputOp(tag, &s.pointer) a := pc.currentArea() b := pc.currentAreaBounds() - if s.filter.key.focusable { + if s.filter.focusable { kq.inputOp(tag, &s.key, t, a, b) } diff --git a/io/key/key.go b/io/key/key.go index 07dd19e4..d1819ba9 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -1,12 +1,6 @@ // SPDX-License-Identifier: Unlicense OR MIT -/* -Package key implements key and text events and operations. - -The InputOp operations is used for declaring key input handlers. Use -an implementation of the Queue interface from package ui to receive -events. -*/ +// Package key implements key and text events and operations. package key import ( @@ -20,7 +14,9 @@ import ( // Filter matches any [Event] that matches the parameters. type Filter struct { - Target event.Tag + // Focus is the tag that must be focused for the filter to match. It has no effect + // if it is nil. + Focus event.Tag // Required is the set of modifiers that must be included in events matched. Required Modifiers // Optional is the set of modifiers that may be included in events matched. diff --git a/widget/button.go b/widget/button.go index 8f7a5327..85233759 100644 --- a/widget/button.go +++ b/widget/button.go @@ -147,17 +147,12 @@ func (b *Clickable) Update(gtx layout.Context) (Click, bool) { }) } } - filters := []event.Filter{ - key.FocusFilter{Target: b}, - } - if b.focused { - filters = append(filters, - key.Filter{Target: b, Name: key.NameReturn}, - key.Filter{Target: b, Name: key.NameSpace}, - ) - } for { - e, ok := gtx.Event(filters...) + e, ok := gtx.Event( + key.FocusFilter{Target: b}, + key.Filter{Focus: b, Name: key.NameReturn}, + key.Filter{Focus: b, Name: key.NameSpace}, + ) if !ok { break } diff --git a/widget/editor.go b/widget/editor.go index ab0d5ac2..d02e35db 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -333,57 +333,47 @@ func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event { return combinedEvents } +func condFilter(pred bool, f key.Filter) event.Filter { + if pred { + return f + } else { + return nil + } +} + func (e *Editor) processKey(gtx layout.Context) { if e.text.Changed() { e.events = append(e.events, ChangeEvent{}) } - filters := []event.Filter{key.FocusFilter{Target: e}, transfer.TargetFilter{Target: e, Type: "application/text"}} - if e.focused { - filters = append(filters, - key.Filter{Target: e, Name: key.NameEnter, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NameReturn, Optional: key.ModShift}, + caret, _ := e.text.Selection() + atBeginning := caret == 0 + atEnd := caret == e.text.Len() + if gtx.Locale.Direction.Progression() != system.FromOrigin { + atEnd, atBeginning = atBeginning, atEnd + } + filters := []event.Filter{ + key.FocusFilter{Target: e}, + transfer.TargetFilter{Target: e, Type: "application/text"}, + key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift}, - key.Filter{Target: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, - key.Filter{Target: e, Name: "C", Required: key.ModShortcut}, - key.Filter{Target: e, Name: "V", Required: key.ModShortcut}, - key.Filter{Target: e, Name: "X", Required: key.ModShortcut}, - key.Filter{Target: e, Name: "A", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, + key.Filter{Focus: e, Name: "C", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "V", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "X", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "A", Required: key.ModShortcut}, - key.Filter{Target: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameHome, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NameEnd, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NamePageUp, Optional: key.ModShift}, - ) - caret, _ := e.text.Selection() - if caret > 0 { - if gtx.Locale.Direction.Progression() == system.FromOrigin { - filters = append(filters, - key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, - ) - } else { - filters = append(filters, - key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, - ) - } - } - if caret < e.text.Len() { - if gtx.Locale.Direction.Progression() == system.FromOrigin { - filters = append(filters, - key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, - ) - } else { - filters = append(filters, - key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, - ) - } - } + key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, + condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}), + condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}), + condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}), + condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}), } // adjust keeps track of runes dropped because of MaxLen. var adjust int diff --git a/widget/editor_test.go b/widget/editor_test.go index ae204b5d..1fd94b1f 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -1177,6 +1177,29 @@ func TestEditor_Submit(t *testing.T) { } } +func TestNoFilterAllocs(t *testing.T) { + b := testing.Benchmark(func(b *testing.B) { + r := new(input.Router) + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{ + Max: image.Pt(100, 100), + }, + Locale: english, + Source: r.Source(), + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.Update(gtx) + } + }) + if allocs := b.AllocsPerOp(); allocs != 0 { + t.Fatalf("expected 0 AllocsPerOp, got %d", allocs) + } +} + // textWidth is a text helper for building simple selection events. // It assumes single-run lines, which isn't safe with non-test text // data. diff --git a/widget/enum.go b/widget/enum.go index ecf4eaf6..ae19e46b 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -64,17 +64,12 @@ func (e *Enum) Update(gtx layout.Context) bool { } } } - filters := []event.Filter{ - key.FocusFilter{Target: &state.tag}, - } - if e.focused && e.focus == state.key { - filters = append(filters, - key.Filter{Target: &state.tag, Name: key.NameReturn}, - key.Filter{Target: &state.tag, Name: key.NameSpace}, - ) - } for { - ev, ok := gtx.Event(filters...) + ev, ok := gtx.Event( + key.FocusFilter{Target: &state.tag}, + key.Filter{Focus: &state.tag, Name: key.NameReturn}, + key.Filter{Focus: &state.tag, Name: key.NameSpace}, + ) if !ok { break } diff --git a/widget/selectable.go b/widget/selectable.go index af3d04c8..149cca8d 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -301,28 +301,23 @@ func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { } func (e *Selectable) processKey(gtx layout.Context) { - filters := []event.Filter{ - key.FocusFilter{Target: e}, - } - if e.focused { - filters = append(filters, - key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, - - key.Filter{Target: e, Name: key.NamePageUp, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NameEnd, Optional: key.ModShift}, - key.Filter{Target: e, Name: key.NameHome, Optional: key.ModShift}, - - key.Filter{Target: e, Name: "C", Required: key.ModShortcut}, - key.Filter{Target: e, Name: "X", Required: key.ModShortcut}, - key.Filter{Target: e, Name: "A", Required: key.ModShortcut}, - ) - } for { - ke, ok := gtx.Event(filters...) + ke, ok := gtx.Event( + key.FocusFilter{Target: e}, + key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + + key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, + + key.Filter{Focus: e, Name: "C", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "X", Required: key.ModShortcut}, + key.Filter{Focus: e, Name: "A", Required: key.ModShortcut}, + ) if !ok { break }