v0.4.0 b6be3a8

constraints_test.go RawHistoryFolder

1package wayland
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/pointerconstraints"
8	"github.com/bnema/go-wayland-bindings/server/pointerwarp"
9	"github.com/bnema/go-wayland-bindings/server/relativepointer"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/go-wayland-bindings/server/xdgshell"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/wlturbo"
14)
15
16// eventLog records the opcodes (and first fixed args) a proxy receives.
17type eventLog struct {
18	wlturbo.BaseProxy
19	events  chan []float64 // opcode, then fixed args for relative motion
20	pointer bool           // a wl_pointer: enter events carry their serial
21}
22
23func (p *eventLog) Dispatch(e *wlturbo.Event) {
24	ev := []float64{float64(e.Opcode)}
25	if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() && !p.pointer {
26		e.Uint32()
27		e.Uint32()
28		for range 4 {
29			ev = append(ev, e.Fixed().Float64())
30		}
31	}
32	if e.Opcode == uint16(wayland.PointerEventEnter) && p.pointer {
33		ev = append(ev, float64(e.Uint32())) // the enter serial
34	}
35	p.events <- ev
36}
37
38func logProxy(t *testing.T, c *wlturbo.Display, id uint32) chan []float64 {
39	t.Helper()
40	p := &eventLog{events: make(chan []float64, 64)}
41	p.SetID(id)
42	registerWireProxy(c, p)
43	return p.events
44}
45
46// next dispatches until the proxy receives an event.
47func next(t *testing.T, c *wlturbo.Display, events chan []float64) []float64 {
48	t.Helper()
49	deadline := time.Now().Add(2 * time.Second)
50	for time.Now().Before(deadline) {
51		select {
52		case ev := <-events:
53			return ev
54		default:
55		}
56		if err := c.Roundtrip(); err != nil {
57			t.Fatal(err)
58		}
59	}
60	t.Fatal("no event")
61	return nil
62}
63
64func constrained(t *testing.T, events <-chan ports.ClientEvent) ports.PointerConstrained {
65	t.Helper()
66	for {
67		if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerConstrained); ok {
68			return v
69		}
70	}
71}
72
73// A warp from the client with the pointer, with its enter serial and on
74// its window, reaches core window-local; others are ignored.
75func TestPointerWarp(t *testing.T) {
76	s, events, commands, dir := lifecycleServer(t)
77	c := protocolClient(t, s, dir)
78	seat := bindProtocol(t, c, "wl_seat")
79	registerProtocol(t, c, seat)
80	pointer := c.AllocateID()
81	p := &eventLog{events: make(chan []float64, 64), pointer: true}
82	p.SetID(pointer)
83	registerWireProxy(c, p)
84	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
85	w, surf, xdgID := surfaceMapper(t, c, events)()
86	requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(4), int32(6), int32(50), int32(50))
87	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
88	warp := bindProtocol(t, c, "wp_pointer_warp_v1")
89
90	// Without the pointer, a warp is ignored.
91	requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), uint32(0))
92	commands <- ports.PointerFocus{ID: w.ID}
93	ev := next(t, c, p.events)
94	for ev[0] != float64(wayland.PointerEventEnter) {
95		ev = next(t, c, p.events)
96	}
97	serial := uint32(ev[1])
98	// A second pointer gets its own enter serial: the first pointer's
99	// serial does not validate it.
100	pointer2 := c.AllocateID()
101	p2 := &eventLog{events: make(chan []float64, 64), pointer: true}
102	p2.SetID(pointer2)
103	registerWireProxy(c, p2)
104	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer2)
105	requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer2, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial)
106	// A stale serial is ignored; the enter serial moves the pointer.
107	requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(10*256), wlturbo.Fixed(10*256), serial+100)
108	requestProtocol(t, c, warp, pointerwarp.WpPointerWarpV1RequestWarpPointer, surf, pointer, wlturbo.Fixed(14*256), wlturbo.Fixed(26*256), serial)
109	if err := c.Roundtrip(); err != nil {
110		t.Fatal(err)
111	}
112	for {
113		if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerWarp); ok {
114			// Surface-local minus the window geometry origin.
115			if v != (ports.PointerWarp{ID: w.ID, X: 10, Y: 20}) {
116				t.Fatalf("warp %+v", v)
117			}
118			break
119		}
120	}
121}
122
123func TestPointerLockAndRelativeMotion(t *testing.T) {
124	s, events, commands, dir := lifecycleServer(t)
125	c := protocolClient(t, s, dir)
126	seat := bindProtocol(t, c, "wl_seat")
127	registerProtocol(t, c, seat)
128	pointer := c.AllocateID()
129	pointerEvents := logProxy(t, c, pointer)
130	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
131	relManager := bindProtocol(t, c, "zwp_relative_pointer_manager_v1")
132	rel := c.AllocateID()
133	relEvents := logProxy(t, c, rel)
134	requestProtocol(t, c, relManager, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer)
135	w, surf, _ := surfaceMapper(t, c, events)()
136
137	constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1")
138	lock := c.AllocateID()
139	lockEvents := logProxy(t, c, lock)
140	requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
141	if err := c.Roundtrip(); err != nil {
142		t.Fatal(err)
143	}
144	select {
145	case ev := <-lockEvents:
146		t.Fatalf("locked without focus: %v", ev)
147	default:
148	}
149
150	// Pointer and keyboard focus activate the lock.
151	commands <- ports.PointerFocus{ID: w.ID}
152	commands <- ports.FocusWindow{ID: w.ID}
153	if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
154		t.Fatalf("lock event %v", ev)
155	}
156	if got := constrained(t, events); got.ID != w.ID || got.Mode != ports.ConstraintLock {
157		t.Fatalf("constrained %+v", got)
158	}
159	for len(pointerEvents) > 0 {
160		<-pointerEvents
161	}
162
163	// Locked: relative motion flows, absolute motion does not.
164	commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5, DX: 1.5, DY: -2, UnaccelDX: 3, UnaccelDY: -4}
165	if ev := next(t, c, relEvents); len(ev) != 5 || ev[1] != 1.5 || ev[2] != -2 || ev[3] != 3 || ev[4] != -4 {
166		t.Fatalf("relative motion %v", ev)
167	}
168	for len(pointerEvents) > 0 {
169		if ev := <-pointerEvents; ev[0] == float64(wayland.PointerEventMotion) {
170			t.Fatal("motion sent to a locked pointer")
171		}
172	}
173
174	// Losing focus unlocks; a persistent lock comes back.
175	commands <- ports.FocusWindow{}
176	if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) {
177		t.Fatalf("unlock event %v", ev)
178	}
179	if got := constrained(t, events); got.ID != 0 {
180		t.Fatalf("constrained %+v", got)
181	}
182	commands <- ports.FocusWindow{ID: w.ID}
183	if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) {
184		t.Fatalf("relock event %v", ev)
185	}
186}
187
188func TestPointerConfineOneshotRegion(t *testing.T) {
189	s, events, commands, dir := lifecycleServer(t)
190	c := protocolClient(t, s, dir)
191	seat := bindProtocol(t, c, "wl_seat")
192	registerProtocol(t, c, seat)
193	pointer := c.AllocateID()
194	pointerEvents := logProxy(t, c, pointer)
195	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
196	comp := bindProtocol(t, c, "wl_compositor")
197	w, surf, xdgID := surfaceMapper(t, c, events)()
198	region := c.AllocateID()
199	registerProtocol(t, c, region)
200	requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region)
201	requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(2), int32(3), int32(10), int32(10))
202	requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(20), int32(3), int32(5), int32(20))
203
204	constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1")
205	confine := c.AllocateID()
206	confineEvents := logProxy(t, c, confine)
207	requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
208	// The region is copied: changing it later has no effect.
209	requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100))
210	// Outside the region the constraint waits for the pointer.
211	commands <- ports.FocusWindow{ID: w.ID}
212	commands <- ports.PointerFocus{ID: w.ID}
213	commands <- ports.PointerMotionTo{ID: w.ID, X: 50, Y: 50}
214	// Sync on the motion reaching the client before checking.
215	for ev := next(t, c, pointerEvents); ev[0] != float64(wayland.PointerEventMotion); ev = next(t, c, pointerEvents) {
216	}
217	if len(confineEvents) > 0 {
218		t.Fatal("confined outside the region")
219	}
220	commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5}
221	if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {
222		t.Fatalf("confine event %v", ev)
223	}
224	want := ports.PointerConstrained{ID: w.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 2, Y: 3, W: 23, H: 20}}}
225	if got := constrained(t, events); got != want {
226		t.Fatalf("constrained %+v, want %+v", got, want)
227	}
228
229	// A geometry change moves the window-local region.
230	requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(1), int32(1), int32(1), int32(1))
231	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
232	if got := constrained(t, events); got.Rect != (ports.Rect{X: 1, Y: 2, W: 23, H: 20}) {
233		t.Fatalf("after geometry %+v", got)
234	}
235
236	// A new region applies on commit.
237	requestProtocol(t, c, confine, pointerconstraints.ZwpConfinedPointerV1RequestSetRegion, uint32(0))
238	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
239	if got := constrained(t, events); got.Rect != (ports.Rect{}) {
240		t.Fatalf("whole surface %+v", got)
241	}
242
243	// A oneshot constraint ends for good with the focus.
244	commands <- ports.PointerFocus{}
245	if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) {
246		t.Fatalf("unconfine event %v", ev)
247	}
248	commands <- ports.PointerFocus{ID: w.ID, X: 5, Y: 5}
249	if err := c.Roundtrip(); err != nil {
250		t.Fatal(err)
251	}
252	select {
253	case ev := <-confineEvents:
254		t.Fatalf("oneshot reactivated: %v", ev)
255	case <-time.After(50 * time.Millisecond):
256	}
257
258	// A second constraint on the surface is a protocol error.
259	again := c.AllocateID()
260	registerProtocol(t, c, again)
261	requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
262	expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained))
263}
264
265// Destroying an active lock unlocks the pointer in core.
266func TestPointerLockDestroyed(t *testing.T) {
267	s, events, commands, dir := lifecycleServer(t)
268	c := protocolClient(t, s, dir)
269	seat := bindProtocol(t, c, "wl_seat")
270	registerProtocol(t, c, seat)
271	pointer := c.AllocateID()
272	registerProtocol(t, c, pointer)
273	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
274	w, surf, _ := surfaceMapper(t, c, events)()
275	constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1")
276	lock := c.AllocateID()
277	lockEvents := logProxy(t, c, lock)
278	requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
279	commands <- ports.PointerFocus{ID: w.ID}
280	commands <- ports.FocusWindow{ID: w.ID}
281	next(t, c, lockEvents)
282	if got := constrained(t, events); got.Mode != ports.ConstraintLock {
283		t.Fatalf("constrained %+v", got)
284	}
285	requestProtocol(t, c, lock, pointerconstraints.ZwpLockedPointerV1RequestDestroy)
286	if got := constrained(t, events); got.ID != 0 {
287		t.Fatalf("after destroy %+v", got)
288	}
289}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close