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