Files
gio/io/input/pointer_test.go
T
2024-02-05 11:09:33 +00:00

1197 lines
32 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"fmt"
"image"
"reflect"
"strings"
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/op/clip"
)
func TestPointerWakeup(t *testing.T) {
handler := new(int)
var ops op.Ops
var r Router
addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100))
// Test that merely adding a handler doesn't trigger redraw.
r.Frame(&ops)
if _, wake := r.WakeupTime(); wake {
t.Errorf("adding pointer.InputOp triggered a redraw")
}
}
func TestPointerDrag(t *testing.T) {
handler := new(int)
var ops op.Ops
var r Router
f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100))
r.Frame(&ops)
r.Queue(
// Press.
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
// Move outside the area.
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
)
assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag)
}
func TestPointerDragNegative(t *testing.T) {
handler := new(int)
var ops op.Ops
var r Router
f := addPointerHandler(&r, &ops, handler, image.Rect(-100, -100, 0, 0))
r.Frame(&ops)
r.Queue(
// Press.
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(-50, -50),
},
// Move outside the area.
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(-150, -150),
},
)
assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag)
}
func TestPointerGrab(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
handler3 := new(int)
var ops op.Ops
filter := pointer.Filter{Kinds: pointer.Press | pointer.Release}
event.InputOp(&ops, handler1)
event.InputOp(&ops, handler2)
event.InputOp(&ops, handler3)
var r Router
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Cancel)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Press)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Release)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Cancel)
}
func TestPointerGrabSameHandlerTwice(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
var ops op.Ops
filter := pointer.Filter{Kinds: pointer.Press | pointer.Release}
event.InputOp(&ops, handler1)
event.InputOp(&ops, handler1)
event.InputOp(&ops, handler2)
var r Router
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
r.Source().Queue(pointer.GrabCmd{Tag: handler1})
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Release)
assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel)
}
func TestPointerMove(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
var ops op.Ops
f := pointer.Filter{
Kinds: pointer.Move | pointer.Enter | pointer.Leave,
}
// Handler 1 area: (0, 0) - (100, 100)
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
event.InputOp(&ops, handler1)
// Handler 2 area: (50, 50) - (100, 100) (areas intersect).
r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops)
event.InputOp(&ops, handler2)
r2.Pop()
r1.Pop()
var r Router
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Cancel)
r.Frame(&ops)
r.Queue(
// Hit both handlers.
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
// Hit handler 1.
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(49, 50),
},
// Hit no handlers.
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(100, 50),
},
pointer.Event{
Kind: pointer.Cancel,
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel)
}
func TestPointerTypes(t *testing.T) {
handler := new(int)
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f := pointer.Filter{
Kinds: pointer.Press | pointer.Release,
}
event.InputOp(&ops, handler)
r1.Pop()
var r Router
assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Cancel)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
pointer.Event{
Kind: pointer.Release,
Position: f32.Pt(150, 150),
},
)
assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Press, pointer.Release)
}
func TestPointerSystemAction(t *testing.T) {
t.Run("simple", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
t.Run("covered by another clip", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
t.Run("uses topmost action op", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
r2 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionClose).Add(&ops)
r2.Pop()
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionClose)
})
}
func TestPointerPriority(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
handler3 := new(int)
var ops op.Ops
var r Router
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f1 := pointer.Filter{
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
}
r.Events(handler1, f1)
event.InputOp(&ops, handler1)
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
f2 := pointer.Filter{
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
}
r.Events(handler2, f2)
event.InputOp(&ops, handler2)
r2.Pop()
r1.Pop()
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
f3 := pointer.Filter{
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
}
r.Events(handler3, f3)
event.InputOp(&ops, handler3)
r3.Pop()
r.Frame(&ops)
r.Queue(
// Hit handler 1 and 2.
pointer.Event{
Kind: pointer.Scroll,
Position: f32.Pt(50, 25),
Scroll: f32.Pt(50, 0),
},
// Hit handler 1.
pointer.Event{
Kind: pointer.Scroll,
Position: f32.Pt(50, 75),
Scroll: f32.Pt(50, 50),
},
// Hit handler 3.
pointer.Event{
Kind: pointer.Scroll,
Position: f32.Pt(50, 150),
Scroll: f32.Pt(-30, -30),
},
// Hit no handlers.
pointer.Event{
Kind: pointer.Scroll,
Position: f32.Pt(50, 225),
},
)
hev1 := r.Events(handler1, f1)
hev2 := r.Events(handler2, f2)
hev3 := r.Events(handler3, f3)
assertEventPointerTypeSequence(t, hev1, pointer.Scroll, pointer.Scroll)
assertEventPointerTypeSequence(t, hev2, pointer.Scroll)
assertEventPointerTypeSequence(t, hev3, pointer.Scroll)
assertEventPriorities(t, hev1, pointer.Shared, pointer.Foremost)
assertEventPriorities(t, hev2, pointer.Foremost)
assertEventPriorities(t, hev3, pointer.Foremost)
assertScrollEvent(t, hev1[0], f32.Pt(30, 0))
assertScrollEvent(t, hev2[0], f32.Pt(20, 0))
assertScrollEvent(t, hev1[1], f32.Pt(50, 0))
assertScrollEvent(t, hev3[0], f32.Pt(-20, -30))
}
func TestPointerEnterLeave(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
var ops op.Ops
var r Router
// Handler 1 area: (0, 0) - (100, 100)
f1 := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100))
// Handler 2 area: (50, 50) - (200, 200) (areas overlap).
f2 := addPointerHandler(&r, &ops, handler2, image.Rect(50, 50, 200, 200))
r.Frame(&ops)
// Hit both handlers.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
// First event for a handler is always a Cancel.
// Only handler2 should receive the enter/move events because it is on top
// and handler1 is not an ancestor in the hit tree.
assertEventPointerTypeSequence(t, r.Events(handler1, f1))
assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Enter, pointer.Move)
// Leave the second area by moving into the first.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(45, 45),
},
)
// The cursor leaves handler2 and enters handler1.
assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Enter, pointer.Move)
assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Leave)
// Move, but stay within the same hit area.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(40, 40),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Move)
assertEventPointerTypeSequence(t, r.Events(handler2, f2))
// Move outside of both inputs.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(300, 300),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Leave)
assertEventPointerTypeSequence(t, r.Events(handler2, f2))
// Check that a Press event generates Enter Events.
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(125, 125),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f1))
assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Enter, pointer.Press)
// Check that a drag only affects the participating handlers.
r.Queue(
// Leave
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
// Enter
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f1))
assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Leave, pointer.Drag, pointer.Enter, pointer.Drag)
// Check that a Release event generates Enter/Leave Events.
r.Queue(
pointer.Event{
Kind: pointer.Release,
Position: f32.Pt(25,
25),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Enter)
// The second handler gets the release event because the press started inside it.
assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Release, pointer.Leave)
}
func TestMultipleAreas(t *testing.T) {
handler := new(int)
var ops op.Ops
var r Router
f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100))
r1 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops)
// Test that declaring a handler twice doesn't affect event handling.
event.InputOp(&ops, handler)
r1.Pop()
assertEventPointerTypeSequence(t, r.Events(handler, f))
r.Frame(&ops)
// Hit first area, then second area, then both.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Move, pointer.Move, pointer.Move)
}
func TestPointerEnterLeaveNested(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
var ops op.Ops
f := pointer.Filter{Kinds: pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave}
// Handler 1 area: (0, 0) - (100, 100)
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
event.InputOp(&ops, handler1)
// Handler 2 area: (25, 25) - (75, 75) (nested within first).
r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops)
event.InputOp(&ops, handler2)
r2.Pop()
r1.Pop()
var r Router
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Cancel)
r.Frame(&ops)
// Hit both handlers.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
// First event for a handler is always a Cancel.
// Both handlers should receive the Enter and Move events because handler2 is a child of handler1.
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Move)
// Leave the second area by moving into the first.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(20, 20),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Move)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Leave)
// Move, but stay within the same hit area.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(10, 10),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Move)
assertEventPointerTypeSequence(t, r.Events(handler2, f))
// Move outside of both inputs.
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(200, 200),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Leave)
assertEventPointerTypeSequence(t, r.Events(handler2, f))
// Check that a Press event generates Enter Events.
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Press)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Press)
// Check that a Release event generates Enter/Leave Events.
r.Queue(
pointer.Event{
Kind: pointer.Release,
Position: f32.Pt(20, 20),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Release)
assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Release, pointer.Leave)
}
func TestPointerActiveInputDisappears(t *testing.T) {
handler1 := new(int)
var ops op.Ops
var r Router
// Draw handler.
ops.Reset()
f := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100))
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move)
r.Frame(&ops)
// Re-render with handler missing.
ops.Reset()
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
)
assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel)
}
func TestMultitouch(t *testing.T) {
var ops op.Ops
var r Router
// Add two separate handlers.
h1, h2 := new(int), new(int)
f1 := addPointerHandler(&r, &ops, h1, image.Rect(0, 0, 100, 100))
f2 := addPointerHandler(&r, &ops, h2, image.Rect(0, 100, 100, 200))
h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100)
var p1, p2 pointer.ID = 0, 1
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: h1pt,
PointerID: p1,
},
)
r.Queue(
pointer.Event{
Kind: pointer.Press,
Position: h2pt,
PointerID: p2,
},
)
r.Queue(
pointer.Event{
Kind: pointer.Release,
Position: h2pt,
PointerID: p2,
},
)
assertEventPointerTypeSequence(t, r.Events(h1, f1), pointer.Enter, pointer.Press)
assertEventPointerTypeSequence(t, r.Events(h2, f2), pointer.Enter, pointer.Press, pointer.Release)
}
func TestCursor(t *testing.T) {
_at := func(x, y float32) []event.Event {
return []event.Event{pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Position: f32.Pt(x, y),
}}
}
ops := new(op.Ops)
var r Router
for _, tc := range []struct {
label string
events []event.Event
cursors []pointer.Cursor
want pointer.Cursor
}{
{label: "no movement",
cursors: []pointer.Cursor{pointer.CursorPointer},
want: pointer.CursorDefault,
},
{label: "move inside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: _at(50, 50),
want: pointer.CursorPointer,
},
{label: "move outside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: _at(200, 200),
want: pointer.CursorDefault,
},
{label: "move back inside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: _at(50, 50),
want: pointer.CursorPointer,
},
{label: "send key events while inside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: []event.Event{
key.Event{Name: "A", State: key.Press},
key.Event{Name: "A", State: key.Release},
},
want: pointer.CursorPointer,
},
{label: "send key events while outside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: append(
_at(200, 200),
key.Event{Name: "A", State: key.Press},
key.Event{Name: "A", State: key.Release},
),
want: pointer.CursorDefault,
},
{label: "add new input on top while inside",
cursors: []pointer.Cursor{pointer.CursorPointer, pointer.CursorCrosshair},
events: append(
_at(50, 50),
key.Event{
Name: "A",
State: key.Press,
},
),
want: pointer.CursorCrosshair,
},
{label: "remove input on top while inside",
cursors: []pointer.Cursor{pointer.CursorPointer},
events: append(
_at(50, 50),
key.Event{
Name: "A",
State: key.Press,
},
),
want: pointer.CursorPointer,
},
} {
t.Run(tc.label, func(t *testing.T) {
ops.Reset()
defer clip.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Push(ops).Pop()
for _, c := range tc.cursors {
c.Add(ops)
}
r.Frame(ops)
r.Queue(tc.events...)
r.Frame(ops)
// The cursor should now have been changed if the mouse moved over the declared area.
if got, want := r.Cursor(), tc.want; got != want {
t.Errorf("got %q; want %q", got, want)
}
})
}
}
func TestPassOp(t *testing.T) {
var ops op.Ops
h1, h2, h3, h4 := new(int), new(int), new(int), new(int)
area := clip.Rect(image.Rect(0, 0, 100, 100))
root := area.Push(&ops)
event.InputOp(&ops, &h1)
event.InputOp(&ops, h1)
child1 := area.Push(&ops)
event.InputOp(&ops, h2)
child1.Pop()
child2 := area.Push(&ops)
pass := pointer.PassOp{}.Push(&ops)
event.InputOp(&ops, h3)
event.InputOp(&ops, h4)
pass.Pop()
child2.Pop()
root.Pop()
var r Router
f := pointer.Filter{Kinds: pointer.Press}
assertEventPointerTypeSequence(t, r.Events(h1, f), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(h2, f), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(h3, f), pointer.Cancel)
assertEventPointerTypeSequence(t, r.Events(h4, f), pointer.Cancel)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h1, f), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(h2, f), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(h3, f), pointer.Press)
assertEventPointerTypeSequence(t, r.Events(h4, f), pointer.Press)
}
func TestAreaPassthrough(t *testing.T) {
var ops op.Ops
h := new(int)
event.InputOp(&ops, h)
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
var r Router
f := pointer.Filter{
Kinds: pointer.Press,
}
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press)
}
func TestEllipse(t *testing.T) {
var ops op.Ops
h := new(int)
cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops)
event.InputOp(&ops, h)
cl.Pop()
var r Router
f := pointer.Filter{
Kinds: pointer.Press,
}
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel)
r.Frame(&ops)
r.Queue(
// Outside ellipse.
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Kind: pointer.Release,
},
// Inside ellipse.
pointer.Event{
Position: f32.Pt(50, 50),
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press)
}
func TestTransfer(t *testing.T) {
srcArea := image.Rect(0, 0, 20, 20)
tgtArea := srcArea.Add(image.Pt(40, 0))
setup := func(r *Router, ops *op.Ops, srcType, tgtType string) (src, tgt event.Tag) {
src, tgt = new(int), new(int)
r.Events(src, transfer.SourceFilter{Type: srcType})
r.Events(tgt, transfer.TargetFilter{Type: tgtType})
srcStack := clip.Rect(srcArea).Push(ops)
event.InputOp(ops, src)
srcStack.Pop()
tgt1Stack := clip.Rect(tgtArea).Push(ops)
event.InputOp(ops, tgt)
tgt1Stack.Pop()
return src, tgt
}
t.Run("drop on no target", func(t *testing.T) {
ops := new(op.Ops)
var r Router
src, tgt := setup(&r, ops, "file", "file")
r.Frame(ops)
// Initiate a drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}))
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{})
// Drop.
r.Queue(
pointer.Event{
Position: f32.Pt(30, 10),
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(30, 10),
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.CancelEvent{})
})
t.Run("drag with valid and invalid targets", func(t *testing.T) {
ops := new(op.Ops)
var r Router
src, tgt1 := setup(&r, ops, "file", "file")
tgt2 := new(int)
r.Events(tgt2, transfer.TargetFilter{Type: "nofile"})
stack := clip.Rect(tgtArea).Push(ops)
event.InputOp(ops, tgt2)
stack.Pop()
r.Frame(ops)
// Initiate a drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}))
assertEventSequence(t, r.Events(tgt1, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{})
assertEventSequence(t, r.Events(tgt2, transfer.TargetFilter{Type: "nofile"}))
})
t.Run("drop on invalid target", func(t *testing.T) {
ops := new(op.Ops)
var r Router
src, tgt := setup(&r, ops, "file", "nofile")
r.Frame(ops)
// Drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}))
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "nofile"}))
// Drop.
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "nofile"}))
})
t.Run("drop on valid target", func(t *testing.T) {
ops := new(op.Ops)
var r Router
src, tgt := setup(&r, ops, "file", "file")
// Make the target also a source. This should have no effect.
r.Events(tgt, transfer.SourceFilter{Type: "file"})
r.Frame(ops)
// Drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}))
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{})
// Drop.
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.RequestEvent{Type: "file"})
// Offer valid type and data.
ofr := &offer{data: "hello"}
r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
evs := r.Events(tgt, transfer.TargetFilter{Type: "file"})
if len(evs) != 2 {
t.Fatalf("unexpected number of events: %d, want 2", len(evs))
}
assertEventSequence(t, evs[1:], transfer.CancelEvent{})
dataEvent, ok := evs[0].(transfer.DataEvent)
if !ok {
t.Fatalf("unexpected event type: %T, want %T", dataEvent, transfer.DataEvent{})
}
if got, want := dataEvent.Type, "file"; got != want {
t.Fatalf("got %s; want %s", got, want)
}
if got, want := dataEvent.Open(), ofr; got != want {
t.Fatalf("got %v; want %v", got, want)
}
// Drag and drop complete.
if ofr.closed {
t.Error("offer closed prematurely")
}
})
t.Run("drop on valid target, DataEvent not used", func(t *testing.T) {
ops := new(op.Ops)
var r Router
src, tgt := setup(&r, ops, "file", "file")
// Make the target also a source. This should have no effect.
r.Events(tgt, transfer.SourceFilter{Type: "file"})
r.Frame(ops)
// Drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Release,
},
)
ofr := &offer{data: "hello"}
r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
r.Events(src, transfer.SourceFilter{Type: "file"})
r.Events(tgt, transfer.TargetFilter{Type: "file"})
r.Frame(ops)
assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{})
// Ignore DataEvent and verify that the next frame closes it as unused.
assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"})[1:], transfer.CancelEvent{})
r.Frame(ops)
if !ofr.closed {
t.Error("offer was not closed")
}
})
}
func TestDeferredInputOp(t *testing.T) {
var ops op.Ops
var r Router
m := op.Record(&ops)
event.InputOp(&ops, new(int))
call := m.Stop()
op.Defer(&ops, call)
r.Frame(&ops)
}
func TestPassCursor(t *testing.T) {
var ops op.Ops
var r Router
rect := clip.Rect(image.Rect(0, 0, 100, 100))
background := rect.Push(&ops)
event.InputOp(&ops, 1)
pointer.CursorDefault.Add(&ops)
background.Pop()
overlayPass := pointer.PassOp{}.Push(&ops)
overlay := rect.Push(&ops)
event.InputOp(&ops, 2)
want := pointer.CursorPointer
want.Add(&ops)
overlay.Pop()
overlayPass.Pop()
r.Frame(&ops)
r.Queue(pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
})
if got := r.Cursor(); want != got {
t.Errorf("got cursor %v, want %v", got, want)
}
}
// offer satisfies io.ReadCloser for use in data transfers.
type offer struct {
data string
closed bool
}
func (offer) Read([]byte) (int, error) { return 0, nil }
func (o *offer) Close() error {
o.closed = true
return nil
}
// addPointerHandler adds a pointer.InputOp for the tag in a
// rectangular area.
func addPointerHandler(r *Router, ops *op.Ops, tag event.Tag, area image.Rectangle) pointer.Filter {
f := pointer.Filter{
Kinds: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
}
r.Events(tag, f)
defer clip.Rect(area).Push(ops).Pop()
event.InputOp(ops, tag)
return f
}
// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
// that all input events are of underlying type pointer.Event, and thus will
// panic if some are not.
func pointerTypes(events []event.Event) []pointer.Kind {
var types []pointer.Kind
for _, e := range events {
if e, ok := e.(pointer.Event); ok {
types = append(types, e.Kind)
}
}
return types
}
// assertEventPointerTypeSequence checks that the provided events match the expected pointer event types
// in the provided order.
func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Kind) {
t.Helper()
got := pointerTypes(events)
if !reflect.DeepEqual(got, expected) {
t.Errorf("expected %v events, got %v", expected, got)
}
}
// assertEventSequence checks that the provided events match the expected ones
// in the provided order.
func assertEventSequence(t *testing.T, got []event.Event, expected ...event.Event) {
t.Helper()
if len(expected) == 0 {
if len(got) > 0 {
t.Errorf("unexpected events: %v", eventsToString(got))
}
return
}
if !reflect.DeepEqual(got, expected) {
t.Errorf("expected %s events, got %s", eventsToString(expected), eventsToString(got))
}
}
func eventsToString(evs []event.Event) string {
var s []string
for _, ev := range evs {
switch e := ev.(type) {
case pointer.Event:
s = append(s, fmt.Sprintf("%T{%s}", e, e.Kind.String()))
default:
s = append(s, fmt.Sprintf("{%T}", e))
}
}
return "[" + strings.Join(s, ",") + "]"
}
// assertEventPriorities checks that the pointer.Event priorities of events match prios.
func assertEventPriorities(t *testing.T, events []event.Event, prios ...pointer.Priority) {
t.Helper()
var got []pointer.Priority
for _, e := range events {
if e, ok := e.(pointer.Event); ok {
got = append(got, e.Priority)
}
}
if !reflect.DeepEqual(got, prios) {
t.Errorf("expected priorities %v, got %v", prios, got)
}
}
// assertScrollEvent checks that the event scrolling amount matches the supplied value.
func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
t.Helper()
if got, want := ev.(pointer.Event).Scroll, scroll; got != want {
t.Errorf("got %v; want %v", got, want)
}
}
// assertActionAt checks that the router has a system action of the expected type at point.
func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) {
t.Helper()
action, ok := q.ActionAt(point)
if !ok {
t.Errorf("expected action %v at %v, got no action", expected, point)
} else if action != expected {
t.Errorf("expected action %v at %v, got %v", expected, point, action)
}
}
func BenchmarkRouterAdd(b *testing.B) {
// Set this to the number of overlapping handlers that you want to
// evaluate performance for. Typical values for the example applications
// are 1-3, though checking highers values helps evaluate performance for
// more complex applications.
const startingHandlerCount = 3
const maxHandlerCount = 100
for i := startingHandlerCount; i < maxHandlerCount; i *= 3 {
handlerCount := i
b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) {
handlers := make([]event.Tag, handlerCount)
for i := 0; i < handlerCount; i++ {
h := new(int)
*h = i
handlers[i] = h
}
var ops op.Ops
var r Router
for i := range handlers {
clip.Rect(image.Rectangle{
Max: image.Point{
X: 100,
Y: 100,
},
}).
Push(&ops)
r.Events(handlers[i], pointer.Filter{Kinds: pointer.Move})
event.InputOp(&ops, handlers[i])
}
r.Frame(&ops)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Queue(
pointer.Event{
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
}
})
}
}