git.lucas.co / cce-compositor
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 }