Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/xdg_popup.rs (11.3K)
1 // SPDX-FileCopyrightText: © 2023 The River Developers
2 // SPDX-License-Identifier: GPL-3.0-only
3
4 use crate::ffi;
5 use crate::server::{WlListener, wl_listener_remove, wl_signal_add};
6
7 pub struct XdgPopup {
8 pub wlr_popup: *mut ffi::wlr_xdg_popup,
9 pub tree: *mut ffi::wlr_scene_tree,
10 pub capture_tree: *mut ffi::wlr_scene_tree,
11 /// The scene tree of the popup's ROOT — the window's or layer
12 /// surface's popup tree, which sits at that surface's origin — shared
13 /// by every submenu under it. `handle_reposition` measures the screen
14 /// from here, because wlroots wants the unconstrain box in the root
15 /// toplevel surface's coordinates, not the immediate parent's.
16 pub root_tree: *mut ffi::wlr_scene_tree,
17 /// The server, found once at creation through the root's scene node —
18 /// a destroyed popup's own tree may already be gone when its destroy
19 /// signal runs, so nothing can be looked up then. Null for a root that
20 /// is neither a window nor a shell surface.
21 pub server: *mut crate::server::Server,
22 /// Where the popup stood and how big it was at its last commit, layout
23 /// coordinates: to tell a map, a move or a resize.
24 pub last_box: (i32, i32, i32, i32),
25
26 pub destroy: ffi::wl_listener,
27 pub commit: ffi::wl_listener,
28 pub new_popup: ffi::wl_listener,
29 pub reposition: ffi::wl_listener,
30 }
31
32 impl XdgPopup {
33 /// `root` is the tree the popup's root surface's popups live in: for a
34 /// top-level menu the same tree as `parent`, for a submenu the one its
35 /// parent popup carries.
36 pub unsafe fn create(
37 wlr_popup: *mut ffi::wlr_xdg_popup,
38 parent: *mut ffi::wlr_scene_tree,
39 capture_parent: *mut ffi::wlr_scene_tree,
40 root: *mut ffi::wlr_scene_tree,
41 ) -> Result<*mut Self, &'static str> {
42 let base_surface = ffi::river_wlr_xdg_popup_get_base(wlr_popup);
43 let tree = ffi::wlr_scene_xdg_surface_create(parent, base_surface);
44 if tree.is_null() {
45 return Err("wlr_scene_xdg_surface_create failed");
46 }
47
48 let mut capture_tree = std::ptr::null_mut();
49 if !capture_parent.is_null() {
50 capture_tree = ffi::wlr_scene_xdg_surface_create(capture_parent, base_surface);
51 if capture_tree.is_null() {
52 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
53 return Err("wlr_scene_xdg_surface_create for capture parent failed");
54 }
55 }
56
57 let popup = Box::into_raw(Box::new(XdgPopup {
58 wlr_popup,
59 tree,
60 capture_tree,
61 root_tree: root,
62 server: tree_server(root),
63 last_box: (0, 0, 0, 0),
64 destroy: std::mem::zeroed(),
65 commit: std::mem::zeroed(),
66 new_popup: std::mem::zeroed(),
67 reposition: std::mem::zeroed(),
68 }));
69
70 let destroy_ptr = &mut (*popup).destroy as *mut ffi::wl_listener as *mut WlListener;
71 (*destroy_ptr).notify = Some(handle_destroy);
72 wl_signal_add(ffi::river_wlr_xdg_popup_get_destroy_signal(wlr_popup), &mut (*popup).destroy);
73
74 let commit_ptr = &mut (*popup).commit as *mut ffi::wl_listener as *mut WlListener;
75 (*commit_ptr).notify = Some(handle_commit);
76 let wlr_surface = ffi::river_wlr_xdg_surface_get_surface(base_surface);
77 wl_signal_add(ffi::river_wlr_surface_get_commit_signal(wlr_surface), &mut (*popup).commit);
78
79 let new_popup_ptr = &mut (*popup).new_popup as *mut ffi::wl_listener as *mut WlListener;
80 (*new_popup_ptr).notify = Some(handle_new_popup);
81 wl_signal_add(ffi::river_wlr_xdg_surface_get_new_popup_signal(base_surface), &mut (*popup).new_popup);
82
83 let reposition_ptr = &mut (*popup).reposition as *mut ffi::wl_listener as *mut WlListener;
84 (*reposition_ptr).notify = Some(handle_reposition);
85 wl_signal_add(ffi::river_wlr_xdg_popup_get_reposition_signal(wlr_popup), &mut (*popup).reposition);
86
87 Ok(popup)
88 }
89 }
90
91 /// A popup coming, going, moving or changing size under a pointer that is
92 /// standing still changes what is under it, and nothing else would say so:
93 /// wlroots moves pointer focus on MOTION. So a menu closed under the pointer
94 /// left the window beneath it without focus until the pointer moved — the
95 /// designer's network menu turning into its Add Node list, a swipe back on
96 /// the list going nowhere — and a menu opened under it took no focus either.
97 /// The same deferred re-evaluation a toplevel's commit asks for when its
98 /// mapping moves (`InputManager::schedule_pointer_refresh`), coalesced and
99 /// run once the scene has caught up.
100 unsafe fn refresh_pointer_under(popup: *mut XdgPopup) {
101 let server = (*popup).server;
102 if !server.is_null() {
103 (*server).input_manager.schedule_pointer_refresh();
104 }
105 }
106
107 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
108 let popup = crate::container_of!(listener, XdgPopup, destroy);
109 refresh_pointer_under(popup);
110
111 wl_listener_remove(&mut (*popup).destroy);
112 wl_listener_remove(&mut (*popup).commit);
113 wl_listener_remove(&mut (*popup).new_popup);
114 wl_listener_remove(&mut (*popup).reposition);
115
116 let _ = Box::from_raw(popup);
117 }
118
119 unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
120 let popup = crate::container_of!(listener, XdgPopup, commit);
121 let base_surface = ffi::river_wlr_xdg_popup_get_base((*popup).wlr_popup);
122 if ffi::river_wlr_xdg_surface_get_initial_commit(base_surface) {
123 handle_reposition(&mut (*popup).reposition, std::ptr::null_mut());
124 return;
125 }
126 update_blur(popup, base_surface);
127 // Mapped (its first buffer), moved or resized: what is under the
128 // pointer changed. Not on every commit — a refresh sends the client a
129 // motion, and an animating menu commits every frame.
130 let surface = ffi::river_wlr_xdg_surface_get_surface(base_surface);
131 let (mut lx, mut ly) = (0, 0);
132 ffi::wlr_scene_node_coords((*popup).tree as *mut ffi::wlr_scene_node, &mut lx, &mut ly);
133 let at = (lx, ly, ffi::river_wlr_surface_get_width(surface), ffi::river_wlr_surface_get_height(surface));
134 if at != (*popup).last_box {
135 (*popup).last_box = at;
136 refresh_pointer_under(popup);
137 }
138 }
139
140 /// The server a scene tree belongs to, through its node data — a window's
141 /// or a shell surface's. Null for any other tree.
142 unsafe fn tree_server(tree: *mut ffi::wlr_scene_tree) -> *mut crate::server::Server {
143 if tree.is_null() {
144 return std::ptr::null_mut();
145 }
146 match crate::scene_node_data::SceneNodeData::from_node(tree as *mut ffi::wlr_scene_node) {
147 Some(node_data) => match node_data.data {
148 crate::scene_node_data::SceneNodeDataVal::Window(w) => (*w).server,
149 crate::scene_node_data::SceneNodeDataVal::ShellSurface(s) => (*s).server,
150 _ => std::ptr::null_mut(),
151 },
152 None => std::ptr::null_mut(),
153 }
154 }
155
156 /// Blur behind the popup as behind a window: a translucent menu is frosted
157 /// glass, and with no blur node it is a clear pane over whatever it opened
158 /// above. cce-ui paints its context-menu popup as the surface's ROOT plate
159 /// for exactly this — translucent, with the frost left to the compositor,
160 /// since the client has no backdrop to frost from inside its own popup.
161 ///
162 /// Masked by the surface's own alpha (`ignore_transparent`), so the menu's
163 /// rounded corners and its shadow margin stay clear; never the optimized
164 /// (cached) blur, which re-bakes on every change beneath a surface stacked
165 /// above windows — see `handle_layer_surface_commit`. Sized from the
166 /// surface, so it runs every commit: a popup is resized by its configure.
167 unsafe fn update_blur(popup: *mut XdgPopup, base_surface: *mut ffi::wlr_xdg_surface) {
168 let server = popup_server(popup);
169 if server.is_null() {
170 return;
171 }
172 let wlr_surface = ffi::river_wlr_xdg_surface_get_surface(base_surface);
173 if wlr_surface.is_null() {
174 return;
175 }
176 ffi::river_scene_node_enable_blur(
177 (*popup).tree as *mut ffi::wlr_scene_node,
178 (*server).wm.layout.window_blur,
179 false,
180 (*server).wm.layout.window_backdrop_blur_ignore_transparent,
181 0,
182 0,
183 ffi::river_wlr_surface_get_width(wlr_surface),
184 ffi::river_wlr_surface_get_height(wlr_surface),
185 0,
186 );
187 }
188
189 /// The server a popup belongs to, found through its parent's scene node —
190 /// a window or a shell surface. Null when the parent is neither.
191 unsafe fn popup_server(popup: *mut XdgPopup) -> *mut crate::server::Server {
192 let parent_tree = ffi::river_wlr_scene_tree_get_parent((*popup).tree);
193 if parent_tree.is_null() {
194 return std::ptr::null_mut();
195 }
196 match crate::scene_node_data::SceneNodeData::from_node(parent_tree as *mut ffi::wlr_scene_node) {
197 Some(node_data) => match node_data.data {
198 crate::scene_node_data::SceneNodeDataVal::Window(w) => (*w).server,
199 crate::scene_node_data::SceneNodeDataVal::ShellSurface(s) => (*s).server,
200 _ => std::ptr::null_mut(),
201 },
202 None => std::ptr::null_mut(),
203 }
204 }
205
206 unsafe extern "C" fn handle_new_popup(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
207 let popup = crate::container_of!(listener, XdgPopup, new_popup);
208 let wlr_xdg_popup = data as *mut ffi::wlr_xdg_popup;
209
210 if let Err(e) = XdgPopup::create(wlr_xdg_popup, (*popup).tree, (*popup).capture_tree, (*popup).root_tree) {
211 log::error!("Failed to create nested popup: {}", e);
212 ffi::wl_resource_post_no_memory((*wlr_xdg_popup).resource);
213 }
214 }
215
216 unsafe extern "C" fn handle_reposition(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
217 let popup = crate::container_of!(listener, XdgPopup, reposition);
218
219 let mut parent_lx: i32 = 0;
220 let mut parent_ly: i32 = 0;
221 let parent_tree = ffi::river_wlr_scene_tree_get_parent((*popup).tree);
222 if parent_tree.is_null() {
223 return;
224 }
225
226 ffi::wlr_scene_node_coords(parent_tree as *mut ffi::wlr_scene_node, &mut parent_lx, &mut parent_ly);
227
228 let mut anchor = std::mem::zeroed();
229 ffi::river_wlr_xdg_popup_get_anchor_rect((*popup).wlr_popup, &mut anchor);
230 anchor.x += parent_lx;
231 anchor.y += parent_ly;
232
233 let server = popup_server(popup);
234 if server.is_null() {
235 return;
236 }
237
238 let wlr_output = (*server).om.max_overlap_output(&anchor);
239 if wlr_output.is_null() {
240 return;
241 }
242
243 // The box goes to wlroots in the ROOT surface's coordinates. For a
244 // top-level menu the parent is the root; for a submenu it is the
245 // menu, and measuring from the menu's corner (as this did until
246 // 2026-09-25) shifted the screen up and left by the menu's offset in
247 // the window, so a tall submenu slid past the real top edge and the
248 // flip decisions were made against the wrong right edge.
249 let mut root_lx: i32 = 0;
250 let mut root_ly: i32 = 0;
251 ffi::wlr_scene_node_coords((*popup).root_tree as *mut ffi::wlr_scene_node, &mut root_lx, &mut root_ly);
252
253 let mut constraint = std::mem::zeroed();
254 ffi::wlr_output_layout_get_box((*server).om.output_layout, wlr_output, &mut constraint);
255 constraint.x -= root_lx;
256 constraint.y -= root_ly;
257
258 ffi::wlr_xdg_popup_unconstrain_from_box((*popup).wlr_popup, &mut constraint);
259 ffi::wlr_xdg_surface_schedule_configure(ffi::river_wlr_xdg_popup_get_base((*popup).wlr_popup));
260 }