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