From 73c3849da4629c7e2fae61f0a0ba1fe4984ce54e Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Thu, 19 Oct 2023 18:31:07 -0500 Subject: [PATCH] io/key: [API] introduce FocusFilter for matching focus and editor events Signed-off-by: Elias Naur --- io/input/key.go | 44 ++++++++++------ io/input/key_test.go | 122 +++++++++++++++++++++++++++++-------------- io/input/router.go | 8 +++ io/key/key.go | 5 ++ widget/button.go | 2 +- widget/editor.go | 2 +- widget/enum.go | 2 +- widget/selectable.go | 2 +- 8 files changed, 127 insertions(+), 60 deletions(-) diff --git a/io/input/key.go b/io/input/key.go index 8f248188..2f217053 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -36,13 +36,14 @@ type keyQueue struct { type keyHandler struct { // visible will be true if the InputOp is present // in the current frame. - visible bool - new bool - hint key.InputHint - order int - dirOrder int - filter key.Set - trans f32.Affine2D + visible bool + new bool + focusable bool + hint key.InputHint + order int + dirOrder int + filter key.Set + trans f32.Affine2D } type dirFocusEntry struct { @@ -87,11 +88,7 @@ func (q *keyQueue) InputHint() (key.InputHint, bool) { } func (q *keyQueue) Reset() { - if q.handlers == nil { - q.handlers = make(map[event.Tag]*keyHandler) - } for _, h := range q.handlers { - h.visible, h.new = false, false h.order = -1 h.hint = key.HintAny } @@ -101,17 +98,24 @@ func (q *keyQueue) Reset() { func (q *keyQueue) Frame(events *handlerEvents) { for k, h := range q.handlers { - if !h.visible { - delete(q.handlers, k) + if !h.visible || !h.focusable { if q.focus == k { - // Remove focus from the handler that is no longer visible. + // Remove focus from the handler that is no longer focusable. q.focus = nil q.state = TextInputClose } - } else if h.new && k != q.focus { + if !h.visible && !h.focusable { + delete(q.handlers, k) + continue + } + } + if h.new && k != q.focus { // Reset the handler on (each) first appearance, but don't trigger redraw. events.AddNoRedraw(k, key.FocusEvent{Focus: false}) } + h.new = false + h.visible = false + h.focusable = false } q.updateFocusLayout() } @@ -284,10 +288,18 @@ func (q *keyQueue) softKeyboard(show bool) { } } +func (q *keyQueue) focusable(tag event.Tag) { + h := q.handlerFor(tag) + h.focusable = true +} + func (q *keyQueue) handlerFor(tag event.Tag) *keyHandler { h, ok := q.handlers[tag] if !ok { h = &keyHandler{new: true, order: -1} + if q.handlers == nil { + q.handlers = make(map[event.Tag]*keyHandler) + } q.handlers[tag] = h } return h @@ -300,8 +312,8 @@ func (q *keyQueue) inputOp(op key.InputOp, t f32.Affine2D, area int, bounds imag q.order = append(q.order, op.Tag) q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: op.Tag, area: area, bounds: bounds}) } - h.visible = true h.filter = op.Keys + h.visible = true h.trans = t } diff --git a/io/input/key_test.go b/io/input/key_test.go index 9265ae6a..a40126df 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -22,12 +22,13 @@ func TestKeyWakeup(t *testing.T) { var r Router // Test that merely adding a handler doesn't trigger redraw. + r.Events(handler, key.FocusFilter{}) r.Frame(&ops) if _, wake := r.WakeupTime(); wake { t.Errorf("adding key.InputOp triggered a redraw") } // However, adding a handler queues a Focus(false) event. - if evts := r.Events(handler); len(evts) != 1 { + if evts := r.Events(handler, key.FocusFilter{}); len(evts) != 1 { t.Errorf("no Focus event for newly registered key.InputOp") } } @@ -45,11 +46,15 @@ func TestKeyMultiples(t *testing.T) { // The last one must be focused: key.InputOp{Tag: &handlers[2]}.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) - assertKeyEvent(t, r.Events(&handlers[0]), false) - assertKeyEvent(t, r.Events(&handlers[1]), false) - assertKeyEvent(t, r.Events(&handlers[2]), true) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), true) assertFocus(t, r, &handlers[2]) assertKeyboard(t, r, TextInputOpen) } @@ -68,12 +73,16 @@ func TestKeyStacked(t *testing.T) { r.Source().Queue(key.SoftKeyboardCmd{Show: true}) key.InputOp{Tag: &handlers[3]}.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) - assertKeyEvent(t, r.Events(&handlers[0]), false) - assertKeyEvent(t, r.Events(&handlers[1]), true) - assertKeyEvent(t, r.Events(&handlers[2]), false) - assertKeyEvent(t, r.Events(&handlers[3]), false) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), true) + assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[3], key.FocusFilter{}), false) assertFocus(t, r, &handlers[1]) assertKeyboard(t, r, TextInputOpen) } @@ -105,14 +114,18 @@ func TestKeyRemoveFocus(t *testing.T) { // New InputOp without any focus: key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) // Add some key events: event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}) r.Queue(event) - assertKeyEvent(t, r.Events(&handlers[0]), true, event) - assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), true, event) + assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false) assertFocus(t, r, &handlers[0]) assertKeyboard(t, r, TextInputOpen) @@ -129,7 +142,7 @@ func TestKeyRemoveFocus(t *testing.T) { r.Frame(ops) - assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertFocus(t, r, nil) assertKeyboard(t, r, TextInputClose) @@ -141,8 +154,8 @@ func TestKeyRemoveFocus(t *testing.T) { r.Frame(ops) - assertKeyEventUnexpected(t, r.Events(&handlers[0])) - assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertKeyEventUnexpected(t, r.Events(&handlers[0], key.FocusFilter{})) + assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertFocus(t, r, nil) assertKeyboard(t, r, TextInputClose) @@ -160,7 +173,7 @@ func TestKeyRemoveFocus(t *testing.T) { r.Frame(ops) - assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertFocus(t, r, nil) assertKeyboard(t, r, TextInputClose) } @@ -178,10 +191,14 @@ func TestKeyFocusedInvisible(t *testing.T) { // Set new InputOp without focus: key.InputOp{Tag: &handlers[1]}.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) - assertKeyEvent(t, r.Events(&handlers[0]), true) - assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), true) + assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false) assertFocus(t, r, &handlers[0]) assertKeyboard(t, r, TextInputOpen) @@ -196,11 +213,18 @@ func TestKeyFocusedInvisible(t *testing.T) { r.Frame(ops) - assertKeyEventUnexpected(t, r.Events(&handlers[0])) - assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertKeyEventUnexpected(t, r.Events(&handlers[0], key.FocusFilter{})) + assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertFocus(t, r, nil) assertKeyboard(t, r, TextInputClose) + r.Frame(ops) + + // Unchanged + key.InputOp{Tag: &handlers[1]}.Add(ops) + + r.Frame(ops) + ops.Reset() // Respawn the first element: @@ -210,13 +234,16 @@ func TestKeyFocusedInvisible(t *testing.T) { // Unchanged key.InputOp{Tag: &handlers[1]}.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) - assertKeyEvent(t, r.Events(&handlers[0]), false) - assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1], key.FocusFilter{})) assertFocus(t, r, nil) assertKeyboard(t, r, TextInputClose) - } func TestNoOps(t *testing.T) { @@ -238,6 +265,7 @@ func TestDirectionalFocus(t *testing.T) { cl := clip.Rect(bounds).Push(ops) key.InputOp{Tag: &handlers[i]}.Add(ops) cl.Pop() + r.Events(&handlers[i], key.FocusFilter{}) } r.Frame(ops) @@ -269,11 +297,14 @@ func TestFocusScroll(t *testing.T) { r := new(Router) h := new(int) - f := pointer.Filter{ - Kinds: pointer.Scroll, - ScrollBounds: image.Rect(-100, -100, 100, 100), + filters := []event.Filter{ + key.FocusFilter{}, + pointer.Filter{ + Kinds: pointer.Scroll, + ScrollBounds: image.Rect(-100, -100, 100, 100), + }, } - r.Events(h, f) + r.Events(h, filters...) parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops) cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops) key.InputOp{Tag: h}.Add(ops) @@ -286,7 +317,7 @@ func TestFocusScroll(t *testing.T) { r.MoveFocus(key.FocusLeft) r.RevealFocus(image.Rect(0, 0, 15, 40)) - evts := r.Events(h, f) + evts := r.Events(h, filters...) assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9)) } @@ -295,10 +326,13 @@ func TestFocusClick(t *testing.T) { r := new(Router) h := new(int) - f := pointer.Filter{ - Kinds: pointer.Press | pointer.Release, + filters := []event.Filter{ + key.FocusFilter{}, + pointer.Filter{ + Kinds: pointer.Press | pointer.Release, + }, } - assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(h, filters...), pointer.Cancel) cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops) key.InputOp{Tag: h}.Add(ops) event.InputOp(ops, h) @@ -308,7 +342,7 @@ func TestFocusClick(t *testing.T) { r.MoveFocus(key.FocusLeft) r.ClickFocus() - assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press, pointer.Release) + assertEventPointerTypeSequence(t, r.Events(h, filters...), pointer.Press, pointer.Release) } func TestNoFocus(t *testing.T) { @@ -337,6 +371,10 @@ func TestKeyRouting(t *testing.T) { call := macro.Stop() call.Add(ops) + for i := range handlers { + r.Events(&handlers[i], key.FocusFilter{}) + } + r.Frame(ops) A, B := key.Event{Name: "A"}, key.Event{Name: "B"} @@ -344,14 +382,18 @@ func TestKeyRouting(t *testing.T) { // With no focus, the events should traverse the final branch of the hit tree // searching for handlers. - assertKeyEvent(t, r.Events(&handlers[4]), false, A) - assertKeyEvent(t, r.Events(&handlers[3]), false) - assertKeyEvent(t, r.Events(&handlers[2]), false) - assertKeyEvent(t, r.Events(&handlers[1]), false, B) - assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[4], key.FocusFilter{}), false, A) + assertKeyEvent(t, r.Events(&handlers[3], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false, B) + assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), false) r2 := new(Router) + for i := range handlers { + r2.Events(&handlers[i], key.FocusFilter{}) + } + r2.Source().Queue(key.FocusCmd{Tag: &handlers[3]}) r2.Frame(ops) @@ -359,11 +401,11 @@ func TestKeyRouting(t *testing.T) { // With focus, the events should traverse the branch of the hit tree // containing the focused element. - assertKeyEvent(t, r2.Events(&handlers[4]), false) - assertKeyEvent(t, r2.Events(&handlers[3]), true) - assertKeyEvent(t, r2.Events(&handlers[2]), false) - assertKeyEvent(t, r2.Events(&handlers[1]), false) - assertKeyEvent(t, r2.Events(&handlers[0]), false, A) + assertKeyEvent(t, r2.Events(&handlers[4], key.FocusFilter{}), false) + assertKeyEvent(t, r2.Events(&handlers[3], key.FocusFilter{}), true) + assertKeyEvent(t, r2.Events(&handlers[2], key.FocusFilter{}), false) + assertKeyEvent(t, r2.Events(&handlers[1], key.FocusFilter{}), false) + assertKeyEvent(t, r2.Events(&handlers[0], key.FocusFilter{}), false, A) } func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus bool, expectedInputs ...event.Event) { diff --git a/io/input/router.go b/io/input/router.go index 92515e62..9d242028 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -131,6 +131,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 { for _, f := range filters { switch f := f.(type) { + case key.FocusFilter: + q.key.queue.focusable(k) case pointer.Filter: q.pointer.queue.filterTag(k, f, &q.handlers) case transfer.SourceFilter: @@ -581,6 +583,12 @@ func (h *handlerEvents) Events(k event.Tag, filters ...event.Filter) []event.Eve func filtersMatches(filters []event.Filter, e event.Event) bool { switch e := e.(type) { + case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: + for _, f := range filters { + if _, ok := f.(key.FocusFilter); ok { + return true + } + } case pointer.Event: for _, f := range filters { if f, ok := f.(pointer.Filter); ok && f.Kinds&e.Kind == e.Kind { diff --git a/io/key/key.go b/io/key/key.go index 5512a485..6e812481 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -128,6 +128,9 @@ type EditEvent struct { Text string } +// FocusFilter matches [FocusEvent]s. +type FocusFilter struct{} + // InputHint changes the on-screen-keyboard type. That hints the // type of data that might be entered by the user. type InputHint uint8 @@ -357,6 +360,8 @@ func (SoftKeyboardCmd) ImplementsCommand() {} func (SelectionCmd) ImplementsCommand() {} func (SnippetCmd) ImplementsCommand() {} +func (FocusFilter) ImplementsFilter() {} + func (m Modifiers) String() string { var strs []string if m.Contain(ModCtrl) { diff --git a/widget/button.go b/widget/button.go index 5e97850c..7eb2edbe 100644 --- a/widget/button.go +++ b/widget/button.go @@ -162,7 +162,7 @@ func (b *Clickable) Update(gtx layout.Context) []Click { }) } } - for _, e := range gtx.Events(&b.keyTag) { + for _, e := range gtx.Events(&b.keyTag, key.FocusFilter{}) { switch e := e.(type) { case key.FocusEvent: b.focused = e.Focus diff --git a/widget/editor.go b/widget/editor.go index b4cfedc8..add1db4e 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -332,7 +332,7 @@ func (e *Editor) processKey(gtx layout.Context) { } // adjust keeps track of runes dropped because of MaxLen. var adjust int - for _, ke := range gtx.Events(&e.eventKey, transfer.TargetFilter{Type: "application/text"}) { + for _, ke := range gtx.Events(&e.eventKey, transfer.TargetFilter{Type: "application/text"}, key.FocusFilter{}) { e.blinkStart = gtx.Now switch ke := ke.(type) { case key.FocusEvent: diff --git a/widget/enum.go b/widget/enum.go index d043b46f..375c1270 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -59,7 +59,7 @@ func (e *Enum) Update(gtx layout.Context) bool { } } } - for _, ev := range gtx.Events(&state.tag) { + for _, ev := range gtx.Events(&state.tag, key.FocusFilter{}) { switch ev := ev.(type) { case key.FocusEvent: if ev.Focus { diff --git a/widget/selectable.go b/widget/selectable.go index adb813b3..c7bfa012 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -304,7 +304,7 @@ func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { } func (e *Selectable) processKey(gtx layout.Context) { - for _, ke := range gtx.Events(e) { + for _, ke := range gtx.Events(e, key.FocusFilter{}) { switch ke := ke.(type) { case key.FocusEvent: e.focused = ke.Focus