v0.8.0 446d1e3

foreigntoplevel_test.go RawHistoryFolder

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	// Core learns it is a remap: window rules only place a first map.
161	if w.Remap {
162		t.Fatal("first map flagged as a remap")
163	}
164	if again := mapped(t, events, 2*time.Second); again.ID != w.ID || !again.Remap {
165		t.Fatalf("remapped as %+v, want ID %d and Remap", again, w.ID)
166	}
167	if got := takeEvents(t, c, p.events); len(got) == 0 || got[len(got)-1] != "done" {
168		t.Fatalf("remap = %q", got)
169	}
170	remapped := <-p.handles
171	if remapped == handle {
172		t.Fatal("remap reused the closed handle")
173	}
174	requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
175	requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestClose)
176	requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetRectangle, surf, int32(0), int32(0), int32(-1), int32(1))
177	requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestDestroy)
178	if err := c.Roundtrip(); err != nil {
179		t.Fatal(err)
180	}
181	select {
182	case ev := <-events:
183		t.Fatalf("event from a closed handle: %#v", ev)
184	case <-time.After(100 * time.Millisecond):
185	}
186	requestProtocol(t, c, remapped, wlr.ZwlrForeignToplevelHandleV1RequestActivate, seat)
187	if ev := clientEvent[ports.WindowActivate](t, events); ev.ID != w.ID {
188		t.Fatalf("activate remapped = %#v", ev)
189	}
190
191	// A window mapped after the bind is announced too.
192	mapWindow()
193	if got := takeEvents(t, c, p.events); len(got) == 0 || got[len(got)-1] != "done" {
194		t.Fatalf("new window = %q", got)
195	}
196}
197
198// Fullscreen is a version 2 state: version 1 clients never see it.
199func TestForeignToplevelV1HidesFullscreen(t *testing.T) {
200	s, events, commands, dir := lifecycleServer(t)
201	c := protocolClient(t, s, dir)
202	w, _, xdg := surfaceMapper(t, c, events)()
203	registerProtocol(t, c, xdg)
204	manager := bindVersion(t, c, "zwlr_foreign_toplevel_manager_v1", 1)
205	p := &toplevelManagerEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
206	p.SetID(manager)
207	registerWireProxy(c, p)
208	takeEvents(t, c, p.events)
209	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Activated: true}
210	want := []string{fmt.Sprint("state ", []uint32{uint32(wlr.ZwlrForeignToplevelHandleV1StateActivated)}), "done"}
211	if got := takeEvents(t, c, p.events); !slices.Equal(got, want) {
212		t.Fatalf("v1 fullscreen = %q, want %q", got, want)
213	}
214}
215
216func TestForeignToplevelInvalidRectangle(t *testing.T) {
217	s, events, _, dir := lifecycleServer(t)
218	c := protocolClient(t, s, dir)
219	_, surf, _ := surfaceMapper(t, c, events)()
220	manager := bindVersion(t, c, "zwlr_foreign_toplevel_manager_v1", 3)
221	p := &toplevelManagerEvents{client: c, handles: make(chan uint32, 4), events: make(chan string, 64)}
222	p.SetID(manager)
223	registerWireProxy(c, p)
224	takeEvents(t, c, p.events)
225	handle := <-p.handles
226	requestProtocol(t, c, handle, wlr.ZwlrForeignToplevelHandleV1RequestSetRectangle, surf, int32(0), int32(0), int32(-1), int32(1))
227	expectProtocolError(t, c, handle, uint32(wlr.ZwlrForeignToplevelHandleV1ErrorInvalidRectangle))
228}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close