1//go:build linux
2
3package client_test
4
5import (
6 "bytes"
7 "encoding/binary"
8 "errors"
9 "os"
10 "reflect"
11 "testing"
12 "time"
13
14 "github.com/bnema/go-wayland-bindings/client/inputmethod"
15 kdeserverdecoration "github.com/bnema/go-wayland-bindings/client/serverdecoration"
16 "github.com/bnema/go-wayland-bindings/client/textinput"
17 "github.com/bnema/go-wayland-bindings/client/virtualkeyboard"
18 "github.com/bnema/go-wayland-bindings/client/wayland"
19 foreigntoplevel "github.com/bnema/go-wayland-bindings/client/wlrforeigntoplevelmanagement"
20 layershell "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
21 outputmanagement "github.com/bnema/go-wayland-bindings/client/wlroutputmanagement"
22 outputpower "github.com/bnema/go-wayland-bindings/client/wlroutputpowermanagement"
23 screencopy "github.com/bnema/go-wayland-bindings/client/wlrscreencopy"
24 "github.com/bnema/go-wayland-bindings/client/xdgshell"
25 "github.com/bnema/wlturbo"
26 "golang.org/x/sys/unix"
27)
28
29// The output head, its modes and configuration objects are all created by
30// server events (new_id in an event) or nested client requests.
31func TestWlrOutputManagementHeadNewID(t *testing.T) {
32 s := newWireEnv(t)
33 mgr := outputmanagement.NewOutputManager(s.d.Context())
34 s.bind(1, outputmanagement.OutputManagerInterface, 4, 4, mgr)
35
36 var head *outputmanagement.OutputHead
37 mgr.OnHead(func(h *outputmanagement.OutputHead) { head = h })
38 var serial uint32
39 mgr.OnDone(func(v uint32) { serial = v })
40 s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
41 if head == nil || head.ID() != serverID+1 || head.Version() != mgr.Version() || head.Version() != 4 {
42 t.Fatalf("head %+v", head)
43 }
44
45 var name string
46 var mode *outputmanagement.OutputMode
47 var current uint32
48 var scale wlturbo.Fixed
49 var enabled int32
50 head.OnName(func(v string) { name = v })
51 head.OnMode(func(m *outputmanagement.OutputMode) { mode = m })
52 head.OnCurrentMode(func(id uint32) { current = id })
53 head.OnScale(func(v wlturbo.Fixed) { scale = v })
54 head.OnEnabled(func(v int32) { enabled = v })
55 s.event(head.ID(), 0, str("HDMI-A-1")...)
56 s.event(head.ID(), 3, wordBytes(serverID+2)...)
57 if mode == nil || mode.ID() != serverID+2 || mode.Version() != 4 {
58 t.Fatalf("mode %+v", mode)
59 }
60 var w, h, refresh int32
61 var preferred bool
62 mode.OnSize(func(a, b int32) { w, h = a, b })
63 mode.OnRefresh(func(v int32) { refresh = v })
64 mode.OnPreferred(func() { preferred = true })
65 s.event(mode.ID(), 0, wordBytes(1920, 1080)...)
66 s.event(mode.ID(), 1, wordBytes(60000)...)
67 s.event(mode.ID(), 2)
68 s.event(head.ID(), 4, wordBytes(1)...)
69 s.event(head.ID(), 5, wordBytes(mode.ID())...)
70 s.event(head.ID(), 8, wordBytes(uint32(wlturbo.NewFixed(1.5)))...)
71 s.event(mgr.ID(), 1, wordBytes(41)...)
72 if name != "HDMI-A-1" || w != 1920 || h != 1080 || refresh != 60000 || !preferred ||
73 enabled != 1 || current != mode.ID() || scale.Float64() != 1.5 || serial != 41 {
74 t.Fatal(name, w, h, refresh, preferred, enabled, current, scale, serial)
75 }
76
77 // create_configuration -> enable_head (new_id + object) -> setters -> apply.
78 cfg, e := mgr.CreateConfiguration(serial)
79 noerr(t, e)
80 wantWords(t, "create_configuration", s.req(mgr.ID(), 0), cfg.ID(), 41)
81 ch, e := cfg.EnableHead(head)
82 noerr(t, e)
83 wantWords(t, "enable_head", s.req(cfg.ID(), 0), ch.ID(), head.ID())
84 noerr(t, ch.SetMode(mode))
85 wantWords(t, "set_mode", s.req(ch.ID(), 0), mode.ID())
86 noerr(t, ch.SetPosition(-10, 20))
87 wantWords(t, "set_position", s.req(ch.ID(), 2), 0xfffffff6, 20)
88 noerr(t, ch.SetTransform(wayland.TRANSFORM_90))
89 wantWords(t, "set_transform", s.req(ch.ID(), 3), wayland.TRANSFORM_90)
90 noerr(t, ch.SetScale(wlturbo.NewFixed(2)))
91 wantWords(t, "set_scale", s.req(ch.ID(), 4), uint32(wlturbo.NewFixed(2)))
92 noerr(t, ch.SetAdaptiveSync(outputmanagement.ADAPTIVE_SYNC_STATE_ENABLED))
93 wantWords(t, "set_adaptive_sync", s.req(ch.ID(), 5), 1)
94 noerr(t, cfg.Test())
95 s.req(cfg.ID(), 3)
96 noerr(t, cfg.Apply())
97 s.req(cfg.ID(), 2)
98 var results []string
99 cfg.OnSucceeded(func() { results = append(results, "succeeded") })
100 cfg.OnFailed(func() { results = append(results, "failed") })
101 cfg.OnCancelled(func() { results = append(results, "cancelled") })
102 s.event(cfg.ID(), 0)
103 s.event(cfg.ID(), 1)
104 s.event(cfg.ID(), 2)
105 if !reflect.DeepEqual(results, []string{"succeeded", "failed", "cancelled"}) {
106 t.Fatal(results)
107 }
108 noerr(t, cfg.Destroy())
109 s.req(cfg.ID(), 4)
110
111 // Release destructors (since 3) and manager finished (destructor event).
112 var headFinished bool
113 head.OnFinished(func() { headFinished = true })
114 s.event(head.ID(), 9)
115 if !headFinished {
116 t.Fatal("head finished not delivered")
117 }
118 noerr(t, mode.Release())
119 s.req(mode.ID(), 0)
120 noerr(t, head.Release())
121 s.req(head.ID(), 0)
122 noerr(t, mgr.Stop())
123 s.req(mgr.ID(), 1)
124 var finished bool
125 mgr.OnFinished(func() { finished = true })
126 s.event(mgr.ID(), 2)
127 if !finished {
128 t.Fatal("finished not delivered")
129 }
130 if e := mgr.Stop(); e == nil {
131 t.Fatal("request on a finished manager must fail")
132 }
133 s.noRequest()
134}
135
136func TestWlrOutputManagementVersionGate(t *testing.T) {
137 s := newWireEnv(t)
138 mgr := outputmanagement.NewOutputManager(s.d.Context())
139 s.bind(1, outputmanagement.OutputManagerInterface, 4, 2, mgr)
140 var head *outputmanagement.OutputHead
141 mgr.OnHead(func(h *outputmanagement.OutputHead) { head = h })
142 s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
143 if head == nil || head.Version() != 2 {
144 t.Fatalf("head %+v", head)
145 }
146 if e := head.Release(); !errors.Is(e, wlturbo.ErrVersionTooLow) {
147 t.Fatalf("release on v2 head: %v", e)
148 }
149 s.noRequest()
150}
151
152func TestWlrScreencopyBufferFlow(t *testing.T) {
153 s := newWireEnv(t)
154 out := s.output()
155 shm := wayland.NewShm(s.d.Context())
156 s.bind(1, wayland.ShmInterface, 1, 1, shm)
157 mgr := screencopy.NewScreencopyManager(s.d.Context())
158 s.bind(2, screencopy.ScreencopyManagerInterface, 3, 3, mgr)
159
160 frame, e := mgr.CaptureOutput(1, out)
161 noerr(t, e)
162 wantWords(t, "capture_output", s.req(mgr.ID(), 0), frame.ID(), 1, out.ID())
163 if frame.Version() != 3 {
164 t.Fatalf("frame version %d", frame.Version())
165 }
166 var events []string
167 var shmW, shmH, shmStride, shmFormat uint32
168 frame.OnBuffer(func(f, w, h, st uint32) {
169 shmFormat, shmW, shmH, shmStride = f, w, h, st
170 events = append(events, "buffer")
171 })
172 frame.OnLinuxDmabuf(func(f, w, h uint32) { events = append(events, "dmabuf") })
173 frame.OnBufferDone(func() { events = append(events, "done") })
174 frame.OnFlags(func(f uint32) {
175 if f != screencopy.FLAGS_Y_INVERT {
176 t.Fatalf("flags %d", f)
177 }
178 events = append(events, "flags")
179 })
180 var sec uint64
181 var nsec uint32
182 frame.OnReady(func(hi, lo, ns uint32) { sec, nsec = uint64(hi)<<32|uint64(lo), ns; events = append(events, "ready") })
183 s.event(frame.ID(), 0, wordBytes(wayland.FORMAT_XRGB8888, 4, 2, 16)...)
184 s.event(frame.ID(), 5, wordBytes(0x34325258, 4, 2)...)
185 s.event(frame.ID(), 6)
186 if shmFormat != wayland.FORMAT_XRGB8888 || shmW != 4 || shmH != 2 || shmStride != 16 {
187 t.Fatal(shmFormat, shmW, shmH, shmStride)
188 }
189
190 // Build the wl_shm buffer the compositor asked for; the pool descriptor is
191 // duplicated by the kernel and the client's copy is closed by the binding.
192 memfd, e := unix.MemfdCreate("wlturbo-shm", unix.MFD_CLOEXEC)
193 noerr(t, e)
194 noerr(t, unix.Ftruncate(memfd, 32))
195 pool, e := shm.CreatePool(memfd, 32)
196 noerr(t, e)
197 requireFDClosed(t, memfd)
198 body, fd := s.reqFD(shm.ID(), 0)
199 unix.Close(fd)
200 wantWords(t, "create_pool", body, pool.ID(), 32)
201 buf, e := pool.CreateBuffer(0, 4, 2, 16, wayland.FORMAT_XRGB8888)
202 noerr(t, e)
203 wantWords(t, "create_buffer", s.req(pool.ID(), 0), buf.ID(), 0, 4, 2, 16, wayland.FORMAT_XRGB8888)
204
205 noerr(t, frame.Copy(buf))
206 wantWords(t, "copy", s.req(frame.ID(), 0), buf.ID())
207 s.event(frame.ID(), 1, wordBytes(screencopy.FLAGS_Y_INVERT)...)
208 s.event(frame.ID(), 2, wordBytes(1, 2, 999999999)...)
209 if sec != 1<<32|2 || nsec != 999999999 {
210 t.Fatal(sec, nsec)
211 }
212 want := []string{"buffer", "dmabuf", "done", "flags", "ready"}
213 if !reflect.DeepEqual(events, want) {
214 t.Fatalf("events %v, want %v", events, want)
215 }
216 noerr(t, frame.Destroy())
217 s.req(frame.ID(), 1)
218
219 // Region capture + damage + failure on a second frame.
220 f2, e := mgr.CaptureOutputRegion(0, out, -1, 2, 30, 40)
221 noerr(t, e)
222 wantWords(t, "capture_output_region", s.req(mgr.ID(), 1), f2.ID(), 0, out.ID(), 0xffffffff, 2, 30, 40)
223 noerr(t, f2.CopyWithDamage(buf))
224 wantWords(t, "copy_with_damage", s.req(f2.ID(), 2), buf.ID())
225 var damage []uint32
226 var failed bool
227 f2.OnDamage(func(x, y, w, h uint32) { damage = []uint32{x, y, w, h} })
228 f2.OnFailed(func() { failed = true })
229 s.event(f2.ID(), 4, wordBytes(1, 2, 3, 4)...)
230 s.event(f2.ID(), 3)
231 if !reflect.DeepEqual(damage, []uint32{1, 2, 3, 4}) || !failed {
232 t.Fatal(damage, failed)
233 }
234 noerr(t, f2.Destroy())
235 s.req(f2.ID(), 1)
236 noerr(t, buf.Destroy())
237 s.req(buf.ID(), 0)
238 noerr(t, mgr.Destroy())
239 s.req(mgr.ID(), 2)
240}
241
242func TestWlrScreencopyVersionGate(t *testing.T) {
243 s := newWireEnv(t)
244 out := s.output()
245 mgr := screencopy.NewScreencopyManager(s.d.Context())
246 s.bind(1, screencopy.ScreencopyManagerInterface, 3, 1, mgr)
247 frame, e := mgr.CaptureOutput(0, out)
248 noerr(t, e)
249 s.req(mgr.ID(), 0)
250 buf := wayland.NewBuffer(s.d.Context())
251 buf.SetID(s.d.AllocateID())
252 s.d.Context().Register(buf)
253 if e := frame.CopyWithDamage(buf); !errors.Is(e, wlturbo.ErrVersionTooLow) {
254 t.Fatalf("copy_with_damage on v1 frame: %v", e)
255 }
256 s.noRequest()
257 noerr(t, frame.Copy(buf))
258 s.req(frame.ID(), 0)
259}
260
261func TestWlrInputMethodFDAndCrossPackageEnums(t *testing.T) {
262 s := newWireEnv(t)
263 seat, surf := s.seat(), s.surface()
264 mgr := inputmethod.NewInputMethodManager(s.d.Context())
265 s.bind(1, inputmethod.InputMethodManagerInterface, 1, 1, mgr)
266 im, e := mgr.GetInputMethod(seat)
267 noerr(t, e)
268 wantWords(t, "get_input_method", s.req(mgr.ID(), 0), seat.ID(), im.ID())
269
270 var seq []string
271 var text string
272 var cursor, anchor, cause, hint, purpose uint32
273 im.OnActivate(func() { seq = append(seq, "activate") })
274 im.OnSurroundingText(func(tx string, c, a uint32) { text, cursor, anchor = tx, c, a; seq = append(seq, "surrounding") })
275 im.OnTextChangeCause(func(c uint32) { cause = c; seq = append(seq, "cause") })
276 im.OnContentType(func(h, p uint32) { hint, purpose = h, p; seq = append(seq, "content") })
277 im.OnDone(func() { seq = append(seq, "done") })
278 s.event(im.ID(), 0)
279 s.event(im.ID(), 2, cat(str("héllo"), wordBytes(3, 3))...)
280 s.event(im.ID(), 3, wordBytes(textinput.CHANGE_CAUSE_OTHER)...)
281 s.event(im.ID(), 4, wordBytes(textinput.CONTENT_HINT_SPELLCHECK, textinput.CONTENT_PURPOSE_EMAIL)...)
282 s.event(im.ID(), 5)
283 if !reflect.DeepEqual(seq, []string{"activate", "surrounding", "cause", "content", "done"}) ||
284 text != "héllo" || cursor != 3 || anchor != 3 || cause != textinput.CHANGE_CAUSE_OTHER ||
285 hint != textinput.CONTENT_HINT_SPELLCHECK || purpose != textinput.CONTENT_PURPOSE_EMAIL {
286 t.Fatal(seq, text, cursor, anchor, cause, hint, purpose)
287 }
288
289 noerr(t, im.CommitString("ok"))
290 if b := s.req(im.ID(), 0); !bytes.Equal(b, str("ok")) {
291 t.Fatalf("commit_string %x", b)
292 }
293 noerr(t, im.SetPreeditString("pre", -1, -1))
294 if b := s.req(im.ID(), 1); !bytes.Equal(b, cat(str("pre"), wordBytes(0xffffffff, 0xffffffff))) {
295 t.Fatalf("set_preedit_string %x", b)
296 }
297 noerr(t, im.DeleteSurroundingText(2, 1))
298 wantWords(t, "delete_surrounding_text", s.req(im.ID(), 2), 2, 1)
299 noerr(t, im.Commit(1))
300 wantWords(t, "commit", s.req(im.ID(), 3), 1)
301
302 popup, e := im.GetInputPopupSurface(surf)
303 noerr(t, e)
304 wantWords(t, "get_input_popup_surface", s.req(im.ID(), 4), popup.ID(), surf.ID())
305 var rect []int32
306 popup.OnTextInputRectangle(func(x, y, w, h int32) { rect = []int32{x, y, w, h} })
307 s.event(popup.ID(), 0, wordBytes(0xfffffffe, 5, 30, 12)...)
308 if !reflect.DeepEqual(rect, []int32{-2, 5, 30, 12}) {
309 t.Fatal(rect)
310 }
311
312 grab, e := im.GrabKeyboard()
313 noerr(t, e)
314 wantWords(t, "grab_keyboard", s.req(im.ID(), 5), grab.ID())
315
316 // keymap carries a descriptor: the first handler may take ownership, the
317 // second sees nil, and an unclaimed descriptor is closed by the transport.
318 var claim = true
319 var first, second []*wlturbo.OwnedFD
320 var format, size uint32
321 var got []byte
322 grab.OnKeymap(func(f uint32, fd *wlturbo.OwnedFD, sz uint32) {
323 first = append(first, fd)
324 format, size = f, sz
325 if !claim {
326 return
327 }
328 n, e := fd.Take()
329 noerr(t, e)
330 buf := make([]byte, 16)
331 m, e := unix.Read(n, buf)
332 noerr(t, e)
333 got = buf[:m]
334 unix.Close(n)
335 })
336 grab.OnKeymap(func(f uint32, fd *wlturbo.OwnedFD, sz uint32) { second = append(second, fd) })
337 pr, pw, e := os.Pipe()
338 noerr(t, e)
339 _, e = pw.Write([]byte("xkb-keymap"))
340 noerr(t, e)
341 send(t, s.p, payload(grab.ID(), 0, wordBytes(wayland.KEYMAP_FORMAT_XKB_V1, 10)...), int(pr.Fd()))
342 pr.Close()
343 pw.Close()
344 s.dispatch()
345 if string(got) != "xkb-keymap" || format != wayland.KEYMAP_FORMAT_XKB_V1 || size != 10 || len(second) != 1 || second[0] != nil {
346 t.Fatalf("keymap got=%q format=%d size=%d second=%v", got, format, size, second)
347 }
348
349 claim = false
350 pr2, pw2, e := os.Pipe()
351 noerr(t, e)
352 defer pr2.Close()
353 send(t, s.p, payload(grab.ID(), 0, wordBytes(wayland.KEYMAP_FORMAT_XKB_V1, 0)...), int(pw2.Fd()))
354 pw2.Close() // only the in-flight duplicate keeps the write end alive
355 s.dispatch()
356 pr2.SetReadDeadline(time.Now().Add(2 * time.Second))
357 if n, e := pr2.Read(make([]byte, 4)); n != 0 || e == nil {
358 t.Fatalf("unclaimed descriptor still open: n=%d err=%v", n, e)
359 }
360
361 var key, mods, repeat []uint32
362 grab.OnKey(func(serial, tm, k, st uint32) { key = []uint32{serial, tm, k, st} })
363 grab.OnModifiers(func(serial, d, l, lk, g uint32) { mods = []uint32{serial, d, l, lk, g} })
364 grab.OnRepeatInfo(func(rate, delay int32) { repeat = []uint32{uint32(rate), uint32(delay)} })
365 s.event(grab.ID(), 1, wordBytes(7, 100, 30, wayland.KEY_STATE_PRESSED)...)
366 s.event(grab.ID(), 2, wordBytes(8, 1, 2, 4, 1)...)
367 s.event(grab.ID(), 3, wordBytes(25, 600)...)
368 if !reflect.DeepEqual(key, []uint32{7, 100, 30, wayland.KEY_STATE_PRESSED}) ||
369 !reflect.DeepEqual(mods, []uint32{8, 1, 2, 4, 1}) || !reflect.DeepEqual(repeat, []uint32{25, 600}) {
370 t.Fatal(key, mods, repeat)
371 }
372
373 noerr(t, grab.Release())
374 s.req(grab.ID(), 0)
375 noerr(t, popup.Destroy())
376 s.req(popup.ID(), 0)
377 var unavailable bool
378 im.OnUnavailable(func() { unavailable = true })
379 s.event(im.ID(), 6)
380 if !unavailable {
381 t.Fatal("unavailable not delivered")
382 }
383 noerr(t, im.Destroy())
384 s.req(im.ID(), 6)
385 noerr(t, mgr.Destroy())
386 s.req(mgr.ID(), 1)
387}
388
389func TestWlrVirtualKeyboardKeymapFD(t *testing.T) {
390 s := newWireEnv(t)
391 seat := s.seat()
392 mgr := virtualkeyboard.NewVirtualKeyboardManager(s.d.Context())
393 s.bind(1, virtualkeyboard.VirtualKeyboardManagerInterface, 1, 1, mgr)
394 kb, e := mgr.CreateVirtualKeyboard(seat)
395 noerr(t, e)
396 wantWords(t, "create_virtual_keyboard", s.req(mgr.ID(), 0), seat.ID(), kb.ID())
397
398 r, w, e := os.Pipe()
399 noerr(t, e)
400 defer r.Close()
401 // Hand the write end to the binding as a plain descriptor.
402 fd, e := unix.Dup(int(w.Fd()))
403 noerr(t, e)
404 w.Close()
405 noerr(t, kb.Keymap(wayland.KEYMAP_FORMAT_XKB_V1, fd, 6))
406 requireFDClosed(t, fd)
407 body, got := s.reqFD(kb.ID(), 0)
408 wantWords(t, "keymap", body, wayland.KEYMAP_FORMAT_XKB_V1, 6)
409 _, e = unix.Write(got, []byte("keymap"))
410 noerr(t, e)
411 unix.Close(got)
412 buf := make([]byte, 16)
413 r.SetReadDeadline(time.Now().Add(2 * time.Second))
414 n, e := r.Read(buf)
415 noerr(t, e)
416 if string(buf[:n]) != "keymap" {
417 t.Fatalf("keymap payload %q", buf[:n])
418 }
419
420 noerr(t, kb.Key(1234, 30, wayland.KEY_STATE_PRESSED))
421 wantWords(t, "key", s.req(kb.ID(), 1), 1234, 30, wayland.KEY_STATE_PRESSED)
422 noerr(t, kb.Modifiers(1, 2, 4, 0))
423 wantWords(t, "modifiers", s.req(kb.ID(), 2), 1, 2, 4, 0)
424 noerr(t, kb.Destroy())
425 s.req(kb.ID(), 3)
426 if e := kb.Key(1, 1, 0); e == nil {
427 t.Fatal("request after destroy must fail")
428 }
429 s.noRequest()
430}
431
432// A layer surface is a wl_surface role defined in layershell, and its popup is
433// an xdg_popup from xdgshell; both packages must agree on the same objects.
434func TestWlrLayerShellPopupCrossPackage(t *testing.T) {
435 s := newWireEnv(t)
436 panel, popupSurf := s.surface(), s.surface()
437 out := s.output()
438
439 wm := xdgshell.NewXdgWmBase(s.d.Context())
440 s.bind(1, xdgshell.XdgWmBaseInterface, 6, 6, wm)
441 ls := layershell.NewLayerShell(s.d.Context())
442 s.bind(2, layershell.LayerShellInterface, 5, 5, ls)
443
444 surf, e := ls.GetLayerSurface(panel, out, layershell.LAYER_TOP, "panel")
445 noerr(t, e)
446 if b := s.req(ls.ID(), 0); !bytes.Equal(b, cat(wordBytes(surf.ID(), panel.ID(), out.ID(), layershell.LAYER_TOP), str("panel"))) {
447 t.Fatalf("get_layer_surface %x", b)
448 }
449 if surf.Version() != 5 {
450 t.Fatalf("layer surface version %d", surf.Version())
451 }
452 // A nullable output is encoded as object 0.
453 nullOut, e := ls.GetLayerSurface(s.surface(), nil, layershell.LAYER_BACKGROUND, "wallpaper")
454 noerr(t, e)
455 if b := s.req(ls.ID(), 0); binary.NativeEndian.Uint32(b[8:]) != 0 || binary.NativeEndian.Uint32(b[:4]) != nullOut.ID() {
456 t.Fatalf("null output %x", b)
457 }
458
459 anchors := uint32(layershell.ANCHOR_TOP | layershell.ANCHOR_LEFT | layershell.ANCHOR_RIGHT)
460 noerr(t, surf.SetSize(0, 30))
461 wantWords(t, "set_size", s.req(surf.ID(), 0), 0, 30)
462 noerr(t, surf.SetAnchor(anchors))
463 wantWords(t, "set_anchor", s.req(surf.ID(), 1), anchors)
464 noerr(t, surf.SetExclusiveZone(-1))
465 wantWords(t, "set_exclusive_zone", s.req(surf.ID(), 2), 0xffffffff)
466 noerr(t, surf.SetMargin(1, 2, 3, 4))
467 wantWords(t, "set_margin", s.req(surf.ID(), 3), 1, 2, 3, 4)
468 noerr(t, surf.SetKeyboardInteractivity(layershell.KEYBOARD_INTERACTIVITY_ON_DEMAND))
469 wantWords(t, "set_keyboard_interactivity", s.req(surf.ID(), 4), layershell.KEYBOARD_INTERACTIVITY_ON_DEMAND)
470 noerr(t, surf.SetLayer(layershell.LAYER_OVERLAY))
471 wantWords(t, "set_layer", s.req(surf.ID(), 8), layershell.LAYER_OVERLAY)
472 noerr(t, surf.SetExclusiveEdge(layershell.ANCHOR_TOP))
473 wantWords(t, "set_exclusive_edge", s.req(surf.ID(), 9), layershell.ANCHOR_TOP)
474
475 var cfgSerial, cfgW, cfgH uint32
476 surf.OnConfigure(func(serial, w, h uint32) {
477 cfgSerial, cfgW, cfgH = serial, w, h
478 noerr(t, surf.AckConfigure(serial))
479 })
480 s.event(surf.ID(), 0, wordBytes(77, 1920, 30)...)
481 if cfgSerial != 77 || cfgW != 1920 || cfgH != 30 {
482 t.Fatal(cfgSerial, cfgW, cfgH)
483 }
484 wantWords(t, "ack_configure", s.req(surf.ID(), 6), 77)
485
486 // The popup is created through xdg_shell with a NULL parent, then adopted
487 // by the layer surface.
488 xs, e := wm.GetXdgSurface(popupSurf)
489 noerr(t, e)
490 s.req(wm.ID(), 2)
491 pos, e := wm.CreatePositioner()
492 noerr(t, e)
493 s.req(wm.ID(), 1)
494 noerr(t, pos.SetSize(100, 50))
495 wantWords(t, "positioner size", s.req(pos.ID(), 1), 100, 50)
496 noerr(t, pos.SetAnchorRect(0, 0, 10, 30))
497 s.req(pos.ID(), 2)
498 popup, e := xs.GetPopup(nil, pos)
499 noerr(t, e)
500 wantWords(t, "get_popup", s.req(xs.ID(), 2), popup.ID(), 0, pos.ID())
501 noerr(t, surf.GetPopup(popup))
502 wantWords(t, "layer get_popup", s.req(surf.ID(), 5), popup.ID())
503
504 var popupCfg []int32
505 var done bool
506 popup.OnConfigure(func(x, y, w, h int32) { popupCfg = []int32{x, y, w, h} })
507 popup.OnPopupDone(func() { done = true })
508 s.event(popup.ID(), 0, wordBytes(4, 30, 100, 50)...)
509 s.event(popup.ID(), 1)
510 if !reflect.DeepEqual(popupCfg, []int32{4, 30, 100, 50}) || !done {
511 t.Fatal(popupCfg, done)
512 }
513
514 var closed bool
515 surf.OnClosed(func() { closed = true })
516 s.event(surf.ID(), 1)
517 if !closed {
518 t.Fatal("closed not delivered")
519 }
520 noerr(t, popup.Destroy())
521 s.req(popup.ID(), 0)
522 noerr(t, surf.Destroy())
523 s.req(surf.ID(), 7)
524 noerr(t, ls.Destroy())
525 s.req(ls.ID(), 1)
526 if e := surf.SetSize(1, 1); e == nil {
527 t.Fatal("request after destroy must fail")
528 }
529 s.noRequest()
530}
531
532func TestWlrLayerShellVersionGate(t *testing.T) {
533 s := newWireEnv(t)
534 ls := layershell.NewLayerShell(s.d.Context())
535 s.bind(1, layershell.LayerShellInterface, 5, 4, ls)
536 surf, e := ls.GetLayerSurface(s.surface(), nil, layershell.LAYER_TOP, "x")
537 noerr(t, e)
538 s.req(ls.ID(), 0)
539 if e := surf.SetExclusiveEdge(layershell.ANCHOR_TOP); !errors.Is(e, wlturbo.ErrVersionTooLow) {
540 t.Fatalf("set_exclusive_edge on v4: %v", e)
541 }
542 s.noRequest()
543 noerr(t, surf.SetLayer(layershell.LAYER_BOTTOM))
544 s.req(surf.ID(), 8)
545}
546
547func TestWlrForeignToplevelOutputPowerAndKDEDecoration(t *testing.T) {
548 s := newWireEnv(t)
549 seat, surf, out := s.seat(), s.surface(), s.output()
550
551 // foreign-toplevel: toplevel is a server new_id event; finished destroys.
552 tm := foreigntoplevel.NewForeignToplevelManager(s.d.Context())
553 s.bind(1, foreigntoplevel.ForeignToplevelManagerInterface, 3, 3, tm)
554 var top *foreigntoplevel.ForeignToplevelHandle
555 tm.OnToplevel(func(h *foreigntoplevel.ForeignToplevelHandle) { top = h })
556 s.event(tm.ID(), 0, wordBytes(serverID+1)...)
557 if top == nil || top.ID() != serverID+1 || top.Version() != 3 {
558 t.Fatalf("toplevel %+v", top)
559 }
560 var title, appID string
561 var states []byte
562 var enter, parent uint32
563 var done, closed bool
564 top.OnTitle(func(v string) { title = v })
565 top.OnAppId(func(v string) { appID = v })
566 top.OnState(func(v []byte) { states = append([]byte(nil), v...) })
567 top.OnOutputEnter(func(id uint32) { enter = id })
568 top.OnParent(func(id uint32) { parent = id })
569 top.OnDone(func() { done = true })
570 top.OnClosed(func() { closed = true })
571 s.event(top.ID(), 0, str("term")...)
572 s.event(top.ID(), 1, str("org.example.term")...)
573 s.event(top.ID(), 2, wordBytes(out.ID())...)
574 s.event(top.ID(), 4, arr(wordBytes(foreigntoplevel.STATE_MAXIMIZED, foreigntoplevel.STATE_ACTIVATED))...)
575 s.event(top.ID(), 7, wordBytes(0)...)
576 s.event(top.ID(), 5)
577 if title != "term" || appID != "org.example.term" || enter != out.ID() || parent != 0 || !done ||
578 !bytes.Equal(states, wordBytes(foreigntoplevel.STATE_MAXIMIZED, foreigntoplevel.STATE_ACTIVATED)) {
579 t.Fatal(title, appID, enter, parent, done, states)
580 }
581 noerr(t, top.SetMaximized())
582 s.req(top.ID(), 0)
583 noerr(t, top.Activate(seat))
584 wantWords(t, "activate", s.req(top.ID(), 4), seat.ID())
585 noerr(t, top.SetRectangle(surf, 1, 2, 3, 4))
586 wantWords(t, "set_rectangle", s.req(top.ID(), 6), surf.ID(), 1, 2, 3, 4)
587 noerr(t, top.SetFullscreen(nil))
588 wantWords(t, "set_fullscreen(nil)", s.req(top.ID(), 8), 0)
589 noerr(t, top.SetFullscreen(out))
590 wantWords(t, "set_fullscreen", s.req(top.ID(), 8), out.ID())
591 noerr(t, top.Close())
592 s.req(top.ID(), 5)
593 s.event(top.ID(), 6)
594 if !closed {
595 t.Fatal("closed not delivered")
596 }
597 noerr(t, top.Destroy())
598 s.req(top.ID(), 7)
599 noerr(t, tm.Stop())
600 s.req(tm.ID(), 0)
601 s.event(tm.ID(), 1)
602 if e := tm.Stop(); e == nil {
603 t.Fatal("request on a finished manager must fail")
604 }
605
606 // output-power: object argument + mode/failed events.
607 pm := outputpower.NewOutputPowerManager(s.d.Context())
608 s.bind(2, outputpower.OutputPowerManagerInterface, 1, 1, pm)
609 pw, e := pm.GetOutputPower(out)
610 noerr(t, e)
611 wantWords(t, "get_output_power", s.req(pm.ID(), 0), pw.ID(), out.ID())
612 var modes []uint32
613 var failed bool
614 pw.OnMode(func(m uint32) { modes = append(modes, m) })
615 pw.OnFailed(func() { failed = true })
616 s.event(pw.ID(), 0, wordBytes(outputpower.MODE_ON)...)
617 noerr(t, pw.SetMode(outputpower.MODE_OFF))
618 wantWords(t, "set_mode", s.req(pw.ID(), 0), outputpower.MODE_OFF)
619 s.event(pw.ID(), 0, wordBytes(outputpower.MODE_OFF)...)
620 s.event(pw.ID(), 1)
621 if !reflect.DeepEqual(modes, []uint32{outputpower.MODE_ON, outputpower.MODE_OFF}) || !failed {
622 t.Fatal(modes, failed)
623 }
624 noerr(t, pw.Destroy())
625 s.req(pw.ID(), 1)
626 noerr(t, pm.Destroy())
627 s.req(pm.ID(), 1)
628
629 // KDE server decoration: mode enums are per-interface constants.
630 dm := kdeserverdecoration.NewOrgKdeKwinServerDecorationManager(s.d.Context())
631 s.bind(3, kdeserverdecoration.OrgKdeKwinServerDecorationManagerInterface, 1, 1, dm)
632 var def uint32 = 99
633 dm.OnDefaultMode(func(m uint32) { def = m })
634 s.event(dm.ID(), 0, wordBytes(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MANAGER_MODE_CLIENT)...)
635 if def != kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MANAGER_MODE_CLIENT {
636 t.Fatal(def)
637 }
638 dec, e := dm.Create(surf)
639 noerr(t, e)
640 wantWords(t, "create", s.req(dm.ID(), 0), dec.ID(), surf.ID())
641 var mode uint32 = 99
642 dec.OnMode(func(m uint32) { mode = m })
643 noerr(t, dec.RequestMode(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER))
644 wantWords(t, "request_mode", s.req(dec.ID(), 1), kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER)
645 s.event(dec.ID(), 0, wordBytes(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER)...)
646 if mode != kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER {
647 t.Fatal(mode)
648 }
649 noerr(t, dec.Release())
650 s.req(dec.ID(), 0)
651 s.noRequest()
652}