~eliasnaur/gio

cd3b4561cffc9c5abb8e051bcc2df2310381f120 — Inkeliz 7 months ago e9cd895
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>
5 files changed, 430 insertions(+), 76 deletions(-)

M internal/opconst/ops.go
M io/key/key.go
M io/router/key.go
A io/router/key_test.go
M widget/editor.go
M internal/opconst/ops.go => internal/opconst/ops.go +24 -21
@@ 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,

M io/key/key.go => io/key/key.go +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
}

// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
type SoftKeyboardOp struct {
	Show bool
}

// HideInputOp request that any on screen text input
// be hidden.
type HideInputOp struct{}
// 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()  {}

M io/router/key.go => io/router/key.go +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,
	}
}

A io/router/key_test.go => io/router/key_test.go +311 -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)
	}
}

M widget/editor.go => widget/editor.go +5 -1
@@ 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}