git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

verify/clients/src/bin/popup_nest.rs (10.6K)

  1 // popup-nest — a toplevel with a menu and a submenu, the way Chrome's
  2 // three-dot menu opens "Bookmarks and lists": a top-level xdg_popup
  3 // anchored in the window, then a nested xdg_popup anchored on the menu's
  4 // right edge, both asking to flip sideways and slide vertically to fit.
  5 // It proves the compositor unconstrains a SUBMENU against the real screen:
  6 // the box wlroots needs is in the root window's coordinates, and measuring
  7 // it from the parent menu instead pushed tall submenus off the top.
  8 //
  9 // Prints one line per milestone, each popup's position RELATIVE TO ITS
 10 // PARENT as the compositor configured it:
 11 //   toplevel mapped WxH
 12 //   menu X Y W H
 13 //   submenu X Y W H
 14 //
 15 // Args: --app-id ID  --size WxH
 16 //       --menu-at X,Y   anchor point in the window   (default 500,250)
 17 //       --menu WxH                                     (default 300x200)
 18 //       --sub-at X,Y    anchor point in the menu      (default 300,180)
 19 //       --sub WxH                                      (default 200x600)
 20 
 21 use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
 22 use wayland_client::{
 23     delegate_noop,
 24     protocol::{wl_buffer, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface},
 25     Connection, Dispatch, QueueHandle,
 26 };
 27 use wayland_protocols::xdg::shell::client::{xdg_popup, xdg_positioner, xdg_surface, xdg_toplevel, xdg_wm_base};
 28 
 29 #[derive(Clone, Copy, PartialEq)]
 30 enum Role {
 31     Toplevel,
 32     Menu,
 33     Submenu,
 34 }
 35 
 36 struct Surf {
 37     role: Role,
 38     surface: wl_surface::WlSurface,
 39     xdg: xdg_surface::XdgSurface,
 40     size: (i32, i32),
 41     placed: Option<(i32, i32, i32, i32)>,
 42     needs_buffer: bool,
 43     mapped: bool,
 44     color: u32,
 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     surfs: Vec<Surf>,
 53     closed: bool,
 54 }
 55 
 56 impl Dispatch<wl_registry::WlRegistry, ()> for State {
 57     fn event(
 58         state: &mut Self,
 59         registry: &wl_registry::WlRegistry,
 60         event: wl_registry::Event,
 61         _: &(),
 62         _: &Connection,
 63         qh: &QueueHandle<Self>,
 64     ) {
 65         if let wl_registry::Event::Global { name, interface, version } = event {
 66             match interface.as_str() {
 67                 "wl_compositor" => {
 68                     state.compositor =
 69                         Some(registry.bind::<wl_compositor::WlCompositor, _, _>(name, version.min(4), qh, ()))
 70                 }
 71                 "wl_shm" => state.shm = Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ())),
 72                 "xdg_wm_base" => {
 73                     state.wm_base =
 74                         Some(registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, version.min(3), qh, ()))
 75                 }
 76                 _ => {}
 77             }
 78         }
 79     }
 80 }
 81 
 82 impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
 83     fn event(_: &mut Self, wm: &xdg_wm_base::XdgWmBase, event: xdg_wm_base::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
 84         if let xdg_wm_base::Event::Ping { serial } = event {
 85             wm.pong(serial);
 86         }
 87     }
 88 }
 89 
 90 impl Dispatch<xdg_surface::XdgSurface, ()> for State {
 91     fn event(state: &mut Self, xdg: &xdg_surface::XdgSurface, event: xdg_surface::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
 92         if let xdg_surface::Event::Configure { serial } = event {
 93             xdg.ack_configure(serial);
 94             if let Some(s) = state.surfs.iter_mut().find(|s| &s.xdg == xdg) {
 95                 s.needs_buffer = true;
 96             }
 97         }
 98     }
 99 }
