v0.3.0 cf370a0

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	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}

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 25

Tags 10

↑↓ moveenter openesc close