1package wayland
2
3import (
4 "slices"
5 "testing"
6
7 ext "github.com/bnema/go-wayland-bindings/server/extforeigntoplevellist"
8 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
9 "github.com/bnema/go-wayland-bindings/server/wayland"
10 xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/bnema/wlturbo"
13)
14
15// extListEvents registers a handle proxy for each announced toplevel.
16type extListEvents struct {
17 wlturbo.BaseProxy
18 client *wlturbo.Display
19 handles chan uint32
20 events chan string
21}
22
23func (p *extListEvents) Dispatch(e *wlturbo.Event) {
24 switch uint32(e.Opcode) {
25 case ext.ExtForeignToplevelListV1EventToplevel:
26 id := e.Uint32()
27 h := &extHandleEvents{events: p.events}
28 h.SetID(id)
29 registerWireProxy(p.client, h)
30 p.handles <- id
31 case ext.ExtForeignToplevelListV1EventFinished:
32 p.events <- "finished"
33 }
34}
35
36// extHandleEvents records handle events as text.
37type extHandleEvents struct {
38 wlturbo.BaseProxy
39 events chan string
40}
41
42func (p *extHandleEvents) Dispatch(e *wlturbo.Event) {
43 switch uint32(e.Opcode) {
44 case ext.ExtForeignToplevelHandleV1EventIdentifier:
45 p.events <- "identifier " + e.String()
46 case ext.ExtForeignToplevelHandleV1EventTitle:
47 p.events <- "title " + e.String()
48 case ext.ExtForeignToplevelHandleV1EventAppId:
49 p.events <- "app_id " + e.String()
50 case ext.ExtForeignToplevelHandleV1EventDone:
51 p.events <- "done"
52 case ext.ExtForeignToplevelHandleV1EventClosed:
53 p.events <- "closed"
54 }
55}
56
57// The ext list announces mapped windows with a stable identifier, sends
58// changes, and closed when a window unmaps.
59func TestExtForeignToplevelList(t *testing.T) {
60 s, events, _, dir := lifecycleServer(t)
61 c := protocolClient(t, s, dir)
62 w, _, xdg := surfaceMapper(t, c, events)()
63 registerProtocol(t, c, xdg)
64 top := xdg + 1
65 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor")
66 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetAppId, "app.editor")
67
68 list := bindProtocol(t, c, "ext_foreign_toplevel_list_v1")
69 p := &extListEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
70 p.SetID(list)
71 registerWireProxy(c, p)
72 want := []string{"identifier " + toplevelIdentifier(w.ID, 1), "title editor", "app_id app.editor", "done"}
73 if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
74 t.Fatalf("announce = %q, want %q", got, want)
75 }
76 <-p.handles
77 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor - file")
78 if got, want := takeEvents(t, c, p.events), []string{"title editor - file", "done"}; !slices.Equal(got, want) {
79 t.Fatalf("title = %q, want %q", got, want)
80 }
81
82 // After stop, a handle already sent still follows its window.
83 requestProtocol(t, c, list, ext.ExtForeignToplevelListV1RequestStop)
84 if err := c.Roundtrip(); err != nil {
85 t.Fatal(err)
86 }
87 if ev := <-p.events; ev != "finished" {
88 t.Fatalf("stop = %q", ev)
89 }
90 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor - other")
91 if got, want := takeEvents(t, c, p.events), []string{"title editor - other", "done"}; !slices.Equal(got, want) {
92 t.Fatalf("title after stop = %q, want %q", got, want)
93 }
94 requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy)
95 if got := takeEvents(t, c, p.events); !slices.Equal(got, []string{"closed"}) {
96 t.Fatalf("close = %q", got)
97 }
98}
99
100// A handle of the list makes a window capture source: its session opens on
101// the window in core, its frames ask for that window off screen, and the
102// session stops when the window goes.
103func TestWindowCaptureSource(t *testing.T) {
104 h := newCaptureHarness(t)
105 cc := h.client(t)
106 win, surf, xdg := surfaceMapper(t, cc.c, h.events)()
107 registerProtocol(t, cc.c, xdg)
108 h.send(ports.ConfigureWindow{ID: win.ID, Width: 3, Height: 2, Output: "HEADLESS-1", Visible: true})
109 h.settle(t, cc)
110
111 list := bindProtocol(t, cc.c, "ext_foreign_toplevel_list_v1")
112 p := &extListEvents{client: cc.c, handles: make(chan uint32, 4), events: make(chan string, 64)}
113 p.SetID(list)
114 registerWireProxy(cc.c, p)
115 takeEvents(t, cc.c, p.events)
116 handle := <-p.handles
117
118 mgr := bindProtocol(t, cc.c, "ext_foreign_toplevel_image_capture_source_manager_v1")
119 src := cc.c.AllocateID()
120 registerProtocol(t, cc.c, src)
121 requestProtocol(t, cc.c, mgr, source.ExtForeignToplevelImageCaptureSourceManagerV1RequestCreateSource, src, handle)
122 session, ev := cc.session(t, src)
123 if w, hh := constraints(t, cc, ev); w != 3 || hh != 2 {
124 t.Fatalf("buffer %dx%d, want the window's 3x2", w, hh)
125 }
126 open := h.open(t)
127 if open.Window != win.ID || open.Workspace != 0 || open.Output != "" {
128 t.Fatalf("open %+v", open)
129 }
130 h.send(ports.CaptureSessionState{ID: open.ID, Output: "HEADLESS-1", Rect: ports.Rect{W: 3, H: 2}, Hidden: true, Active: true})
131 h.settle(t, cc)
132 cc.extFrame(t, session, 3, 2)
133 r := receiveCapture(t, h.captures)
134 if r.Window != win.ID || !r.OffScreen || r.Region.Dx() != 3 || r.Region.Dy() != 2 {
135 t.Fatalf("request %+v", r)
136 }
137
138 // The window unmaps: the session stops at once.
139 requestProtocol(t, cc.c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
140 requestProtocol(t, cc.c, surf, wayland.SurfaceRequestCommit)
141 for {
142 if got := nextSession(t, cc, ev); got[0] == evStopped {
143 break
144 }
145 }
146 if ev := clientEvent[ports.WindowUnmapped](t, h.events); ev.ID != win.ID {
147 t.Fatalf("unmapped %d, want %d", ev.ID, win.ID)
148 }
149
150 // The remap is a new toplevel: a new identifier, and the source of the
151 // closed handle is dead.
152 serials := make(chan uint32, 8)
153 xp := &configureProxy{serial: serials}
154 xp.SetID(xdg)
155 registerWireProxy(cc.c, xp)
156 requestProtocol(t, cc.c, surf, wayland.SurfaceRequestCommit)
157 ackAndAttach(t, cc.c, surf, xdg, shmBuffer(t, cc.c), serials)
158 clientEvent[ports.WindowMapped](t, h.events)
159 if got := takeEvents(t, cc.c, p.events); !slices.Equal(got, []string{"closed"}) {
160 t.Fatalf("unmap = %q, want closed", got)
161 }
162 got := takeEvents(t, cc.c, p.events)
163 if !slices.Contains(got, "identifier "+toplevelIdentifier(win.ID, 2)) {
164 t.Fatalf("remap = %q", got)
165 }
166 _, ev = cc.session(t, src)
167 if got := nextSession(t, cc, ev); got[0] != evStopped {
168 t.Fatalf("session on a stale source: %v", got)
169 }
170}