100 
101 impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
102     fn event(state: &mut Self, _: &xdg_toplevel::XdgToplevel, event: xdg_toplevel::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
103         if let xdg_toplevel::Event::Close = event {
104             state.closed = true;
105         }
106     }
107 }
108 
109 impl Dispatch<xdg_popup::XdgPopup, Role> for State {
110     fn event(state: &mut Self, _: &xdg_popup::XdgPopup, event: xdg_popup::Event, role: &Role, _: &Connection, _: &QueueHandle<Self>) {
111         match event {
112             xdg_popup::Event::Configure { x, y, width, height } => {
113                 if let Some(s) = state.surfs.iter_mut().find(|s| s.role == *role) {
114                     s.placed = Some((x, y, width, height));
115                 }
116             }
117             xdg_popup::Event::PopupDone => println!("popup done"),
118             _ => {}
119         }
120     }
121 }
122 
123 delegate_noop!(State: ignore wl_compositor::WlCompositor);
124 delegate_noop!(State: ignore wl_shm::WlShm);
125 delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
126 delegate_noop!(State: ignore wl_buffer::WlBuffer);
127 delegate_noop!(State: ignore wl_surface::WlSurface);
128 delegate_noop!(State: ignore xdg_positioner::XdgPositioner);
129 
130 fn make_buffer(shm: &wl_shm::WlShm, w: i32, h: i32, color: u32, qh: &QueueHandle<State>) -> wl_buffer::WlBuffer {
131     let size = (w * 4 * h) as u64;
132     let fd = unsafe { libc::memfd_create(b"popup-nest\0".as_ptr() as *const _, 0) };
133     assert!(fd >= 0, "memfd_create failed");
134     let file = unsafe { std::fs::File::from_raw_fd(fd) };
135     file.set_len(size).unwrap();
136     let mapped = unsafe {
137         libc::mmap(std::ptr::null_mut(), size as usize, libc::PROT_WRITE, libc::MAP_SHARED, file.as_raw_fd(), 0)
138     };
139     assert!(mapped != libc::MAP_FAILED, "mmap failed");
140     unsafe {
141         let px = mapped as *mut u32;
142         for i in 0..(w * h) as usize {
143             *px.add(i) = color;
144         }
145         libc::munmap(mapped, size as usize);
146     }
147     let fd: OwnedFd = OwnedFd::from(file);
148     let pool = shm.create_pool(fd.as_fd(), w * 4 * h, qh, ());
149     pool.create_buffer(0, w, h, w * 4, wl_shm::Format::Argb8888, qh, ())
150 }
151 
152 fn pair(s: &str, sep: char) -> (i32, i32) {
153     let (a, b) = s.split_once(sep).expect("pair");
154     (a.parse().unwrap(), b.parse().unwrap())
155 }
156 
157 fn main() {
158     let mut app_id = "test.popupnest".to_string();
159     let mut size = (800, 500);
160     let mut menu_at = (500, 250);
161     let mut menu = (300, 200);
162     let mut sub_at = (300, 180);
163     let mut sub = (200, 600);
164     let mut args = std::env::args().skip(1);
165     while let Some(a) = args.next() {
166         match a.as_str() {
167             "--app-id" => app_id = args.next().unwrap(),
168             "--size" => size = pair(&args.next().unwrap(), 'x'),
169             "--menu-at" => menu_at = pair(&args.next().unwrap(), ','),
170             "--menu" => menu = pair(&args.next().unwrap(), 'x'),
171             "--sub-at" => sub_at = pair(&args.next().unwrap(), ','),
172             "--sub" => sub = pair(&args.next().unwrap(), 'x'),
173             other => panic!("unknown arg {other}"),
174         }
175     }
176 
177     let conn = Connection::connect_to_env().expect("connect to wayland display");
178     let mut queue = conn.new_event_queue();
179     let qh = queue.handle();
180     let _registry = conn.display().get_registry(&qh, ());
181     let mut state = State::default();
182     queue.roundtrip(&mut state).unwrap();
183     let compositor = state.compositor.clone().expect("no wl_compositor");
184     let shm = state.shm.clone().expect("no wl_shm");
185     let wm = state.wm_base.clone().expect("no xdg_wm_base");
186 
187     // The toplevel insists on its size, so the popup arithmetic is fixed.
188     let surface = compositor.create_surface(&qh, ());
189     let xdg = wm.get_xdg_surface(&surface, &qh, ());
190     let toplevel = xdg.get_toplevel(&qh, ());
191     toplevel.set_app_id(app_id);
192     toplevel.set_title("popup-nest".into());
193     toplevel.set_min_size(size.0, size.1);
194     toplevel.set_max_size(size.0, size.1);
195     surface.commit();
196     state.surfs.push(Surf { role: Role::Toplevel, surface, xdg, size, placed: None, needs_buffer: false, mapped: false, color: 0xff2a_6f97 });
197 
198     // What Chrome asks for: open to the lower right of the anchor point,
199     // flip sideways and slide vertically when that does not fit.
200     let adjust = xdg_positioner::ConstraintAdjustment::FlipX | xdg_positioner::ConstraintAdjustment::SlideY;
201     let mut menu_open = false;
202     let mut sub_open = false;
203 
204     while !state.closed {
205         conn.flush().unwrap();
206         if let Some(guard) = conn.prepare_read() {
207             let mut pfd = libc::pollfd { fd: guard.connection_fd().as_raw_fd(), events: libc::POLLIN, revents: 0 };
208             if unsafe { libc::poll(&mut pfd, 1, 50) } > 0 && (pfd.revents & libc::POLLIN) != 0 {
209                 let _ = guard.read();
210             }
211         }
212         queue.dispatch_pending(&mut state).unwrap();
213 
214         for s in state.surfs.iter_mut() {
215             if !s.needs_buffer {
216                 continue;
217             }
218             s.needs_buffer = false;
219             let (w, h) = match (s.role, s.placed) {
220                 (Role::Toplevel, _) => s.size,
221                 (_, Some((_, _, w, h))) if w > 0 && h > 0 => (w, h),
222                 _ => s.size,
223             };
224             let buf = make_buffer(&shm, w, h, s.color, &qh);
225             s.surface.attach(Some(&buf), 0, 0);
226             s.surface.damage_buffer(0, 0, w, h);
227             s.surface.commit();
228             if !s.mapped {
229                 s.mapped = true;
230                 match (s.role, s.placed) {
231                     (Role::Toplevel, _) => println!("toplevel mapped {}x{}", w, h),
232                     (Role::Menu, Some((x, y, w, h))) => println!("menu {x} {y} {w} {h}"),
233                     (Role::Submenu, Some((x, y, w, h))) => println!("submenu {x} {y} {w} {h}"),
234                     _ => {}
235                 }
236             }
237         }
238 
239         let mapped = |state: &State, role: Role| state.surfs.iter().any(|s| s.role == role && s.mapped);
240         let open = |state: &mut State, role: Role, parent: &xdg_surface::XdgSurface, at: (i32, i32), sz: (i32, i32), color: u32| {
241             let pos = wm.create_positioner(&qh, ());
242             pos.set_size(sz.0, sz.1);
243             pos.set_anchor_rect(at.0, at.1, 1, 1);
244             pos.set_anchor(xdg_positioner::Anchor::BottomRight);
245             pos.set_gravity(xdg_positioner::Gravity::BottomRight);
246             pos.set_constraint_adjustment(adjust);
247             let surface = compositor.create_surface(&qh, ());
248             let xdg = wm.get_xdg_surface(&surface, &qh, ());
249             let _popup = xdg.get_popup(Some(parent), &pos, &qh, role);
250             surface.commit();
251             state.surfs.push(Surf { role, surface, xdg, size: sz, placed: None, needs_buffer: false, mapped: false, color });
252         };
253         if !menu_open && mapped(&state, Role::Toplevel) {
254             menu_open = true;
255             let parent = state.surfs[0].xdg.clone();
256             open(&mut state, Role::Menu, &parent, menu_at, menu, 0xffe0_e0e0);
257         }
258         if !sub_open && mapped(&state, Role::Menu) {
259             sub_open = true;
260             let parent = state.surfs.iter().find(|s| s.role == Role::Menu).unwrap().xdg.clone();
261             open(&mut state, Role::Submenu, &parent, sub_at, sub, 0xffd9_8c2b);
262         }
263     }
264 }