From c3f2abebcaf0d208e866ee1139c3da3e2aa925c4 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 28 Nov 2023 19:11:22 -0600 Subject: [PATCH] io/input: implement key.Filter.Name special case for matching every key The empty key.Filter.Name now means matching every key name. This is a replacement for the previous special case where the top-level key.InputOp handler would get all unmatched events. Add special case for system events such as focus switch shortcuts. Signed-off-by: Elias Naur --- app/window.go | 54 ++++++++++++++++++++++++------------------- io/input/key.go | 8 +++---- io/input/key_test.go | 24 +++++++++++++++++-- io/input/router.go | 55 +++++++++++++++++++++++++------------------- 4 files changed, 87 insertions(+), 54 deletions(-) diff --git a/app/window.go b/app/window.go index 9b4f9f29..8a03613a 100644 --- a/app/window.go +++ b/app/window.go @@ -558,8 +558,9 @@ func (c *callbacks) SetEditorSnippet(r key.Range) { c.Event(key.SnippetEvent(r)) } -func (w *Window) moveFocus(dir key.FocusDirection, d driver) { - if w.queue.MoveFocus(dir) { +func (w *Window) moveFocus(dir key.FocusDirection) { + w.queue.MoveFocus(dir) + if _, handled := w.queue.WakeupTime(); handled { w.queue.RevealFocus(w.viewport) } else { var v image.Point @@ -881,32 +882,37 @@ func (w *Window) processEvent(d driver, e event.Event) bool { w.out <- e2 case wakeupEvent: case event.Event: - handled := w.queue.Queue(e2) - if e, ok := e.(key.Event); ok && !handled { - if e.State == key.Press { - handled = true - isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" - switch { - case e.Name == key.NameTab && e.Modifiers == 0: - w.moveFocus(key.FocusForward, d) - case e.Name == key.NameTab && e.Modifiers == key.ModShift: - w.moveFocus(key.FocusBackward, d) - case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: - w.moveFocus(key.FocusUp, d) - case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: - w.moveFocus(key.FocusDown, d) - case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: - w.moveFocus(key.FocusLeft, d) - case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile: - w.moveFocus(key.FocusRight, d) - default: - handled = false - } + focusDir := key.FocusDirection(-1) + if e, ok := e2.(key.Event); ok && e.State == key.Press { + isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" + switch { + case e.Name == key.NameTab && e.Modifiers == 0: + focusDir = key.FocusForward + case e.Name == key.NameTab && e.Modifiers == key.ModShift: + focusDir = key.FocusBackward + case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: + focusDir = key.FocusUp + case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: + focusDir = key.FocusDown + case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: + focusDir = key.FocusLeft + case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile: + focusDir = key.FocusRight } } + e := e2 + if focusDir != -1 { + e = input.SystemEvent{Event: e} + } + w.queue.Queue(e) + t, handled := w.queue.WakeupTime() + if focusDir != -1 && !handled { + w.moveFocus(focusDir) + t, handled = w.queue.WakeupTime() + } w.updateCursor(d) if handled { - w.setNextFrame(time.Time{}) + w.setNextFrame(t) w.updateAnimation(d) } return handled diff --git a/io/input/key.go b/io/input/key.go index 8841863b..b5006f31 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -250,20 +250,20 @@ func (q *keyQueue) AreaFor(k *keyHandler) int { return q.dirOrder[order].area } -func (k *keyFilter) Matches(focus event.Tag, e key.Event) bool { +func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool { for _, f := range *k { - if keyFilterMatch(focus, f, e) { + if keyFilterMatch(focus, f, e, system) { return true } } return false } -func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event) bool { +func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool { if f.Focus != nil && f.Focus != focus { return false } - if f.Name != e.Name { + if (f.Name != "" || system) && f.Name != e.Name { return false } if e.Modifiers&f.Required != f.Required { diff --git a/io/input/key_test.go b/io/input/key_test.go index 6f28fa3d..34c5fcc8 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -14,6 +14,24 @@ import ( "gioui.org/op/clip" ) +func TestAllMatchKeyFilter(t *testing.T) { + r := new(Router) + r.Event(key.Filter{}) + ke := key.Event{Name: "A"} + r.Queue(ke) + // Catch-all gets all non-system events. + assertEventSequence(t, events(r, -1, key.Filter{}), ke) + + r = new(Router) + r.Event(key.Filter{Name: "A"}) + r.Queue(SystemEvent{ke}) + if _, handled := r.WakeupTime(); !handled { + t.Errorf("system event was unexpectedly ignored") + } + // Only specific filters match system events. + assertEventSequence(t, events(r, -1, key.Filter{Name: "A"}), ke) +} + func TestInputHint(t *testing.T) { r := new(Router) if hint, changed := r.TextInputHint(); hint != key.HintAny || changed { @@ -66,11 +84,13 @@ func TestInputWakeup(t *testing.T) { t.Errorf("InputOp or the resetting FocusEvent triggered a wakeup") } // And neither does events that don't match anything. - if r.Queue(key.SnippetEvent{}) { + r.Queue(key.SnippetEvent{}) + if _, handled := r.WakeupTime(); handled { t.Errorf("a not-matching event triggered a wakeup") } // However, events that does match should trigger wakeup. - if !r.Queue(key.Event{Name: "A"}) { + r.Queue(key.Event{Name: "A"}) + if _, handled := r.WakeupTime(); !handled { t.Errorf("a key.Event didn't trigger redraw") } } diff --git a/io/input/router.go b/io/input/router.go index b2a27822..6f69fac2 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -107,6 +107,12 @@ const ( // By convention, the zero value denotes the non-existent ID. type SemanticID uint +// SystemEvent is a marker for events that have platform specific +// side-effects. SystemEvents are never matched by catch-all filters. +type SystemEvent struct { + Event event.Event +} + // handler contains the per-handler state tracked by a [Router]. type handler struct { // active tracks whether the handler was active in the current @@ -267,7 +273,7 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { match := false switch e := evt.event.(type) { case key.Event: - match = q.key.scratchFilter.Matches(change.state.keyState.focus, e) + match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false) default: for _, tf := range q.scratchFilters { if evt.tag == tf.tag && tf.filter.Matches(evt.event) { @@ -363,21 +369,17 @@ func (q *Router) Frame(frame *op.Ops) { // Collapse state and events. q.collapseState(len(q.changes) - 1) - - if len(q.changes) > 0 && len(q.changes[0].events) > 0 { - q.wakeup = true - q.wakeupTime = time.Time{} - } } // Queue events and report whether at least one event matched a handler. -func (q *Router) Queue(events ...event.Event) bool { - matched := false +func (q *Router) Queue(events ...event.Event) { for _, e := range events { - hadEvents := q.processEvent(e) - matched = matched || hadEvents + se, system := e.(SystemEvent) + if system { + e = se.Event + } + q.processEvent(e, system) } - return matched } func (f *filter) Add(flt event.Filter) { @@ -415,19 +417,19 @@ func (f *filter) Reset() { } } -func (q *Router) processEvent(e event.Event) bool { +func (q *Router) processEvent(e event.Event, system bool) { state := q.lastState() switch e := e.(type) { case pointer.Event: pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e) state.pointerState = pstate - return q.changeState(e, state, evts) + q.changeState(e, state, evts) case key.Event: var evts []taggedEvent - if q.key.filter.Matches(state.keyState.focus, e) { + if q.key.filter.Matches(state.keyState.focus, e, system) { evts = append(evts, taggedEvent{event: e}) } - return q.changeState(e, state, evts) + q.changeState(e, state, evts) case key.SnippetEvent: // Expand existing, overlapping snippet. if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) { @@ -442,17 +444,17 @@ func (q *Router) processEvent(e event.Event) bool { if f := state.focus; f != nil { evts = append(evts, taggedEvent{tag: f, event: e}) } - return q.changeState(e, state, evts) + q.changeState(e, state, evts) case key.EditEvent, key.FocusEvent, key.SelectionEvent: var evts []taggedEvent if f := state.focus; f != nil { evts = append(evts, taggedEvent{tag: f, event: e}) } - return q.changeState(e, state, evts) + q.changeState(e, state, evts) case transfer.DataEvent: cstate, evts := q.cqueue.Push(state.clipboardState, e) state.clipboardState = cstate - return q.changeState(e, state, evts) + q.changeState(e, state, evts) default: panic("unknown event type") } @@ -541,7 +543,7 @@ func (q *Router) executeCommand(c Command) stateChange { return stateChange{state: state, events: evts} } -func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) bool { +func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) { // Wrap pointer.DataEvent.Open functions to detect them not being called. for i := range evts { e := &evts[i] @@ -571,7 +573,6 @@ func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent prev.state = state prev.events = append(prev.events, evts...) } - return len(evts) > 0 } func rangeOverlaps(r1, r2 key.Range) bool { @@ -588,11 +589,11 @@ func rangeNorm(r key.Range) key.Range { return r } -func (q *Router) MoveFocus(dir key.FocusDirection) bool { +func (q *Router) MoveFocus(dir key.FocusDirection) { state := q.lastState() kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir) state.keyState = kstate - return q.changeState(nil, state, evts) + q.changeState(nil, state, evts) } // RevealFocus scrolls the current focus (if any) into viewport @@ -852,9 +853,13 @@ func (q *Router) collect() { // WakeupTime returns the most recent time for doing another frame, // as determined from the last call to Frame. func (q *Router) WakeupTime() (time.Time, bool) { - w := q.wakeup + t, w := q.wakeupTime, q.wakeup q.wakeup = false - return q.wakeupTime, w + // Pending events always trigger wakeups. + if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 { + t, w = time.Time{}, true + } + return t, w } func (s SemanticGestures) String() string { @@ -864,3 +869,5 @@ func (s SemanticGestures) String() string { } return strings.Join(gestures, ",") } + +func (SystemEvent) ImplementsEvent() {}