mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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 <inkeliz@inkeliz.com>
This commit is contained in:
+25
-11
@@ -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() {}
|
||||
|
||||
+65
-43
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user