1package wayland
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 "github.com/bnema/go-wayland-bindings/server/wlrlayershell"
12 "github.com/bnema/go-wayland-bindings/server/xdgshell"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/wlturbo"
15 "golang.org/x/sys/unix"
16)
17
18type layerProxy struct {
19 wlturbo.BaseProxy
20 configured chan [3]uint32
21}
22
23func (p *layerProxy) Dispatch(e *wlturbo.Event) {
24 if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventConfigure) {
25 p.configured <- [3]uint32{e.Uint32(), e.Uint32(), e.Uint32()}
26 }
27}
28func layerProtocol(t *testing.T, c *wlturbo.Display) (uint32, uint32) {
29 t.Helper()
30 comp := bindProtocol(t, c, "wl_compositor")
31 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
32 surf := c.AllocateID()
33 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
34 registerProtocol(t, c, surf)
35 layer := c.AllocateID()
36 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layer, surf, uint32(0), uint32(ports.LayerTop), "test")
37 return surf, layer
38}
39func TestLayerInvalidSize(t *testing.T) {
40 s, _, _, dir := lifecycleServer(t)
41 c := protocolClient(t, s, dir)
42 surf, layer := layerProtocol(t, c)
43 registerProtocol(t, c, layer)
44 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30))
45 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(ports.AnchorTop))
46 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
47 expectProtocolError(t, c, layer, uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize))
48}
49func TestLayerLifecycle(t *testing.T) {
50 s, events, _, dir := lifecycleServer(t)
51 c := protocolClient(t, s, dir)
52 surf, layer := layerProtocol(t, c)
53 proxy := &layerProxy{configured: make(chan [3]uint32, 2)}
54 proxy.SetID(layer)
55 registerWireProxy(c, proxy)
56 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(13))
57 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30))
58 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetExclusiveZone, int32(30))
59 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
60 if err := c.Roundtrip(); err != nil {
61 t.Fatal(err)
62 }
63 var config [3]uint32
64 select {
65 case config = <-proxy.configured:
66 case <-time.After(2 * time.Second):
67 t.Fatal("configure timeout")
68 }
69 if config[1] != 1920 || config[2] != 30 {
70 t.Fatalf("configure: %v", config)
71 }
72 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0])
73 shm := bindProtocol(t, c, "wl_shm")
74 registerProtocol(t, c, shm)
75 fd, err := unix.MemfdCreate("layer-buffer", 0)
76 if err != nil {
77 t.Fatal(err)
78 }
79 defer unix.Close(fd)
80 if err = unix.Ftruncate(fd, 1920*30*4); err != nil {
81 t.Fatal(err)
82 }
83 pool, buf := c.AllocateID(), c.AllocateID()
84 registerProtocol(t, c, pool)
85 registerProtocol(t, c, buf)
86 if err = wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(1920*30*4)); err != nil {
87 t.Fatal(err)
88 }
89 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1920), int32(30), int32(1920*4), uint32(wayland.ShmFormatArgb8888))
90 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
91 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
92 if err = c.Roundtrip(); err != nil {
93 t.Fatal(err)
94 }
95 select {
96 case ev := <-events:
97 v, ok := ev.(ports.LayerChanged)
98 if !ok || len(v.Layers) != 1 || v.Layers[0].Anchor != 13 || v.Layers[0].ExclusiveZone != 30 || v.Layers[0].Width != 1920 || v.Layers[0].Height != 30 {
99 t.Fatalf("mapped: %#v", ev)
100 }
101 case <-time.After(2 * time.Second):
102 t.Fatal("map timeout")
103 }
104 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
105 if err = c.Roundtrip(); err != nil {
106 t.Fatal(err)
107 }
108 select {
109 case ev := <-events:
110 v, ok := ev.(ports.LayerChanged)
111 if !ok || len(v.Layers) != 0 {
112 t.Fatalf("destroy: %#v", ev)
113 }
114 case <-time.After(2 * time.Second):
115 t.Fatal("destroy timeout")
116 }
117}
118func TestWaybarLayer(t *testing.T) {
119 tool, err := exec.LookPath("waybar")
120 if err != nil {
121 t.Skip("waybar missing")
122 }
123 s, events, _, dir := keyboardServer(t)
124 home := t.TempDir()
125 config := home + "/config.json"
126 style := home + "/style.css"
127 if err = os.WriteFile(config, []byte(`{"layer":"top","position":"top","height":30,"modules-left":["clock"]}`), 0600); err != nil {
128 t.Fatal(err)
129 }
130 if err = os.WriteFile(style, nil, 0600); err != nil {
131 t.Fatal(err)
132 }
133 // waybar (gio) aborts without a session bus; the user bus is the default.
134 bus := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
135 if bus == "" {
136 sock := os.Getenv("XDG_RUNTIME_DIR") + "/bus"
137 if _, err := os.Stat(sock); err != nil {
138 t.Skip("no D-Bus session bus")
139 }
140 bus = "unix:path=" + sock
141 }
142 _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + home, "DBUS_SESSION_BUS_ADDRESS=" + bus}, "-c", config, "-s", style)
143 select {
144 case ev := <-events:
145 v, ok := ev.(ports.LayerChanged)
146 if !ok || len(v.Layers) != 1 || v.Layers[0].Height != 30 {
147 t.Fatalf("waybar event: %#v stderr: %s", ev, output.String())
148 }
149 case err := <-done:
150 t.Fatalf("waybar exited: %v stderr: %s", err, output.String())
151 case <-time.After(10 * time.Second):
152 t.Fatalf("waybar timeout stderr: %s", output.String())
153 }
154}
155
156func TestLayerAlreadyConstructed(t *testing.T) {
157 for _, scenario := range []string{"role"} {
158 t.Run(scenario, func(t *testing.T) {
159 s, _, _, dir := lifecycleServer(t)
160 c := protocolClient(t, s, dir)
161 comp := bindProtocol(t, c, "wl_compositor")
162 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
163 surf := c.AllocateID()
164 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
165 registerProtocol(t, c, surf)
166 switch scenario {
167 case "role":
168 sub := bindProtocol(t, c, "wl_subcompositor")
169 parent := c.AllocateID()
170 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent)
171 registerProtocol(t, c, parent)
172 id := c.AllocateID()
173 requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent)
174 }
175 id := c.AllocateID()
176 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test")
177 expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed))
178 })
179 }
180}
181
182// A wl_surface keeps its layer role after its layer surface is destroyed,
183// so the client may give it a new one (swayosd does this on every OSD).
184func TestLayerRecreateOnSameSurface(t *testing.T) {
185 s, _, _, dir := lifecycleServer(t)
186 c := protocolClient(t, s, dir)
187 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
188 surf, layer := layerProtocol(t, c)
189 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
190 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
192 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
193 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
194 again := c.AllocateID()
195 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, again, surf, uint32(0), uint32(ports.LayerOverlay), "test")
196 requestProtocol(t, c, again, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
197 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
198 if err := c.Roundtrip(); err != nil {
199 t.Fatalf("recreating the layer surface failed: %v", err)
200 }
201}
202
203type doneProxy struct {
204 wlturbo.BaseProxy
205 done int
206 opcode uint16
207}
208
209func (p *doneProxy) Dispatch(e *wlturbo.Event) {
210 if e.Opcode == p.opcode {
211 p.done++
212 }
213}
214func TestXDGOutputDone(t *testing.T) {
215 for _, version := range []uint32{1, 2} {
216 t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) {
217 s, _, _, dir := lifecycleServer(t)
218 c := protocolClient(t, s, dir)
219 global, ok := c.Registry().FindGlobal("wl_output")
220 if !ok {
221 t.Fatal("missing wl_output")
222 }
223 output, err := bindWireID(c, global.Name, global.Interface, version)
224 if err != nil {
225 t.Fatal(err)
226 }
227 op := &doneProxy{opcode: uint16(wayland.OutputEventDone)}
228 op.SetID(output)
229 registerWireProxy(c, op)
230 global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1")
231 if !ok {
232 t.Fatal("missing xdg output manager")
233 }
234 manager, err := bindWireID(c, global.Name, global.Interface, 3)
235 if err != nil {
236 t.Fatal(err)
237 }
238 registerProtocol(t, c, manager)
239 if err := c.Roundtrip(); err != nil {
240 t.Fatal(err)
241 }
242 op.done = 0
243 xdg := c.AllocateID()
244 xp := &doneProxy{opcode: 2}
245 xp.SetID(xdg)
246 registerWireProxy(c, xp)
247 requestProtocol(t, c, manager, 1, xdg, output)
248 if err := c.Roundtrip(); err != nil {
249 t.Fatal(err)
250 }
251 if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) {
252 t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done)
253 }
254 })
255 }
256}
257
258type layerPointerProxy struct {
259 wlturbo.BaseProxy
260 enters chan [2]float64
261 buttons chan uint32 // serials
262}
263
264func (p *layerPointerProxy) Dispatch(e *wlturbo.Event) {
265 switch e.Opcode {
266 case uint16(wayland.PointerEventEnter):
267 _, _ = e.Uint32(), e.Uint32()
268 p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()}
269 case uint16(wayland.PointerEventButton):
270 p.buttons <- e.Uint32()
271 }
272}
273
274// A bar gets the pointer, and a menu it opens on a click through
275// get_popup reaches core with the bar as its parent and a grab.
276func TestLayerPointerAndPopup(t *testing.T) {
277 s, events, commands, dir := lifecycleServer(t)
278 c := protocolClient(t, s, dir)
279 seat := bindVersion(t, c, "wl_seat", 8)
280 registerProtocol(t, c, seat)
281 pointer := c.AllocateID()
282 pp := &layerPointerProxy{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)}
283 pp.SetID(pointer)
284 registerWireProxy(c, pp)
285 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
286 comp := bindProtocol(t, c, "wl_compositor")
287 wm := bindProtocol(t, c, "xdg_wm_base")
288 shm := bindProtocol(t, c, "wl_shm")
289 registerProtocol(t, c, shm)
290 fd, err := unix.MemfdCreate("layer-popup-buffer", 0)
291 if err != nil {
292 t.Fatal(err)
293 }
294 defer unix.Close(fd)
295 if err = unix.Ftruncate(fd, 4); err != nil {
296 t.Fatal(err)
297 }
298 pool, buf := c.AllocateID(), c.AllocateID()
299 registerProtocol(t, c, pool)
300 registerProtocol(t, c, buf)
301 if err = wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
302 t.Fatal(err)
303 }
304 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
305
306 surf, layer := layerProtocol(t, c)
307 lp := &layerProxy{configured: make(chan [3]uint32, 2)}
308 lp.SetID(layer)
309 registerWireProxy(c, lp)
310 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
311 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
312 if err = c.Roundtrip(); err != nil {
313 t.Fatal(err)
314 }
315 var config [3]uint32
316 select {
317 case config = <-lp.configured:
318 case <-time.After(2 * time.Second):
319 t.Fatal("configure timeout")
320 }
321 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0])
322 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
323 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
324 if err = c.Roundtrip(); err != nil {
325 t.Fatal(err)
326 }
327 lc, ok := waitEvent(t, events, 2*time.Second).(ports.LayerChanged)
328 if !ok || len(lc.Layers) != 1 {
329 t.Fatalf("layer mapped %+v", lc)
330 }
331 id := lc.Layers[0].ID
332
333 commands <- ports.PointerFocus{ID: id, X: 0.5, Y: 0.25}
334 commands <- ports.PointerButtonTo{ID: id, Button: 0x110, Pressed: true}
335 var press uint32
336 for deadline := time.Now().Add(2 * time.Second); len(pp.buttons) == 0; time.Sleep(5 * time.Millisecond) {
337 if time.Now().After(deadline) {
338 t.Fatal("pointer events timeout")
339 }
340 if err = c.Roundtrip(); err != nil {
341 t.Fatal(err)
342 }
343 }
344 if at := <-pp.enters; at != [2]float64{0.5, 0.25} {
345 t.Fatalf("enter at %v", at)
346 }
347 press = <-pp.buttons
348
349 positioner := c.AllocateID()
350 requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
351 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
352 requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
353 done := make(chan struct{}, 1)
354 xdgWindow(t, c, comp, wm, buf, func(pxdg uint32) {
355 popup := c.AllocateID()
356 proxy := &popupDoneProxy{done: done}
357 proxy.SetID(popup)
358 registerWireProxy(c, proxy)
359 requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner)
360 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestGetPopup, popup)
361 requestProtocol(t, c, popup, xdgshell.PopupRequestGrab, seat, press)
362 })
363 req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
364 if !ok || req.Parent != id || !req.Grab {
365 t.Fatalf("popup request %+v, want parent %d with a grab", req, id)
366 }
367 if len(done) != 0 {
368 t.Fatal("layer popup dismissed")
369 }
370}