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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-11-28 19:11:22 -06:00
parent 77ff21605c
commit c3f2abebca
4 changed files with 87 additions and 54 deletions
+30 -24
View File
@@ -558,8 +558,9 @@ func (c *callbacks) SetEditorSnippet(r key.Range) {
c.Event(key.SnippetEvent(r)) c.Event(key.SnippetEvent(r))
} }
func (w *Window) moveFocus(dir key.FocusDirection, d driver) { func (w *Window) moveFocus(dir key.FocusDirection) {
if w.queue.MoveFocus(dir) { w.queue.MoveFocus(dir)
if _, handled := w.queue.WakeupTime(); handled {
w.queue.RevealFocus(w.viewport) w.queue.RevealFocus(w.viewport)
} else { } else {
var v image.Point var v image.Point
@@ -881,32 +882,37 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
w.out <- e2 w.out <- e2
case wakeupEvent: case wakeupEvent:
case event.Event: case event.Event:
handled := w.queue.Queue(e2) focusDir := key.FocusDirection(-1)
if e, ok := e.(key.Event); ok && !handled { if e, ok := e2.(key.Event); ok && e.State == key.Press {
if e.State == key.Press { isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android"
handled = true switch {
isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" case e.Name == key.NameTab && e.Modifiers == 0:
switch { focusDir = key.FocusForward
case e.Name == key.NameTab && e.Modifiers == 0: case e.Name == key.NameTab && e.Modifiers == key.ModShift:
w.moveFocus(key.FocusForward, d) focusDir = key.FocusBackward
case e.Name == key.NameTab && e.Modifiers == key.ModShift: case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(key.FocusBackward, d) focusDir = key.FocusUp
case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(key.FocusUp, d) focusDir = key.FocusDown
case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(key.FocusDown, d) focusDir = key.FocusLeft
case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(key.FocusLeft, d) focusDir = key.FocusRight
case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(key.FocusRight, d)
default:
handled = false
}
} }
} }
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) w.updateCursor(d)
if handled { if handled {
w.setNextFrame(time.Time{}) w.setNextFrame(t)
w.updateAnimation(d) w.updateAnimation(d)
} }
return handled return handled
+4 -4
View File
@@ -250,20 +250,20 @@ func (q *keyQueue) AreaFor(k *keyHandler) int {
return q.dirOrder[order].area 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 { for _, f := range *k {
if keyFilterMatch(focus, f, e) { if keyFilterMatch(focus, f, e, system) {
return true return true
} }
} }
return false 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 { if f.Focus != nil && f.Focus != focus {
return false return false
} }
if f.Name != e.Name { if (f.Name != "" || system) && f.Name != e.Name {
return false return false
} }
if e.Modifiers&f.Required != f.Required { if e.Modifiers&f.Required != f.Required {
+22 -2
View File
@@ -14,6 +14,24 @@ import (
"gioui.org/op/clip" "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) { func TestInputHint(t *testing.T) {
r := new(Router) r := new(Router)
if hint, changed := r.TextInputHint(); hint != key.HintAny || changed { 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") t.Errorf("InputOp or the resetting FocusEvent triggered a wakeup")
} }
// And neither does events that don't match anything. // 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") t.Errorf("a not-matching event triggered a wakeup")
} }
// However, events that does match should trigger 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") t.Errorf("a key.Event didn't trigger redraw")
} }
} }
+31 -24
View File
@@ -107,6 +107,12 @@ const (
// By convention, the zero value denotes the non-existent ID. // By convention, the zero value denotes the non-existent ID.
type SemanticID uint 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]. // handler contains the per-handler state tracked by a [Router].
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
@@ -267,7 +273,7 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
match := false match := false
switch e := evt.event.(type) { switch e := evt.event.(type) {
case key.Event: 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: default:
for _, tf := range q.scratchFilters { for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) { 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. // Collapse state and events.
q.collapseState(len(q.changes) - 1) 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. // Queue events and report whether at least one event matched a handler.
func (q *Router) Queue(events ...event.Event) bool { func (q *Router) Queue(events ...event.Event) {
matched := false
for _, e := range events { for _, e := range events {
hadEvents := q.processEvent(e) se, system := e.(SystemEvent)
matched = matched || hadEvents if system {
e = se.Event
}
q.processEvent(e, system)
} }
return matched
} }
func (f *filter) Add(flt event.Filter) { 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() state := q.lastState()
switch e := e.(type) { switch e := e.(type) {
case pointer.Event: case pointer.Event:
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e) pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
state.pointerState = pstate state.pointerState = pstate
return q.changeState(e, state, evts) q.changeState(e, state, evts)
case key.Event: case key.Event:
var evts []taggedEvent 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}) evts = append(evts, taggedEvent{event: e})
} }
return q.changeState(e, state, evts) q.changeState(e, state, evts)
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)) {
@@ -442,17 +444,17 @@ 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.changeState(e, state, evts) 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.changeState(e, state, evts) q.changeState(e, state, evts)
case transfer.DataEvent: case transfer.DataEvent:
cstate, evts := q.cqueue.Push(state.clipboardState, e) cstate, evts := q.cqueue.Push(state.clipboardState, e)
state.clipboardState = cstate state.clipboardState = cstate
return q.changeState(e, state, evts) q.changeState(e, state, evts)
default: default:
panic("unknown event type") panic("unknown event type")
} }
@@ -541,7 +543,7 @@ func (q *Router) executeCommand(c Command) stateChange {
return stateChange{state: state, events: evts} 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. // 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]
@@ -571,7 +573,6 @@ func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent
prev.state = state prev.state = state
prev.events = append(prev.events, evts...) prev.events = append(prev.events, evts...)
} }
return len(evts) > 0
} }
func rangeOverlaps(r1, r2 key.Range) bool { func rangeOverlaps(r1, r2 key.Range) bool {
@@ -588,11 +589,11 @@ func rangeNorm(r key.Range) key.Range {
return r return r
} }
func (q *Router) MoveFocus(dir key.FocusDirection) bool { func (q *Router) MoveFocus(dir key.FocusDirection) {
state := q.lastState() state := q.lastState()
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir) kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
state.keyState = kstate state.keyState = kstate
return q.changeState(nil, state, evts) q.changeState(nil, state, evts)
} }
// RevealFocus scrolls the current focus (if any) into viewport // 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, // WakeupTime returns the most recent time for doing another frame,
// as determined from the last call to Frame. // as determined from the last call to Frame.
func (q *Router) WakeupTime() (time.Time, bool) { func (q *Router) WakeupTime() (time.Time, bool) {
w := q.wakeup t, w := q.wakeupTime, q.wakeup
q.wakeup = false 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 { func (s SemanticGestures) String() string {
@@ -864,3 +869,5 @@ func (s SemanticGestures) String() string {
} }
return strings.Join(gestures, ",") return strings.Join(gestures, ",")
} }
func (SystemEvent) ImplementsEvent() {}