From be36fc88aa249fa20060c521f7173e4e3b6867e2 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 9 Oct 2023 13:35:25 -0500 Subject: [PATCH] io/input,io/key: [API] introduce Command, replace FocusOp with FocusCmd Modeling focus change as an operation is awkward, because focus changes logically happen during event processing, not layout. In particular, you want to apply focus changes even if a widget is subsequently never laid out. Now that input.Source is concrete, it's much more straightforward to offer focus changes as a command which can be queued through the Source. A future change may similarly offer a command for directional focus changes. Signed-off-by: Elias Naur --- internal/ops/ops.go | 5 ---- io/input/key.go | 67 +++++++++++++++++--------------------------- io/input/key_test.go | 21 +++++++------- io/input/router.go | 58 +++++++++++++++++++++++++++----------- io/key/key.go | 22 ++++++--------- widget/button.go | 4 +-- widget/editor.go | 2 +- widget/enum.go | 2 +- widget/selectable.go | 2 +- 9 files changed, 90 insertions(+), 93 deletions(-) diff --git a/internal/ops/ops.go b/internal/ops/ops.go index fbc2f44c..27a51044 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -69,7 +69,6 @@ const ( TypeTarget TypeOffer TypeKeyInput - TypeKeyFocus TypeKeySoftKeyboard TypeSave TypeLoad @@ -154,7 +153,6 @@ const ( TypeTargetLen = 1 TypeOfferLen = 1 TypeKeyInputLen = 1 + 1 - TypeKeyFocusLen = 1 + 1 TypeKeySoftKeyboardLen = 1 + 1 TypeSaveLen = 1 + 4 TypeLoadLen = 1 + 4 @@ -437,7 +435,6 @@ var opProps = [0x100]opProp{ TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, TypeOffer: {Size: TypeOfferLen, NumRefs: 3}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, - TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1}, TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0}, TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, @@ -514,8 +511,6 @@ func (t OpType) String() string { return "Offer" case TypeKeyInput: return "KeyInput" - case TypeKeyFocus: - return "KeyFocus" case TypeKeySoftKeyboard: return "KeySoftKeyboard" case TypeSave: diff --git a/io/input/key.go b/io/input/key.go index 2daebeaf..1fae2cd7 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -44,14 +44,6 @@ type keyHandler struct { filter key.Set } -// keyCollector tracks state required to update a keyQueue -// from key ops. -type keyCollector struct { - q *keyQueue - focus event.Tag - changed bool -} - type dirFocusEntry struct { tag event.Tag row int @@ -99,8 +91,7 @@ func (q *keyQueue) Reset() { q.dirOrder = q.dirOrder[:0] } -func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) { - changed, focus := collector.changed, collector.focus +func (q *keyQueue) Frame(events *handlerEvents) { for k, h := range q.handlers { if !h.visible { delete(q.handlers, k) @@ -109,14 +100,11 @@ func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) { q.focus = nil q.state = TextInputClose } - } else if h.new && k != focus { + } else 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}) } } - if changed { - q.setFocus(focus, events) - } q.updateFocusLayout() } @@ -187,7 +175,7 @@ func (q *keyQueue) MoveFocus(dir key.FocusDirection, events *handlerEvents) bool } } order = (order + len(q.order)) % len(q.order) - q.setFocus(q.order[order], events) + q.Focus(q.order[order], events) return true case key.FocusRight, key.FocusLeft: next := order @@ -200,7 +188,7 @@ func (q *keyQueue) MoveFocus(dir key.FocusDirection, events *handlerEvents) bool if 0 <= next && next < len(q.dirOrder) { newFocus := q.dirOrder[next] if newFocus.row == focus.row { - q.setFocus(newFocus.tag, events) + q.Focus(newFocus.tag, events) return true } } @@ -237,7 +225,7 @@ func (q *keyQueue) MoveFocus(dir key.FocusDirection, events *handlerEvents) bool order += delta } if closest != nil { - q.setFocus(closest, events) + q.Focus(closest, events) return true } } @@ -258,7 +246,7 @@ func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool { return q.handlers[t].filter.Contains(e.Name, e.Modifiers) } -func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) { +func (q *keyQueue) Focus(focus event.Tag, events *handlerEvents) { if focus != nil { if _, exists := q.handlers[focus]; !exists { focus = nil @@ -280,51 +268,46 @@ func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) { } } -func (k *keyCollector) focusOp(tag event.Tag) { - k.focus = tag - k.changed = true -} - -func (k *keyCollector) softKeyboard(show bool) { +func (q *keyQueue) softKeyboard(show bool) { if show { - k.q.state = TextInputOpen + q.state = TextInputOpen } else { - k.q.state = TextInputClose + q.state = TextInputClose } } -func (k *keyCollector) handlerFor(tag event.Tag, area int, bounds image.Rectangle) *keyHandler { - h, ok := k.q.handlers[tag] +func (q *keyQueue) handlerFor(tag event.Tag, area int, bounds image.Rectangle) *keyHandler { + h, ok := q.handlers[tag] if !ok { h = &keyHandler{new: true, order: -1} - k.q.handlers[tag] = h + q.handlers[tag] = h } if h.order == -1 { - h.order = len(k.q.order) - k.q.order = append(k.q.order, tag) - k.q.dirOrder = append(k.q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds}) + h.order = len(q.order) + q.order = append(q.order, tag) + q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds}) } return h } -func (k *keyCollector) inputOp(op key.InputOp, area int, bounds image.Rectangle) { - h := k.handlerFor(op.Tag, area, bounds) +func (q *keyQueue) inputOp(op key.InputOp, area int, bounds image.Rectangle) { + h := q.handlerFor(op.Tag, area, bounds) h.visible = true h.hint = op.Hint h.filter = op.Keys } -func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) { - if op.Tag == k.q.focus { - k.q.content.Selection.Range = op.Range - k.q.content.Selection.Caret = op.Caret - k.q.content.Selection.Transform = t +func (q *keyQueue) selectionOp(t f32.Affine2D, op key.SelectionOp) { + if op.Tag == q.focus { + q.content.Selection.Range = op.Range + q.content.Selection.Caret = op.Caret + q.content.Selection.Transform = t } } -func (k *keyCollector) snippetOp(op key.SnippetOp) { - if op.Tag == k.q.focus { - k.q.content.Snippet = op.Snippet +func (q *keyQueue) snippetOp(op key.SnippetOp) { + if op.Tag == q.focus { + q.content.Snippet = op.Snippet } } diff --git a/io/input/key_test.go b/io/input/key_test.go index 2885ee2c..3313033c 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -39,7 +39,7 @@ func TestKeyMultiples(t *testing.T) { key.SoftKeyboardOp{Show: true}.Add(ops) key.InputOp{Tag: &handlers[0]}.Add(ops) - key.FocusOp{Tag: &handlers[2]}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: &handlers[2]}) key.InputOp{Tag: &handlers[1]}.Add(ops) // The last one must be focused: @@ -60,10 +60,10 @@ func TestKeyStacked(t *testing.T) { r := new(Router) key.InputOp{Tag: &handlers[0]}.Add(ops) - key.FocusOp{Tag: nil}.Add(ops) + r.Source().Queue(key.FocusCmd{}) key.SoftKeyboardOp{Show: false}.Add(ops) key.InputOp{Tag: &handlers[1]}.Add(ops) - key.FocusOp{Tag: &handlers[1]}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: &handlers[1]}) key.InputOp{Tag: &handlers[2]}.Add(ops) key.SoftKeyboardOp{Show: true}.Add(ops) key.InputOp{Tag: &handlers[3]}.Add(ops) @@ -99,7 +99,7 @@ func TestKeyRemoveFocus(t *testing.T) { // New InputOp with Focus and Keyboard: key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops) - key.FocusOp{Tag: &handlers[0]}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) key.SoftKeyboardOp{Show: true}.Add(ops) // New InputOp without any focus: @@ -125,7 +125,7 @@ func TestKeyRemoveFocus(t *testing.T) { key.InputOp{Tag: &handlers[1]}.Add(ops) // Remove focus by focusing on a tag that don't exist. - key.FocusOp{Tag: new(int)}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: new(int)}) r.Frame(ops) @@ -150,19 +150,19 @@ func TestKeyRemoveFocus(t *testing.T) { // Set focus to InputOp which already // exists in the previous frame: - key.FocusOp{Tag: &handlers[0]}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) key.InputOp{Tag: &handlers[0]}.Add(ops) key.SoftKeyboardOp{Show: true}.Add(ops) // Remove focus. key.InputOp{Tag: &handlers[1]}.Add(ops) - key.FocusOp{Tag: nil}.Add(ops) + r.Source().Queue(key.FocusCmd{}) r.Frame(ops) assertKeyEventUnexpected(t, r.Events(&handlers[1])) assertFocus(t, r, nil) - assertKeyboard(t, r, TextInputOpen) + assertKeyboard(t, r, TextInputClose) } func TestKeyFocusedInvisible(t *testing.T) { @@ -171,7 +171,7 @@ func TestKeyFocusedInvisible(t *testing.T) { r := new(Router) // Set new InputOp with focus: - key.FocusOp{Tag: &handlers[0]}.Add(ops) + r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) key.InputOp{Tag: &handlers[0]}.Add(ops) key.SoftKeyboardOp{Show: true}.Add(ops) @@ -351,8 +351,7 @@ func TestKeyRouting(t *testing.T) { r2 := new(Router) - call.Add(ops) - key.FocusOp{Tag: &handlers[3]}.Add(ops) + r2.Source().Queue(key.FocusCmd{Tag: &handlers[3]}) r2.Frame(ops) r2.Queue(A, B) diff --git a/io/input/router.go b/io/input/router.go index dacb4c10..24c8e470 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -33,8 +33,7 @@ type Router struct { collector pointerCollector } key struct { - queue keyQueue - collector keyCollector + queue keyQueue } cqueue clipboardQueue @@ -45,6 +44,9 @@ type Router struct { // InvalidateOp summary. wakeup bool wakeupTime time.Time + + // Changes queued for next call to Frame. + commands []Command } // Source implements the interface between a Router and user interface widgets. @@ -53,6 +55,12 @@ type Source struct { r *Router } +// Command represents a request such as moving the focus, or initiating a clipboard read. +// Commands are queued by calling [Source.Queue]. +type Command interface { + ImplementsCommand() +} + // SemanticNode represents a node in the tree describing the components // contained in a frame. type SemanticNode struct { @@ -98,6 +106,15 @@ func (q *Router) Source() Source { return Source{r: q} } +// Queue a command to be executed after the current frame +// has completed. +func (s Source) Queue(c Command) { + if !s.Enabled() { + return + } + s.r.queue(c) +} + // Enabled reports whether the source is enabled. Only enabled // Sources deliver events and respond to commands. func (s Source) Enabled() bool { @@ -129,9 +146,10 @@ func (q *Router) Frame(frame *op.Ops) { } q.reader.Reset(ops) q.collect() - + q.executeCommands() q.pointer.queue.Frame(&q.handlers) - q.key.queue.Frame(&q.handlers, q.key.collector) + q.key.queue.Frame(&q.handlers) + if q.handlers.HadEvents() { q.wakeup = true q.wakeupTime = time.Time{} @@ -189,6 +207,20 @@ func (q *Router) Queue(events ...event.Event) bool { return q.handlers.HadEvents() } +func (q *Router) queue(f Command) { + q.commands = append(q.commands, f) +} + +func (q *Router) executeCommands() { + for _, req := range q.commands { + switch req := req.(type) { + case key.FocusCmd: + q.key.queue.Focus(req.Tag, &q.handlers) + } + } + q.commands = nil +} + func rangeOverlaps(r1, r2 key.Range) bool { r1 = rangeNorm(r1) r2 = rangeNorm(r2) @@ -377,8 +409,7 @@ func (q *Router) collect() { pc := &q.pointer.collector pc.q = &q.pointer.queue pc.reset() - kc := &q.key.collector - *kc = keyCollector{q: &q.key.queue} + kq := &q.key.queue q.key.queue.Reset() var t f32.Affine2D bo := binary.LittleEndian @@ -473,18 +504,11 @@ func (q *Router) collect() { act := system.Action(encOp.Data[1]) pc.actionInputOp(act) - // Key ops. - case ops.TypeKeyFocus: - tag, _ := encOp.Refs[0].(event.Tag) - op := key.FocusOp{ - Tag: tag, - } - kc.focusOp(op.Tag) case ops.TypeKeySoftKeyboard: op := key.SoftKeyboardOp{ Show: encOp.Data[1] != 0, } - kc.softKeyboard(op.Show) + kq.softKeyboard(op.Show) case ops.TypeKeyInput: filter := key.Set(*encOp.Refs[1].(*string)) op := key.InputOp{ @@ -495,7 +519,7 @@ func (q *Router) collect() { a := pc.currentArea() b := pc.currentAreaBounds() pc.keyInputOp(op) - kc.inputOp(op, a, b) + kq.inputOp(op, a, b) case ops.TypeSnippet: op := key.SnippetOp{ Tag: encOp.Refs[0].(event.Tag), @@ -507,7 +531,7 @@ func (q *Router) collect() { Text: *(encOp.Refs[1].(*string)), }, } - kc.snippetOp(op) + kq.snippetOp(op) case ops.TypeSelection: op := key.SelectionOp{ Tag: encOp.Refs[0].(event.Tag), @@ -524,7 +548,7 @@ func (q *Router) collect() { Descent: math.Float32frombits(bo.Uint32(encOp.Data[21:])), }, } - kc.selectionOp(t, op) + kq.selectionOp(t, op) // Semantic ops. case ops.TypeSemanticLabel: diff --git a/io/key/key.go b/io/key/key.go index f02893aa..26704e84 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -56,14 +56,6 @@ type SoftKeyboardOp struct { Show bool } -// FocusOp sets or clears the keyboard focus. It replaces any previous -// FocusOp in the same frame. -type FocusOp struct { - // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag - // has no InputOp in the same frame. - Tag event.Tag -} - // SelectionOp updates the selection for an input handler. type SelectionOp struct { Tag event.Tag @@ -242,6 +234,13 @@ func (m Modifiers) Contain(m2 Modifiers) bool { return m&m2 == m2 } +// FocusCmd requests to set or clear the keyboard focus. +type FocusCmd struct { + // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag + // has no InputOp in the same frame. + Tag event.Tag +} + func (k Set) Contains(name string, mods Modifiers) bool { ks := string(k) for len(ks) > 0 { @@ -346,11 +345,6 @@ func (h SoftKeyboardOp) Add(o *op.Ops) { } } -func (h FocusOp) Add(o *op.Ops) { - data := ops.Write1(&o.Internal, ops.TypeKeyFocusLen, h.Tag) - data[0] = byte(ops.TypeKeyFocus) -} - func (s SnippetOp) Add(o *op.Ops) { data := ops.Write2String(&o.Internal, ops.TypeSnippetLen, s.Tag, s.Text) data[0] = byte(ops.TypeSnippet) @@ -377,6 +371,8 @@ func (FocusEvent) ImplementsEvent() {} func (SnippetEvent) ImplementsEvent() {} func (SelectionEvent) ImplementsEvent() {} +func (FocusCmd) ImplementsCommand() {} + func (m Modifiers) String() string { var strs []string if m.Contain(ModCtrl) { diff --git a/widget/button.go b/widget/button.go index 1f956bc5..90070dd0 100644 --- a/widget/button.go +++ b/widget/button.go @@ -122,7 +122,7 @@ func (b *Clickable) Update(gtx layout.Context) []Click { b.focused = false } if b.requestFocus { - key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops) + gtx.Queue(key.FocusCmd{Tag: &b.keyTag}) b.requestFocus = false } for len(b.history) > 0 { @@ -159,7 +159,7 @@ func (b *Clickable) Update(gtx layout.Context) []Click { } case gesture.KindPress: if e.Source == pointer.Mouse { - key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops) + gtx.Queue(key.FocusCmd{Tag: &b.keyTag}) } b.history = append(b.history, Press{ Position: e.Position, diff --git a/widget/editor.go b/widget/editor.go index 6937d3e3..056cfc58 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -645,7 +645,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call } key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keys}.Add(gtx.Ops) if e.requestFocus { - key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops) + gtx.Queue(key.FocusCmd{Tag: &e.eventKey}) key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) } e.requestFocus = false diff --git a/widget/enum.go b/widget/enum.go index fa4c6fce..d043b46f 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -50,7 +50,7 @@ func (e *Enum) Update(gtx layout.Context) bool { switch ev.Kind { case gesture.KindPress: if ev.Source == pointer.Mouse { - key.FocusOp{Tag: &state.tag}.Add(gtx.Ops) + gtx.Queue(key.FocusCmd{Tag: &state.tag}) } case gesture.KindClick: if state.key != e.Value { diff --git a/widget/selectable.go b/widget/selectable.go index bff04a42..10d8ab22 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -210,7 +210,7 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, } key.InputOp{Tag: l, Keys: keys}.Add(gtx.Ops) if l.requestFocus { - key.FocusOp{Tag: l}.Add(gtx.Ops) + gtx.Queue(key.FocusCmd{Tag: l}) key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) } l.requestFocus = false