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