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 }