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}