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