From cd3b4561cffc9c5abb8e051bcc2df2310381f120 Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Mon, 30 Nov 2020 23:40:20 +0000 Subject: [PATCH] io/key: improve InputOp focus and blur The existing implementation cannot remove the focus of some widget, doesn't have an option to focus without display the on-screen keyboard and it automatically focuses the first InputOp, aggressively. That change aims to make possible: remove focus from any widget. Add focus without displaying the on-screen-keyboard/soft keyboard. Don't automatically focus any widget. Don't recover focus when the widget is visible again. Fixes gio#180. Signed-off-by: Inkeliz --- internal/opconst/ops.go | 45 +++--- io/key/key.go | 36 +++-- io/router/key.go | 108 ++++++++------ io/router/key_test.go | 311 ++++++++++++++++++++++++++++++++++++++++ widget/editor.go | 6 +- 5 files changed, 430 insertions(+), 76 deletions(-) create mode 100644 io/router/key_test.go diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 80a3bc62..dba0f542 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -21,7 +21,8 @@ const ( TypePointerInput TypePass TypeKeyInput - TypeHideInput + TypeKeyFocus + TypeKeySoftKeyboard TypePush TypePop TypeAux @@ -30,25 +31,26 @@ const ( ) const ( - TypeMacroLen = 1 + 4 + 4 - TypeCallLen = 1 + 4 + 4 - TypeTransformLen = 1 + 4*6 - TypeLayerLen = 1 - TypeRedrawLen = 1 + 8 - TypeImageLen = 1 - TypePaintLen = 1 - TypeColorLen = 1 + 4 - TypeLinearGradientLen = 1 + 8*2 + 4*2 - TypeAreaLen = 1 + 1 + 4*4 - TypePointerInputLen = 1 + 1 + 1 - TypePassLen = 1 + 1 - TypeKeyInputLen = 1 + 1 - TypeHideInputLen = 1 - TypePushLen = 1 - TypePopLen = 1 - TypeAuxLen = 1 - TypeClipLen = 1 + 4*4 + 4 + 2 + 4 - TypeProfileLen = 1 + TypeMacroLen = 1 + 4 + 4 + TypeCallLen = 1 + 4 + 4 + TypeTransformLen = 1 + 4*6 + TypeLayerLen = 1 + TypeRedrawLen = 1 + 8 + TypeImageLen = 1 + TypePaintLen = 1 + TypeColorLen = 1 + 4 + TypeLinearGradientLen = 1 + 8*2 + 4*2 + TypeAreaLen = 1 + 1 + 4*4 + TypePointerInputLen = 1 + 1 + 1 + TypePassLen = 1 + 1 + TypeKeyInputLen = 1 + TypeKeyFocusLen = 1 + 1 + TypeKeySoftKeyboardLen = 1 + 1 + TypePushLen = 1 + TypePopLen = 1 + TypeAuxLen = 1 + TypeClipLen = 1 + 4*4 + 4 + 2 + 4 + TypeProfileLen = 1 ) func (t OpType) Size() int { @@ -66,7 +68,8 @@ func (t OpType) Size() int { TypePointerInputLen, TypePassLen, TypeKeyInputLen, - TypeHideInputLen, + TypeKeyFocusLen, + TypeKeySoftKeyboardLen, TypePushLen, TypePopLen, TypeAuxLen, diff --git a/io/key/key.go b/io/key/key.go index 09655b82..f3cda60d 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -20,16 +20,22 @@ import ( // InputOp declares a handler ready for key events. // Key events are in general only delivered to the -// focused key handler. Set the Focus flag to request -// the focus. +// focused key handler. type InputOp struct { - Tag event.Tag - Focus bool + Tag event.Tag } -// HideInputOp request that any on screen text input -// be hidden. -type HideInputOp struct{} +// SoftKeyboardOp shows or hide the on-screen keyboard, if available. +type SoftKeyboardOp struct { + Show bool +} + +// FocusOp sets or clears the keyboard focus. +type FocusOp struct { + // Focus, if set, moves the focus to the current InputOp. If Focus + // is false, the focus is cleared. + Focus bool +} // A FocusEvent is generated when a handler gains or loses // focus. @@ -115,14 +121,22 @@ func (m Modifiers) Contain(m2 Modifiers) bool { func (h InputOp) Add(o *op.Ops) { data := o.Write1(opconst.TypeKeyInputLen, h.Tag) data[0] = byte(opconst.TypeKeyInput) - if h.Focus { +} + +func (h SoftKeyboardOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeKeySoftKeyboardLen) + data[0] = byte(opconst.TypeKeySoftKeyboard) + if h.Show { data[1] = 1 } } -func (h HideInputOp) Add(o *op.Ops) { - data := o.Write(opconst.TypeHideInputLen) - data[0] = byte(opconst.TypeHideInput) +func (h FocusOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeKeyFocusLen) + data[0] = byte(opconst.TypeKeyFocus) + if h.Focus { + data[1] = 1 + } } func (EditEvent) ImplementsEvent() {} diff --git a/io/router/key.go b/io/router/key.go index a64544b2..acfa3982 100644 --- a/io/router/key.go +++ b/io/router/key.go @@ -20,15 +20,18 @@ type keyQueue struct { } type keyHandler struct { - active bool + // visible will be true if the InputOp is present + // in the current frame. + visible bool + new bool } type listenerPriority uint8 const ( - priNone listenerPriority = iota - priDefault + priDefault listenerPriority = iota priCurrentFocus + priNone priNewFocus ) @@ -49,18 +52,27 @@ func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { q.handlers = make(map[event.Tag]*keyHandler) } for _, h := range q.handlers { - h.active = false + h.visible, h.new = false, false } q.reader.Reset(root) - focus, pri, hide := q.resolveFocus(events) + + focus, pri, keyboard := q.resolveFocus(events) + if pri == priNone { + focus = nil + } for k, h := range q.handlers { - if !h.active { + if !h.visible { delete(q.handlers, k) if q.focus == k { + // Remove the focus from the handler that is no longer visible. q.focus = nil - hide = true + keyboard = TextInputClose } } + if h.new && k != focus { + // Reset the handler on (each) first appearance. + events.Add(k, key.FocusEvent{Focus: false}) + } } if focus != q.focus { if q.focus != nil { @@ -70,17 +82,10 @@ func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { if q.focus != nil { events.Add(q.focus, key.FocusEvent{Focus: true}) } else { - hide = true + keyboard = TextInputClose } } - switch { - case pri == priNewFocus: - q.state = TextInputOpen - case hide: - q.state = TextInputClose - default: - q.state = TextInputKeep - } + q.state = keyboard } func (q *keyQueue) Push(e event.Event, events *handlerEvents) { @@ -89,49 +94,49 @@ func (q *keyQueue) Push(e event.Event, events *handlerEvents) { } } -func (q *keyQueue) resolveFocus(events *handlerEvents) (event.Tag, listenerPriority, bool) { - var k event.Tag - var pri listenerPriority - var hide bool +func (q *keyQueue) resolveFocus(events *handlerEvents) (tag event.Tag, pri listenerPriority, keyboard TextInputState) { loop: for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeKeyFocus: + op := decodeFocusOp(encOp.Data, encOp.Refs) + if op.Focus { + pri = priNewFocus + } else { + pri, keyboard = priNone, TextInputClose + } + case opconst.TypeKeySoftKeyboard: + op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs) + if op.Show { + keyboard = TextInputOpen + } else { + keyboard = TextInputClose + } case opconst.TypeKeyInput: op := decodeKeyInputOp(encOp.Data, encOp.Refs) - var newPri listenerPriority - switch { - case op.Focus: - newPri = priNewFocus - case op.Tag == q.focus: - newPri = priCurrentFocus - default: - newPri = priDefault - } - // Switch focus if higher priority or if focus requested. - if newPri.replaces(pri) { - k, pri = op.Tag, newPri + if op.Tag == q.focus && pri < priCurrentFocus { + pri = priCurrentFocus } h, ok := q.handlers[op.Tag] if !ok { - h = new(keyHandler) + h = &keyHandler{new: true} q.handlers[op.Tag] = h - // Reset the handler on (each) first appearance. - events.Add(op.Tag, key.FocusEvent{Focus: false}) } - h.active = true - case opconst.TypeHideInput: - hide = true + h.visible = true + tag = op.Tag case opconst.TypePush: - newK, newPri, h := q.resolveFocus(events) - hide = hide || h + newK, newPri, newKeyboard := q.resolveFocus(events) + if newKeyboard > keyboard { + keyboard = newKeyboard + } if newPri.replaces(pri) { - k, pri = newK, newPri + tag, pri = newK, newPri } case opconst.TypePop: break loop } } - return k, pri, hide + return tag, pri, keyboard } func (p listenerPriority) replaces(p2 listenerPriority) bool { @@ -144,7 +149,24 @@ func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp { panic("invalid op") } return key.InputOp{ - Tag: refs[0].(event.Tag), + Tag: refs[0].(event.Tag), + } +} + +func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp { + if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard { + panic("invalid op") + } + return key.SoftKeyboardOp{ + Show: d[1] != 0, + } +} + +func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp { + if opconst.OpType(d[0]) != opconst.TypeKeyFocus { + panic("invalid op") + } + return key.FocusOp{ Focus: d[1] != 0, } } diff --git a/io/router/key_test.go b/io/router/key_test.go new file mode 100644 index 00000000..b147ec9f --- /dev/null +++ b/io/router/key_test.go @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "reflect" + "testing" + + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/op" +) + +func TestKeyMultiples(t *testing.T) { + handlers := make([]int, 3) + ops := new(op.Ops) + r := new(Router) + + key.SoftKeyboardOp{Show: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Focus: true}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + + // The last one must be focused: + key.InputOp{Tag: &handlers[2]}.Add(ops) + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[2]), true) + assertFocus(t, r, &handlers[2]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyStacked(t *testing.T) { + handlers := make([]int, 4) + ops := new(op.Ops) + r := new(Router) + + s := op.Push(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + // FocusOp must not overwrite the + // FocusOp{Focus: true}. + key.FocusOp{Focus: false}.Add(ops) + s.Pop() + s = op.Push(ops) + key.SoftKeyboardOp{Show: false}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Focus: true}.Add(ops) + s.Pop() + s = op.Push(ops) + key.InputOp{Tag: &handlers[2]}.Add(ops) + // SoftwareKeyboardOp will open the keyboard, + // overwriting `SoftKeyboardOp{Show: false}`. + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Pop() + s = op.Push(ops) + key.SoftKeyboardOp{Show: false}.Add(ops) + key.InputOp{Tag: &handlers[3]}.Add(ops) + // FocusOp must not overwrite the + // FocusOp{Focus: true}. + key.FocusOp{Focus: false}.Add(ops) + s.Pop() + + 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) + assertFocus(t, r, &handlers[1]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeySoftKeyboardNoFocus(t *testing.T) { + ops := new(op.Ops) + r := new(Router) + + // It's possible to open the keyboard + // without any active focus: + key.SoftKeyboardOp{Show: true}.Add(ops) + + r.Frame(ops) + + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyRemoveFocus(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // New InputOp with Focus and Keyboard: + s := op.Push(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Focus: true}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Pop() + + // New InputOp without any focus: + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + r.Frame(ops) + + // Add some key events: + event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}) + r.Add(event) + + assertKeyEvent(t, r.Events(&handlers[0]), true, event) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // Will get the focus removed: + s = op.Push(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Pop() + + // Unchanged: + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + // Removing any Focus: + s = op.Push(ops) + key.FocusOp{Focus: false}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + s = op.Push(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Pop() + + // Setting Focus without InputOp: + s = op.Push(ops) + key.FocusOp{Focus: true}.Add(ops) + s.Pop() + + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + + ops.Reset() + + // Set focus to InputOp which already + // exists in the previous frame: + s = op.Push(ops) + key.FocusOp{Focus: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Pop() + + // Tries to remove focus: + // It must not overwrite the previous `FocusOp`. + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Focus: false}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), true) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyFocusedInvisible(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // Set new InputOp with focus: + s := op.Push(ops) + key.FocusOp{Focus: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Pop() + + // Set new InputOp without focus: + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), true) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // + // Removed first (focused) element! + // + + // Unchanged: + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + // Respawn the first element: + // It must receive one `Event{Focus: false}`. + s = op.Push(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Pop() + + // Unchanged + s = op.Push(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Pop() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + +} + +func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) { + t.Helper() + var evtFocus int + var evtKeyPress int + for _, e := range events { + switch ev := e.(type) { + case key.FocusEvent: + if ev.Focus != expected { + t.Errorf("focus is expected to be %v, got %v", expected, ev.Focus) + } + evtFocus++ + case key.Event, key.EditEvent: + if len(expectedInputs) <= evtKeyPress { + t.Errorf("unexpected key events") + } + if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) { + t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev) + } + evtKeyPress++ + } + } + if evtFocus <= 0 { + t.Errorf("expected focus event") + } + if evtFocus > 1 { + t.Errorf("expected single focus event") + } + if evtKeyPress != len(expectedInputs) { + t.Errorf("expected key events") + } +} + +func assertKeyEventUnexpected(t *testing.T, events []event.Event) { + t.Helper() + var evtFocus int + for _, e := range events { + switch e.(type) { + case key.FocusEvent: + evtFocus++ + } + } + if evtFocus > 1 { + t.Errorf("unexpected focus event") + } +} + +func assertFocus(t *testing.T, router *Router, expected event.Tag) { + t.Helper() + if router.kqueue.focus != expected { + t.Errorf("expected %v to be focused, got %v", expected, router.kqueue.focus) + } +} + +func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { + t.Helper() + if router.kqueue.state != expected { + t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state) + } +} diff --git a/widget/editor.go b/widget/editor.go index 7d925b99..f2743201 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -399,7 +399,11 @@ func (e *Editor) layout(gtx layout.Context) layout.Dimensions { e.shapes = append(e.shapes, line{off, path}) } - key.InputOp{Tag: &e.eventKey, Focus: e.requestFocus}.Add(gtx.Ops) + key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops) + if e.requestFocus { + key.FocusOp{Focus: true}.Add(gtx.Ops) + key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) + } e.requestFocus = false pointerPadding := gtx.Px(unit.Dp(4)) r := image.Rectangle{Max: e.viewSize}