mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
496fc3cc82
If the client asks for the focus to be set to a tag, allow it. There is a check at the end of Router.Frame that clears the focus if the tag turns out to fail the requirements (visible and has asked for FocusEvents). The change simplifies the logic for determining whether a command can be executed immediately. Signed-off-by: Elias Naur <mail@eliasnaur.com>
477 lines
11 KiB
Go
477 lines
11 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package input
|
|
|
|
import (
|
|
"image"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
)
|
|
|
|
func TestInputWakeup(t *testing.T) {
|
|
handler := new(int)
|
|
var ops op.Ops
|
|
event.InputOp(&ops, handler)
|
|
|
|
var r Router
|
|
// Test that merely adding a handler doesn't trigger redraw.
|
|
evts := events(&r, handler, key.FocusFilter{})
|
|
r.Frame(&ops)
|
|
if _, wake := r.WakeupTime(); wake {
|
|
t.Errorf("adding key.InputOp triggered a redraw")
|
|
}
|
|
if len(evts) != 1 {
|
|
t.Errorf("no Focus event for newly registered key.InputOp")
|
|
}
|
|
}
|
|
|
|
func TestKeyMultiples(t *testing.T) {
|
|
handlers := make([]int, 3)
|
|
ops := new(op.Ops)
|
|
r := new(Router)
|
|
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
event.InputOp(ops, &handlers[0])
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
// The last one must be focused:
|
|
event.InputOp(ops, &handlers[2])
|
|
|
|
for i := range handlers {
|
|
assertKeyEvent(t, events(r, &handlers[i], key.FocusFilter{}), false)
|
|
}
|
|
|
|
r.Frame(ops)
|
|
|
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[2]})
|
|
assertKeyEvent(t, events(r, &handlers[2], key.FocusFilter{}), 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)
|
|
|
|
for i := range handlers {
|
|
assertKeyEvent(t, events(r, &handlers[i], key.FocusFilter{}), false)
|
|
}
|
|
|
|
event.InputOp(ops, &handlers[0])
|
|
r.Source().Execute(key.FocusCmd{})
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: false})
|
|
event.InputOp(ops, &handlers[1])
|
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[1]})
|
|
event.InputOp(ops, &handlers[2])
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
event.InputOp(ops, &handlers[3])
|
|
|
|
r.Frame(ops)
|
|
|
|
assertKeyEvent(t, events(r, &handlers[1], key.FocusFilter{}), true)
|
|
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:
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
|
|
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)
|
|
|
|
filters := []event.Filter{
|
|
key.FocusFilter{},
|
|
key.Filter{Name: key.NameTab, Required: key.ModShortcut},
|
|
}
|
|
for i := range handlers {
|
|
assertKeyEvent(t, events(r, &handlers[i], filters...), false)
|
|
}
|
|
// New InputOp with Focus and Keyboard:
|
|
event.InputOp(ops, &handlers[0])
|
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
|
|
// New InputOp without any focus:
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
r.Frame(ops)
|
|
|
|
// Add some key events:
|
|
evt := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press})
|
|
r.Queue(evt)
|
|
|
|
assertKeyEvent(t, events(r, &handlers[0], filters...), true, evt)
|
|
assertFocus(t, r, &handlers[0])
|
|
assertKeyboard(t, r, TextInputOpen)
|
|
|
|
ops.Reset()
|
|
|
|
// Will get the focus removed:
|
|
event.InputOp(ops, &handlers[0])
|
|
|
|
// Unchanged:
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
// Remove focus by focusing on a tag that don't exist.
|
|
r.Source().Execute(key.FocusCmd{Tag: new(int)})
|
|
|
|
r.Frame(ops)
|
|
|
|
assertKeyEventUnexpected(t, events(r, &handlers[1], key.FocusFilter{}))
|
|
assertFocus(t, r, nil)
|
|
assertKeyboard(t, r, TextInputClose)
|
|
|
|
ops.Reset()
|
|
|
|
event.InputOp(ops, &handlers[0])
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
assertKeyEventUnexpected(t, events(r, &handlers[0], key.FocusFilter{}))
|
|
assertKeyEventUnexpected(t, events(r, &handlers[1], key.FocusFilter{}))
|
|
assertFocus(t, r, nil)
|
|
assertKeyboard(t, r, TextInputClose)
|
|
|
|
r.Frame(ops)
|
|
ops.Reset()
|
|
|
|
// Set focus to InputOp which already
|
|
// exists in the previous frame:
|
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
|
event.InputOp(ops, &handlers[0])
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
assertFocus(t, r, &handlers[0])
|
|
|
|
ops.Reset()
|
|
|
|
// Remove focus.
|
|
event.InputOp(ops, &handlers[1])
|
|
r.Source().Execute(key.FocusCmd{})
|
|
|
|
assertKeyEventUnexpected(t, events(r, &handlers[1], key.FocusFilter{}))
|
|
assertFocus(t, r, nil)
|
|
assertKeyboard(t, r, TextInputClose)
|
|
}
|
|
|
|
func TestKeyFocusedInvisible(t *testing.T) {
|
|
handlers := make([]int, 2)
|
|
ops := new(op.Ops)
|
|
r := new(Router)
|
|
|
|
for i := range handlers {
|
|
assertKeyEvent(t, events(r, &handlers[i], key.FocusFilter{}), false)
|
|
}
|
|
|
|
// Set new InputOp with focus:
|
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
|
event.InputOp(ops, &handlers[0])
|
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
|
|
|
// Set new InputOp without focus:
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
r.Frame(ops)
|
|
|
|
assertKeyEvent(t, events(r, &handlers[0], key.FocusFilter{}), true)
|
|
assertFocus(t, r, &handlers[0])
|
|
assertKeyboard(t, r, TextInputOpen)
|
|
|
|
ops.Reset()
|
|
|
|
//
|
|
// Removed first (focused) element!
|
|
//
|
|
|
|
// Unchanged:
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
r.Frame(ops)
|
|
|
|
assertKeyEventUnexpected(t, events(r, &handlers[0], key.FocusFilter{}))
|
|
assertKeyEventUnexpected(t, events(r, &handlers[1], key.FocusFilter{}))
|
|
assertFocus(t, r, nil)
|
|
assertKeyboard(t, r, TextInputClose)
|
|
|
|
r.Frame(ops)
|
|
|
|
// Unchanged
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
r.Frame(ops)
|
|
|
|
ops.Reset()
|
|
|
|
// Respawn the first element:
|
|
// It must receive one `Event{Focus: false}`.
|
|
event.InputOp(ops, &handlers[0])
|
|
|
|
// Unchanged
|
|
event.InputOp(ops, &handlers[1])
|
|
|
|
for i := range handlers {
|
|
assertKeyEventUnexpected(t, events(r, &handlers[i], key.FocusFilter{}))
|
|
}
|
|
|
|
r.Frame(ops)
|
|
|
|
assertKeyEventUnexpected(t, events(r, &handlers[1], key.FocusFilter{}))
|
|
assertFocus(t, r, nil)
|
|
assertKeyboard(t, r, TextInputClose)
|
|
}
|
|
|
|
func TestNoOps(t *testing.T) {
|
|
r := new(Router)
|
|
r.Frame(nil)
|
|
}
|
|
|
|
func TestDirectionalFocus(t *testing.T) {
|
|
ops := new(op.Ops)
|
|
r := new(Router)
|
|
handlers := []image.Rectangle{
|
|
image.Rect(10, 10, 50, 50),
|
|
image.Rect(50, 20, 100, 80),
|
|
image.Rect(20, 26, 60, 80),
|
|
image.Rect(10, 60, 50, 100),
|
|
}
|
|
|
|
for i, bounds := range handlers {
|
|
cl := clip.Rect(bounds).Push(ops)
|
|
event.InputOp(ops, &handlers[i])
|
|
cl.Pop()
|
|
events(r, &handlers[i], key.FocusFilter{})
|
|
}
|
|
r.Frame(ops)
|
|
|
|
r.MoveFocus(key.FocusLeft)
|
|
assertFocus(t, r, &handlers[0])
|
|
r.MoveFocus(key.FocusLeft)
|
|
assertFocus(t, r, &handlers[0])
|
|
r.MoveFocus(key.FocusRight)
|
|
assertFocus(t, r, &handlers[1])
|
|
r.MoveFocus(key.FocusRight)
|
|
assertFocus(t, r, &handlers[1])
|
|
r.MoveFocus(key.FocusDown)
|
|
assertFocus(t, r, &handlers[2])
|
|
r.MoveFocus(key.FocusDown)
|
|
assertFocus(t, r, &handlers[2])
|
|
r.MoveFocus(key.FocusLeft)
|
|
assertFocus(t, r, &handlers[3])
|
|
r.MoveFocus(key.FocusUp)
|
|
assertFocus(t, r, &handlers[0])
|
|
|
|
r.MoveFocus(key.FocusForward)
|
|
assertFocus(t, r, &handlers[1])
|
|
r.MoveFocus(key.FocusBackward)
|
|
assertFocus(t, r, &handlers[0])
|
|
}
|
|
|
|
func TestFocusScroll(t *testing.T) {
|
|
ops := new(op.Ops)
|
|
r := new(Router)
|
|
h := new(int)
|
|
|
|
filters := []event.Filter{
|
|
key.FocusFilter{},
|
|
pointer.Filter{
|
|
Kinds: pointer.Scroll,
|
|
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
|
},
|
|
}
|
|
events(r, h, filters...)
|
|
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
|
|
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
|
|
event.InputOp(ops, h)
|
|
// Test that h is scrolled even if behind another handler.
|
|
event.InputOp(ops, new(int))
|
|
cl.Pop()
|
|
parent.Pop()
|
|
r.Frame(ops)
|
|
|
|
r.MoveFocus(key.FocusLeft)
|
|
r.RevealFocus(image.Rect(0, 0, 15, 40))
|
|
evts := events(r, h, filters...)
|
|
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
|
|
}
|
|
|
|
func TestFocusClick(t *testing.T) {
|
|
ops := new(op.Ops)
|
|
r := new(Router)
|
|
h := new(int)
|
|
|
|
filters := []event.Filter{
|
|
key.FocusFilter{},
|
|
pointer.Filter{
|
|
Kinds: pointer.Press | pointer.Release,
|
|
},
|
|
}
|
|
assertEventPointerTypeSequence(t, events(r, h, filters...), pointer.Cancel)
|
|
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
|
|
event.InputOp(ops, h)
|
|
cl.Pop()
|
|
r.Frame(ops)
|
|
|
|
r.MoveFocus(key.FocusLeft)
|
|
r.ClickFocus()
|
|
|
|
assertEventPointerTypeSequence(t, events(r, h, filters...), pointer.Press, pointer.Release)
|
|
}
|
|
|
|
func TestNoFocus(t *testing.T) {
|
|
r := new(Router)
|
|
r.MoveFocus(key.FocusForward)
|
|
}
|
|
|
|
func TestKeyRouting(t *testing.T) {
|
|
handlers := make([]int, 5)
|
|
ops := new(op.Ops)
|
|
macroOps := new(op.Ops)
|
|
r := new(Router)
|
|
|
|
rect := clip.Rect{Max: image.Pt(10, 10)}
|
|
|
|
macro := op.Record(macroOps)
|
|
event.InputOp(ops, &handlers[0])
|
|
cl1 := rect.Push(ops)
|
|
event.InputOp(ops, &handlers[1])
|
|
event.InputOp(ops, &handlers[2])
|
|
cl1.Pop()
|
|
cl2 := rect.Push(ops)
|
|
event.InputOp(ops, &handlers[3])
|
|
event.InputOp(ops, &handlers[4])
|
|
cl2.Pop()
|
|
call := macro.Stop()
|
|
call.Add(ops)
|
|
|
|
fa := []event.Filter{
|
|
key.FocusFilter{},
|
|
key.Filter{Name: "A"},
|
|
}
|
|
fb := []event.Filter{
|
|
key.FocusFilter{},
|
|
key.Filter{Name: "B"},
|
|
}
|
|
events(r, &handlers[0], fa...)
|
|
events(r, &handlers[1], fb...)
|
|
events(r, &handlers[2], fa...)
|
|
events(r, &handlers[3], key.FocusFilter{})
|
|
events(r, &handlers[4], fa...)
|
|
|
|
r.Frame(ops)
|
|
|
|
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
|
|
r.Queue(A, B)
|
|
|
|
// With no focus, the events should traverse the final branch of the hit tree
|
|
// searching for handlers.
|
|
if evts := events(r, &handlers[4], fa...); len(evts) != 1 || evts[0] != A {
|
|
t.Errorf("expected key event")
|
|
}
|
|
events(r, &handlers[3], key.FocusFilter{})
|
|
events(r, &handlers[2], fa...)
|
|
if evts := events(r, &handlers[1], fb...); len(evts) != 1 || evts[0] != B {
|
|
t.Errorf("expected key event")
|
|
}
|
|
events(r, &handlers[0], fa...)
|
|
|
|
r2 := new(Router)
|
|
|
|
events(r2, &handlers[0], fa...)
|
|
events(r2, &handlers[1], fb...)
|
|
events(r2, &handlers[2], fa...)
|
|
events(r2, &handlers[3], key.FocusFilter{})
|
|
events(r2, &handlers[4], fa...)
|
|
|
|
r2.Source().Execute(key.FocusCmd{Tag: &handlers[3]})
|
|
r2.Frame(ops)
|
|
|
|
r2.Queue(A, B)
|
|
|
|
// With focus, the events should traverse the branch of the hit tree
|
|
// containing the focused element.
|
|
assertKeyEvent(t, events(r2, &handlers[3], key.FocusFilter{}), true)
|
|
if evts := events(r2, &handlers[0], fa...); len(evts) != 1 || evts[0] != A {
|
|
t.Errorf("expected key event")
|
|
}
|
|
}
|
|
|
|
func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus 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 != expectedFocus {
|
|
t.Errorf("focus is expected to be %v, got %v", expectedFocus, ev.Focus)
|
|
}
|
|
evtFocus++
|
|
case key.Event, key.EditEvent:
|
|
if len(expectedInputs) <= evtKeyPress {
|
|
t.Fatalf("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 got := router.lastState().focus; got != expected {
|
|
t.Errorf("expected %v to be focused, got %v", expected, got)
|
|
}
|
|
}
|
|
|
|
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
|
|
t.Helper()
|
|
if got := router.lastState().state; got != expected {
|
|
t.Errorf("expected %v keyboard, got %v", expected, got)
|
|
}
|
|
}
|