Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
verify/clients/src/bin/float_pair.rs (14.2K)
1 // float-pair — one client, two parentless floating toplevels, the second
2 // opened later the way a Chromium/Electron app opens an "Authorize" dialog:
3 // same app_id, no xdg parent, a fixed size it insists on (min == max, and it
4 // commits its own size whatever the configure said), a server-side
5 // decoration request, and an xdg-activation request BEFORE its first buffer,
6 // with a token issued against the first window's surface.
7 //
8 // Prints one line per milestone so a driver can wait on them:
9 // main mapped | dialog created | token <t> | activated | dialog mapped
10 //
11 // Args: --app-id ID --delay SECS --main WxH --dialog WxH
12 // --dialog-honours-configure --no-activate --no-decoration
13 // --bare-token (no seat/serial/surface on the token)
14 // --reactivate SECS that long after the dialog, request activation of
15 // the (by then unfocused) MAIN window — an activation
16 // for an already-mapped window
17 // Extra milestones: reactivate requested | reactivated
18
19 use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
20 use std::time::{Duration, Instant};
21 use wayland_client::{
22 delegate_noop,
23 protocol::{wl_buffer, wl_compositor, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface},
24 Connection, Dispatch, QueueHandle,
25 };
26 use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1};
27 use wayland_protocols::xdg::decoration::zv1::client::{
28 zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
29 };
30 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
31
32 struct Win {
33 surface: wl_surface::WlSurface,
34 xdg: xdg_surface::XdgSurface,
35 toplevel: xdg_toplevel::XdgToplevel,
36 want: (i32, i32),
37 honour: bool,
38 pending: Option<(i32, i32)>,
39 needs_buffer: bool,
40 mapped: bool,
41 color: u32,
42 name: &'static str,
43 }
44
45 #[derive(Default)]
46 struct State {
47 compositor: Option<wl_compositor::WlCompositor>,
48 shm: Option<wl_shm::WlShm>,
49 wm_base: Option<xdg_wm_base::XdgWmBase>,
50 activation: Option<xdg_activation_v1::XdgActivationV1>,
51 seat: Option<wl_seat::WlSeat>,
52 decoration: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
53 wins: Vec<Win>,
54 token: Option<String>,
55 closed: bool,
56 }
57
58 impl Dispatch<wl_registry::WlRegistry, ()> for State {
59 fn event(
60 state: &mut Self,
61 registry: &wl_registry::WlRegistry,
62 event: wl_registry::Event,
63 _: &(),
64 _: &Connection,
65 qh: &QueueHandle<Self>,
66 ) {
67 if let wl_registry::Event::Global { name, interface, version } = event {
68 match interface.as_str() {
69 "wl_compositor" => {
70 state.compositor = Some(
71 registry.bind::<wl_compositor::WlCompositor, _, _>(name, version.min(4), qh, ()),
72 );
73 }
74 "wl_shm" => state.shm = Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ())),
75 "xdg_wm_base" => {
76 state.wm_base = Some(registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ()))
77 }
78 "xdg_activation_v1" => {
79 state.activation =
80 Some(registry.bind::<xdg_activation_v1::XdgActivationV1, _, _>(name, 1, qh, ()))
81 }
82 "wl_seat" => state.seat = Some(registry.bind::<wl_seat::WlSeat, _, _>(name, 1, qh, ())),
83 "zxdg_decoration_manager_v1" => {
84 state.decoration = Some(
85 registry
86 .bind::<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, _, _>(name, 1, qh, ()),
87 )
88 }
89 _ => {}
90 }
91 }
92 }
93 }
94
95 impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
96 fn event(
97 _: &mut Self,
98 wm_base: &xdg_wm_base::XdgWmBase,
99 event: xdg_wm_base::Event,
100 _: &(),
101 _: &Connection,
102 _: &QueueHandle<Self>,
103 ) {
104 if let xdg_wm_base::Event::Ping { serial } = event {
105 wm_base.pong(serial);
106 }
107 }
108 }
109
110 impl Dispatch<xdg_surface::XdgSurface, ()> for State {
111 fn event(
112 state: &mut Self,
113 xdg_surface: &xdg_surface::XdgSurface,
114 event: xdg_surface::Event,
115 _: &(),
116 _: &Connection,
117 _: &QueueHandle<Self>,
118 ) {
119 if let xdg_surface::Event::Configure { serial } = event {
120 xdg_surface.ack_configure(serial);
121 if let Some(win) = state.wins.iter_mut().find(|w| &w.xdg == xdg_surface) {
122 win.needs_buffer = true;
123 }
124 }
125 }
126 }
127
128 impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
129 fn event(
130 state: &mut Self,
131 toplevel: &xdg_toplevel::XdgToplevel,
132 event: xdg_toplevel::Event,
133 _: &(),
134 _: &Connection,
135 _: &QueueHandle<Self>,
136 ) {
137 match event {
138 xdg_toplevel::Event::Configure { width, height, .. } => {
139 if width > 0 && height > 0 {
140 if let Some(win) = state.wins.iter_mut().find(|w| &w.toplevel == toplevel) {
141 win.pending = Some((width, height));
142 }
143 }
144 }
145 xdg_toplevel::Event::Close => state.closed = true,
146 _ => {}
147 }
148 }
149 }
150
151 impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for State {
152 fn event(
153 state: &mut Self,
154 _: &xdg_activation_token_v1::XdgActivationTokenV1,
155 event: xdg_activation_token_v1::Event,
156 _: &(),
157 _: &Connection,
158 _: &QueueHandle<Self>,
159 ) {
160 if let xdg_activation_token_v1::Event::Done { token } = event {
161 state.token = Some(token);
162 }
163 }
164 }
165
166 delegate_noop!(State: ignore wl_compositor::WlCompositor);
167 delegate_noop!(State: ignore wl_shm::WlShm);
168 delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
169 delegate_noop!(State: ignore wl_buffer::WlBuffer);
170 delegate_noop!(State: ignore wl_surface::WlSurface);
171 delegate_noop!(State: ignore wl_seat::WlSeat);
172 delegate_noop!(State: ignore xdg_activation_v1::XdgActivationV1);
173 delegate_noop!(State: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
174 delegate_noop!(State: ignore zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1);
175
176 fn make_buffer(
177 shm: &wl_shm::WlShm,
178 w: i32,
179 h: i32,
180 color: u32,
181 qh: &QueueHandle<State>,
182 ) -> wl_buffer::WlBuffer {
183 let size = (w * 4 * h) as u64;
184 let fd = unsafe { libc::memfd_create(b"float-pair\0".as_ptr() as *const _, 0) };
185 assert!(fd >= 0, "memfd_create failed");
186 let file = unsafe { std::fs::File::from_raw_fd(fd) };
187 file.set_len(size).unwrap();
188 let mmapped = unsafe {
189 libc::mmap(std::ptr::null_mut(), size as usize, libc::PROT_WRITE, libc::MAP_SHARED, file.as_raw_fd(), 0)
190 };
191 assert!(mmapped != libc::MAP_FAILED, "mmap failed");
192 unsafe {
193 let px = mmapped as *mut u32;
194 for i in 0..(w * h) as usize {
195 *px.add(i) = color;
196 }
197 libc::munmap(mmapped, size as usize);
198 }
199 let fd: OwnedFd = OwnedFd::from(file);
200 let pool = shm.create_pool(fd.as_fd(), w * 4 * h, qh, ());
201 // The pool object is leaked on purpose: the buffer outlives it anyway,
202 // and this is a test client.
203 pool.create_buffer(0, w, h, w * 4, wl_shm::Format::Argb8888, qh, ())
204 }
205
206 fn parse_size(s: &str) -> (i32, i32) {
207 let (w, h) = s.split_once('x').expect("size is WxH");
208 (w.parse().unwrap(), h.parse().unwrap())
209 }
210
211 fn main() {
212 let mut app_id = "test.floatpair".to_string();
213 let mut delay = 3.0f64;
214 let mut main_size = (1024, 800);
215 let mut dialog_size = (400, 370);
216 let mut dialog_honours = false;
217 let mut activate = true;
218 let mut decoration = true;
219 let mut bare_token = false;
220 let mut reactivate: Option<f64> = None;
221 let mut args = std::env::args().skip(1);
222 while let Some(a) = args.next() {
223 match a.as_str() {
224 "--app-id" => app_id = args.next().unwrap(),
225 "--delay" => delay = args.next().unwrap().parse().unwrap(),
226 "--main" => main_size = parse_size(&args.next().unwrap()),
227 "--dialog" => dialog_size = parse_size(&args.next().unwrap()),
228 "--dialog-honours-configure" => dialog_honours = true,
229 "--no-activate" => activate = false,
230 "--no-decoration" => decoration = false,
231 "--bare-token" => bare_token = true,
232 "--reactivate" => reactivate = Some(args.next().unwrap().parse().unwrap()),
233 other => panic!("unknown arg {other}"),
234 }
235 }
236
237 let conn = Connection::connect_to_env().expect("connect to wayland display");
238 let display = conn.display();
239 let mut queue = conn.new_event_queue();
240 let qh = queue.handle();
241 let _registry = display.get_registry(&qh, ());
242
243 let mut state = State::default();
244 queue.roundtrip(&mut state).unwrap();
245
246 let compositor = state.compositor.clone().expect("no wl_compositor");
247 let shm = state.shm.clone().expect("no wl_shm");
248 let wm_base = state.wm_base.clone().expect("no xdg_wm_base");
249
250 let make_win = |state: &mut State, title: &str, want: (i32, i32), honour: bool, color: u32, fixed: bool, name: &'static str| {
251 let surface = compositor.create_surface(&qh, ());
252 let xdg = wm_base.get_xdg_surface(&surface, &qh, ());
253 let toplevel = xdg.get_toplevel(&qh, ());
254 toplevel.set_app_id(app_id.clone());
255 toplevel.set_title(title.into());
256 if fixed {
257 toplevel.set_min_size(want.0, want.1);
258 toplevel.set_max_size(want.0, want.1);
259 }
260 if decoration {
261 if let Some(dm) = &state.decoration {
262 let deco = dm.get_toplevel_decoration(&toplevel, &qh, ());
263 deco.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide);
264 }
265 }
266 surface.commit();
267 state.wins.push(Win {
268 surface,
269 xdg,
270 toplevel,
271 want,
272 honour,
273 pending: None,
274 needs_buffer: false,
275 mapped: false,
276 color,
277 name,
278 });
279 };
280
281 make_win(&mut state, "Main window", main_size, true, 0xff2a_6f97, false, "main");
282
283 let start = Instant::now();
284 let mut dialog_created = false;
285 let mut token_requested = false;
286 let mut activated = false;
287 let mut react_requested = false;
288 let mut reactivated = false;
289
290 while !state.closed {
291 conn.flush().unwrap();
292 if let Some(guard) = conn.prepare_read() {
293 let mut pfd = libc::pollfd { fd: guard.connection_fd().as_raw_fd(), events: libc::POLLIN, revents: 0 };
294 let n = unsafe { libc::poll(&mut pfd, 1, 50) };
295 if n > 0 && (pfd.revents & libc::POLLIN) != 0 {
296 let _ = guard.read();
297 } else {
298 drop(guard);
299 }
300 }
301 queue.dispatch_pending(&mut state).unwrap();
302
303 for win in state.wins.iter_mut() {
304 if win.needs_buffer {
305 win.needs_buffer = false;
306 let size = match (win.honour, win.pending) {
307 (true, Some(p)) => p,
308 _ => win.want,
309 };
310 let buf = make_buffer(&shm, size.0, size.1, win.color, &qh);
311 win.surface.attach(Some(&buf), 0, 0);
312 win.surface.damage_buffer(0, 0, size.0, size.1);
313 win.surface.commit();
314 if !win.mapped {
315 win.mapped = true;
316 println!("{} mapped {}x{}", win.name, size.0, size.1);
317 }
318 }
319 }
320
321 // The token is fetched a second before the dialog so it is in hand
322 // when the dialog is created, and the activation goes out right
323 // after the dialog's initial commit — before its first buffer, the
324 // way Chromium does it (the live log shows the request landing
325 // between the app_id and the map).
326 if activate && !token_requested && start.elapsed() + Duration::from_secs(1) >= Duration::from_secs_f64(delay) {
327 token_requested = true;
328 if let (Some(act), Some(seat)) = (state.activation.clone(), state.seat.clone()) {
329 let tok = act.get_activation_token(&qh, ());
330 tok.set_app_id(app_id.clone());
331 if !bare_token {
332 tok.set_surface(&state.wins[0].surface);
333 tok.set_serial(0, &seat);
334 }
335 tok.commit();
336 } else {
337 println!("no xdg_activation_v1 or wl_seat; not activating");
338 }
339 }
340 if !dialog_created && start.elapsed() >= Duration::from_secs_f64(delay) {
341 dialog_created = true;
342 make_win(&mut state, "Authorize", dialog_size, dialog_honours, 0xffd9_8c2b, true, "dialog");
343 println!("dialog created");
344 if let Some(t) = state.token.clone() {
345 activated = true;
346 println!("token {}", t);
347 state.activation.as_ref().unwrap().activate(t, &state.wins[1].surface);
348 println!("activated");
349 }
350 }
351 if let Some(secs) = reactivate {
352 if dialog_created && !react_requested && start.elapsed() >= Duration::from_secs_f64(delay + secs) {
353 react_requested = true;
354 state.token = None;
355 let act = state.activation.clone().expect("no xdg_activation_v1");
356 let tok = act.get_activation_token(&qh, ());
357 tok.set_app_id(app_id.clone());
358 tok.commit();
359 println!("reactivate requested");
360 }
361 if react_requested && !reactivated {
362 if let Some(t) = state.token.clone() {
363 reactivated = true;
364 state.activation.as_ref().unwrap().activate(t, &state.wins[0].surface);
365 println!("reactivated");
366 }
367 }
368 }
369 if dialog_created && token_requested && !activated {
370 if let Some(t) = state.token.clone() {
371 activated = true;
372 println!("token (late) {}", t);
373 state.activation.as_ref().unwrap().activate(t, &state.wins[1].surface);
374 println!("activated");
375 }
376 }
377 }
378 }