1package wayland
2
3import (
4 "encoding/binary"
5 "fmt"
6 "slices"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 wlr "github.com/bnema/go-wayland-bindings/server/wlrforeigntoplevelmanagement"
12 "github.com/bnema/go-wayland-bindings/server/xdgshell"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/wlturbo"
15)
16
17// toplevelManagerEvents registers a handle proxy for each announced toplevel.
18type toplevelManagerEvents struct {
19 wlturbo.BaseProxy
20 client *wlturbo.Display
21 handles chan uint32
22 events chan string
23}
24
25func (p *toplevelManagerEvents) Dispatch(e *wlturbo.Event) {
26 if e.Opcode != uint16(wlr.ZwlrForeignToplevelManagerV1EventToplevel) {
27 return
28 }
29 id := e.Uint32()
30 h := &toplevelHandleEvents{events: p.events}
31 h.SetID(id)
32 registerWireProxy(p.client, h)
33 p.handles <- id
34}
35
36// toplevelHandleEvents records handle events as text.
37type toplevelHandleEvents struct {
38 wlturbo.BaseProxy
39 events chan string
40}
41
42func (p *toplevelHandleEvents) Dispatch(e *wlturbo.Event) {
43 switch uint32(e.Opcode) {
44 case wlr.ZwlrForeignToplevelHandleV1EventTitle:
45 p.events <- "title " + e.String()
46 case wlr.ZwlrForeignToplevelHandleV1EventAppId:
47 p.events <- "app_id " + e.String()
48 case wlr.ZwlrForeignToplevelHandleV1EventOutputEnter:
49 p.events <- "output_enter"
50 case wlr.ZwlrForeignToplevelHandleV1EventOutputLeave:
51 p.events <- "output_leave"
52 case wlr.ZwlrForeignToplevelHandleV1EventState:
53 var states []uint32
54 for b := e.Array(); len(b) >= 4; b = b[4:] {
55 states = append(states, binary.NativeEndian.Uint32(b))
56 }
57 p.events <- fmt.Sprint("state ", states)
58 case wlr.ZwlrForeignToplevelHandleV1EventDone:
59 p.events <- "done"
60 case wlr.ZwlrForeignToplevelHandleV1EventClosed:
61 p.events <- "closed"
62 }
63}
64
65// takeEvents returns the handle events received up to the next done or
66// closed, after a roundtrip.
67func takeEvents(t *testing.T, c *wlturbo.Display, events chan string) []string {
68 t.Helper()
69 var got []string
70 deadline := time.Now().Add(2 * time.Second)
71 for time.Now().Before(deadline) {
72 if err := c.Roundtrip(); err != nil {
73 t.Fatal(err)
74 }
75 for len(events) > 0 {
76 ev := <-events
77 got = append(got, ev)
78 if ev == "done" || ev == "closed" {
79 return got
80 }
81 }
82 time.Sleep(10 * time.Millisecond)
83 }
84 t.Fatalf("no done, got %q", got)
85 return nil
86}
87
88func TestForeignToplevel(t *testing.T) {
89 s, events, commands, dir := lifecycleServer(t)
90 c := protocolClient(t, s, dir)
91 out := bindVersion(t, c, "wl_output", 4)
92 registerProtocol(t, c, out)
93 seat := bindProtocol(t, c, "wl_seat")
94 registerProtocol(t, c, seat)
95 mapWindow := surfaceMapper(t, c, events)
96 w, surf, xdg := mapWindow()
97 // Core configures below are not acked: drop them.
98 registerProtocol(t, c, xdg)
99 // surfaceMapper allocates the wl_surface, xdg_surface and xdg_toplevel in order.
100 top := xdg + 1
101 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor")
102 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetAppId, "app.editor")
103
104 // A window mapped before the bind is announced.
105 manager := bindVersion(t, c, "zwlr_foreign_toplevel_manager_v1", 3)
106 p := &toplevelManagerEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
107 p.SetID(manager)
108 registerWireProxy(c, p)
109 if got, want := takeEvents(t, c, p.events), []string{"title editor", "app_id app.editor", "state []", "done"}; !slices.Equal(got, want) {
110 t.Fatalf("announce = %q, want %q", got, want)
111 }
112 handle := <-p.handles
113
114 // Core configures it fullscreen and focused on the output.
115 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Activated: true, Output: "HEADLESS-1"}
116 want := []string{fmt.Sprint("state ", []uint32{uint32(wlr.ZwlrForeignToplevelHandleV1StateActivated), uint32(wlr.ZwlrForeignToplevelHandleV1StateFullscreen)}), "output_enter", "done"}
117 if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
118 t.Fatalf("fullscreen = %q, want %q", got, want)
119 }
120 commands <- ports.ConfigureWindow{ID: w.ID, Width: 960, Height: 1080, Output: "HEADLESS-1"}
121 want = []string{fmt.Sprint("state ", []uint32{uint32(wlr.ZwlrForeignToplevelHandleV1StateMaximized)}), "done"}
122 if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
123 t.Fatalf("tiled = %q, want %q", got, want)
124 }
125 requestProtocol(t, c, top, xdgshell.ToplevelRequestSetTitle, "editor - file")
126 if got, want := takeEvents(t, c, p.events), []string{"title editor - file", "done"}; !slices.Equal(got, want) {
127 t.Fatalf("title = %q, want %q", got, want)
128 }
129
130 // Requests reach core as the xdg-shell and xdg-activation messages.
131 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetFullscreen, uint32(0))
132 if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, Fullscreen: true, External: true}) {
133 t.Fatalf("set_fullscreen = %#v", ev)
134 }
135 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestUnsetFullscreen)
136 if ev := clientEvent[ports.WindowFullscreenRequest](t, events); ev != (ports.WindowFullscreenRequest{ID: w.ID, External: true}) {
137 t.Fatalf("unset_fullscreen = %#v", ev)
138 }
139 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
140 if ev := clientEvent[ports.WindowActivate](t, events); ev.ID != w.ID {
141 t.Fatalf("activate = %#v", ev)
142 }
143
144 // Unmapping closes the handle; requests on it are then ignored.
145 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
146 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
147 if got, want := takeEvents(t, c, p.events), []string{"closed"}; !slices.Equal(got, want) {
148 t.Fatalf("unmap = %q, want %q", got, want)
149 }
150 unmapped(t, events, w.ID)
151
152 // Remapping the same toplevel announces a new handle; the closed one
153 // stays inert although the window ID is the same.
154 serials := make(chan uint32, 8)
155 xp := &configureProxy{serial: serials}
156 xp.SetID(xdg)
157 registerWireProxy(c, xp)
158 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
159 ackAndAttach(t, c, surf, xdg, shmBuffer(t, c), serials)
160 if again := mapped(t, events, 2*time.Second); again.ID != w.ID {
161 t.Fatalf("remapped as %d, want %d", again.ID, w.ID)
162 }
163 if got := takeEvents(t, c, p.events); len(got) == 0 || got[len(got)-1] != "done" {
164 t.Fatalf("remap = %q", got)
165 }
166 remapped := <-p.handles
167 if remapped == handle {
168 t.Fatal("remap reused the closed handle")
169 }
170 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
171 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestClose)
172 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetRectangle, surf, int32(0), int32(0), int32(-1), int32(1))
173 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestDestroy)
174 if err := c.Roundtrip(); err != nil {
175 t.Fatal(err)
176 }
177 select {
178 case ev := <-events:
179 t.Fatalf("event from a closed handle: %#v", ev)
180 case <-time.After(100 * time.Millisecond):
181 }
182 requestProtocol(t, c, remapped, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
183 if ev := clientEvent[ports.WindowActivate](t, events); ev.ID != w.ID {
184 t.Fatalf("activate remapped = %#v", ev)
185 }
186
187 // A window mapped after the bind is announced too.
188 mapWindow()
189 if got := takeEvents(t, c, p.events); len(got) == 0 || got[len(got)-1] != "done" {
190 t.Fatalf("new window = %q", got)
191 }
192}
193
194// Fullscreen is a version 2 state: version 1 clients never see it.
195func TestForeignToplevelV1HidesFullscreen(t *testing.T) {
196 s, events, commands, dir := lifecycleServer(t)
197 c := protocolClient(t, s, dir)
198 w, _, xdg := surfaceMapper(t, c, events)()
199 registerProtocol(t, c, xdg)
200 manager := bindVersion(t, c, "zwlr_foreign_toplevel_manager_v1", 1)
201 p := &toplevelManagerEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
202 p.SetID(manager)
203 registerWireProxy(c, p)
204 takeEvents(t, c, p.events)
205 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Activated: true}
206 want := []string{fmt.Sprint("state ", []uint32{uint32(wlr.ZwlrForeignToplevelHandleV1StateActivated)}), "done"}
207 if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
208 t.Fatalf("v1 fullscreen = %q, want %q", got, want)
209 }
210}
211
212func TestForeignToplevelInvalidRectangle(t *testing.T) {
213 s, events, _, dir := lifecycleServer(t)
214 c := protocolClient(t, s, dir)
215 _, surf, _ := surfaceMapper(t, c, events)()
216 manager := bindVersion(t, c, "zwlr_foreign_toplevel_manager_v1", 3)
217 p := &toplevelManagerEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
218 p.SetID(manager)
219 registerWireProxy(c, p)
220 takeEvents(t, c, p.events)
221 handle := <-p.handles
222 requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetRectangle, surf, int32(0), int32(0), int32(-1), int32(1))
223 expectProtocolError(t, c, handle, uint32(wlr.ZwlrForeignToplevelHandleV1ErrorInvalidRectangle))
224}