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 }