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

src/server/xwayland_override_redirect.rs (16.4K)

  1 // SPDX-FileCopyrightText: © 2020 The River Developers
  2 // SPDX-License-Identifier: GPL-3.0-only
  3 
  4 use crate::ffi;
  5 use crate::server::{Server, WlListener, wl_signal_add};
  6 use crate::xwayland_window::XwaylandWindow;
  7 
  8 #[repr(C)]
  9 pub struct XwaylandOverrideRedirect {
 10     pub server: *mut Server,
 11     pub xsurface: *mut ffi::wlr_xwayland_surface,
 12     pub surface_tree: *mut ffi::wlr_scene_tree,
 13 
 14     pub request_configure: ffi::wl_listener,
 15     pub destroy: ffi::wl_listener,
 16     pub set_override_redirect: ffi::wl_listener,
 17     pub associate: ffi::wl_listener,
 18     pub dissociate: ffi::wl_listener,
 19 
 20     pub map: ffi::wl_listener,
 21     pub unmap: ffi::wl_listener,
 22 
 23     pub set_geometry: ffi::wl_listener,
 24     /// Re-applies the 1/scale dest size after every commit (the scene's
 25     /// commit handler resets it) — see `Window`'s commit handler for why a
 26     /// per-frame pass alone leaves a hit-testing gap.
 27     pub commit: ffi::wl_listener,
 28 }
 29 
 30 unsafe fn connect_listener(
 31     signal: *mut ffi::wl_signal,
 32     listener: *mut ffi::wl_listener,
 33     callback: unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void),
 34 ) {
 35     let wl_lis = listener as *mut WlListener;
 36     (*wl_lis).notify = Some(callback);
 37     wl_signal_add(signal, listener);
 38 }
 39 
 40 unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
 41     let prev = (*listener).link.prev;
 42     let next = (*listener).link.next;
 43     if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
 44         ffi::wl_list_remove(&mut (*listener).link);
 45         (*listener).link.prev = std::ptr::null_mut();
 46         (*listener).link.next = std::ptr::null_mut();
 47     }
 48 }
 49 
 50 impl XwaylandOverrideRedirect {
 51     pub unsafe fn create(
 52         xsurface: *mut ffi::wlr_xwayland_surface,
 53         server: *mut Server,
 54     ) -> Result<(), &'static str> {
 55         let title_ptr = (*xsurface).title;
 56         let class_ptr = (*xsurface).class;
 57         log::debug!(
 58             "new xwayland override redirect: title='{:?}', class='{:?}'",
 59             if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") },
 60             if class_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(class_ptr).to_str().unwrap_or("") }
 61         );
 62 
 63         let override_redirect = Box::new(XwaylandOverrideRedirect {
 64             server,
 65             xsurface,
 66             surface_tree: std::ptr::null_mut(),
 67             request_configure: std::mem::zeroed(),
 68             destroy: std::mem::zeroed(),
 69             set_override_redirect: std::mem::zeroed(),
 70             associate: std::mem::zeroed(),
 71             dissociate: std::mem::zeroed(),
 72             map: std::mem::zeroed(),
 73             unmap: std::mem::zeroed(),
 74             set_geometry: std::mem::zeroed(),
 75             commit: std::mem::zeroed(),
 76         });
 77 
 78         let raw = Box::into_raw(override_redirect);
 79         (*server).wm.override_redirects.push(raw);
 80 
 81         connect_listener(&mut (*xsurface).events.request_configure, &mut (*raw).request_configure, handle_request_configure);
 82         connect_listener(&mut (*xsurface).events.destroy, &mut (*raw).destroy, handle_destroy);
 83         connect_listener(&mut (*xsurface).events.set_override_redirect, &mut (*raw).set_override_redirect, handle_set_override_redirect);
 84         connect_listener(&mut (*xsurface).events.associate, &mut (*raw).associate, handle_associate);
 85         connect_listener(&mut (*xsurface).events.dissociate, &mut (*raw).dissociate, handle_dissociate);
 86 
 87         if !(*xsurface).surface.is_null() {
 88             handle_associate_impl(raw);
 89             if ffi::river_wlr_surface_is_mapped((*xsurface).surface) {
 90                 handle_map_impl(raw);
 91             }
 92         }
 93 
 94         Ok(())
 95     }
 96 
 97     /// Put the surface tree where X11 says the window is, in logical
 98     /// pixels (`xwayland_window::x11_scale`).
 99     pub unsafe fn place(&mut self) {
100         if self.surface_tree.is_null() {
101             return;
102         }
103         let s = crate::xwayland_window::x11_scale_for(self.server, self.xsurface);
104         ffi::wlr_scene_node_set_position(
105             self.surface_tree as *mut ffi::wlr_scene_node,
106             crate::xwayland_window::from_x11((*self.xsurface).x as i32, s),
107             crate::xwayland_window::from_x11((*self.xsurface).y as i32, s),
108         );
109     }
110 
111     /// Draw the physical-pixel X11 buffer at 1/scale. Runs from the
112     /// per-frame pass (output.rs) because the scene's commit listener
113     /// resets a committed buffer's dest size; every setter is change-checked.
114     pub unsafe fn apply_x11_scale(&mut self) {
115         if self.surface_tree.is_null() {
116             return;
117         }
118         let s = crate::xwayland_window::x11_scale_for(self.server, self.xsurface) as f64;
119         if s == 1.0 {
120             return;
121         }
122         unsafe extern "C" fn iter(
123             buffer: *mut ffi::wlr_scene_buffer,
124             _sx: i32,
125             _sy: i32,
126             user_data: *mut std::ffi::c_void,
127         ) {
128             let inv = *(user_data as *const f64);
129             let node = buffer as *mut ffi::wlr_scene_node;
130             let surface = ffi::river_scene_node_get_surface(node);
131             if surface.is_null() {
132                 return;
133             }
134             let w = ffi::river_wlr_surface_get_width(surface);
135             let h = ffi::river_wlr_surface_get_height(surface);
136             ffi::river_scene_buffer_set_dest_size_if_changed(
137                 buffer,
138                 (w as f64 * inv).round() as i32,
139                 (h as f64 * inv).round() as i32,
140             );
141             ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, inv);
142         }
143         let inv = 1.0 / s;
144         ffi::wlr_scene_node_for_each_buffer(
145             self.surface_tree as *mut ffi::wlr_scene_node,
146             Some(iter),
147             &inv as *const f64 as *mut std::ffi::c_void,
148         );
149     }
150 
151     /// Give the keyboard to this popup when it asks for it, but never take
152     /// it from another client: only when the seat holds nothing, or holds
153     /// a window or popup of the popup's own process.
154     ///
155     /// wlroots' `override_redirect_wants_focus` only rules out the window
156     /// types a popup is expected to declare, and Wine declares none of
157     /// them: its tooltips and menus alike are `_NET_WM_WINDOW_TYPE_DIALOG`
158     /// with `WM_TAKE_FOCUS`, and their Win32 styles (`_WINE_HWND_STYLE`)
159     /// match too. So a tray icon's tooltip — which Wine's `explorer.exe`
160     /// shows on every click the XEmbed bridge forwards — took the keyboard
161     /// from whatever the user was typing in until 2026-09-27. An X server's
162     /// own window manager never focuses an override-redirect window at
163     /// all; the cost of holding back here is keyboard navigation in a menu
164     /// whose app has no focused window, e.g. a tray menu, which the pointer
165     /// still drives.
166     pub unsafe fn focus_if_desired(&self) {
167         if (*self.server).lock_manager.state != crate::lock_manager::LockState::Unlocked {
168             return;
169         }
170         if !ffi::wlr_xwayland_surface_override_redirect_wants_focus(self.xsurface)
171             || ffi::wlr_xwayland_surface_icccm_input_model(self.xsurface)
172                 == ffi::wlr_xwayland_icccm_input_model_WLR_ICCCM_INPUT_MODEL_NONE
173         {
174             return;
175         }
176         let seat = (*self.server).input_manager.default_seat;
177         if seat.is_null() {
178             return;
179         }
180         let pid = (*self.xsurface).pid;
181         match (*seat).focused {
182             crate::seat::Focus::Window(window) if !window.is_null() => {
183                 if let crate::window::WindowImpl::Xwayland(xwindow) = (*window).impl_type {
184                     if !xwindow.is_null() && (*(*xwindow).xsurface).pid == pid {
185                         (*seat).keyboard_enter_or_leave((*self.xsurface).surface);
186                         return;
187                     }
188                 }
189                 log::debug!("override redirect (pid {pid}) mapped unfocused: another client holds the keyboard");
190                 return;
191             }
192             crate::seat::Focus::None => {}
193             crate::seat::Focus::OverrideRedirect(or) if !or.is_null() && (*(*or).xsurface).pid == pid => {}
194             _ => {
195                 log::debug!("override redirect (pid {pid}) mapped unfocused: another client holds the keyboard");
196                 return;
197             }
198         }
199         (*seat).focus(crate::seat::Focus::OverrideRedirect(self as *const _ as *mut _));
200     }
201 }
202 
203 unsafe extern "C" fn handle_request_configure(_listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
204     let event = data as *mut ffi::wlr_xwayland_surface_configure_event;
205     ffi::wlr_xwayland_surface_configure((*event).surface, (*event).x, (*event).y, (*event).width, (*event).height);
206 }
207 
208 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
209     let or = crate::container_of!(listener, XwaylandOverrideRedirect, destroy);
210     (*(*or).server).wm.override_redirects.retain(|&p| p != or);
211 
212     wl_listener_remove_safe(&mut (*or).request_configure);
213     wl_listener_remove_safe(&mut (*or).destroy);
214     wl_listener_remove_safe(&mut (*or).associate);
215     wl_listener_remove_safe(&mut (*or).dissociate);
216     wl_listener_remove_safe(&mut (*or).set_override_redirect);
217 
218     let _ = Box::from_raw(or);
219 }
220 
221 unsafe extern "C" fn handle_associate(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
222     let or = crate::container_of!(listener, XwaylandOverrideRedirect, associate);
223     handle_associate_impl(or);
224 }
225 
226 unsafe fn handle_associate_impl(or: *mut XwaylandOverrideRedirect) {
227     let surface = (*(*or).xsurface).surface;
228     if !surface.is_null() {
229         connect_listener(
230             ffi::river_wlr_surface_get_map_signal(surface),
231             &mut (*or).map,
232             handle_map,
233         );
234         connect_listener(
235             ffi::river_wlr_surface_get_unmap_signal(surface),
236             &mut (*or).unmap,
237             handle_unmap,
238         );
239     }
240 }
241 
242 unsafe extern "C" fn handle_dissociate(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
243     let or = crate::container_of!(listener, XwaylandOverrideRedirect, dissociate);
244     wl_listener_remove_safe(&mut (*or).map);
245     wl_listener_remove_safe(&mut (*or).unmap);
246 }
247 
248 unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
249     let or = crate::container_of!(listener, XwaylandOverrideRedirect, map);
250     handle_map_impl(or);
251 }
252 
253 /// WM_CLASS of the XEmbed tray bridge's container windows
254 /// (`cce-status-interface`'s `cce-xembed-tray`).
255 const XEMBED_TRAY_CLASS: &str = "cce-xembed-tray";
256 
257 /// Whether this override-redirect window is one of the tray bridge's
258 /// containers. Each holds a legacy X11 tray icon the bridge adopted and
259 /// republishes as a StatusNotifierItem; X needs it mapped for the icon to
260 /// draw at all, but the icon is shown in the status bar, so the window
261 /// itself must never be. The bridge also gives it an empty input region,
262 /// so X never routes the pointer into it either.
263 unsafe fn is_xembed_tray_container(xsurface: *mut ffi::wlr_xwayland_surface) -> bool {
264     let class = (*xsurface).class;
265     !class.is_null() && std::ffi::CStr::from_ptr(class).to_bytes() == XEMBED_TRAY_CLASS.as_bytes()
266 }
267 
268 unsafe fn handle_map_impl(or: *mut XwaylandOverrideRedirect) {
269     // No scene node at all: nothing to draw, hit-test or focus. Unmap
270     // copes with the missing tree and the unconnected listeners.
271     if is_xembed_tray_container((*or).xsurface) {
272         log::debug!("xembed tray container mapped; not shown");
273         return;
274     }
275     let surface = (*(*or).xsurface).surface;
276     let override_redirect_tree = (*(*or).server).scene.layers.override_redirect;
277 
278     let surface_tree = ffi::wlr_scene_subsurface_tree_create(override_redirect_tree, surface);
279     if surface_tree.is_null() {
280         log::error!("out of memory creating subsurface tree for override redirect");
281         let surface_resource = ffi::river_wlr_surface_get_resource(surface);
282         let client = ffi::wl_resource_get_client(surface_resource);
283         ffi::wl_client_post_no_memory(client);
284         return;
285     }
286     (*or).surface_tree = surface_tree;
287 
288     crate::scene_node_data::SceneNodeData::attach(
289         surface_tree as *mut ffi::wlr_scene_node,
290         crate::scene_node_data::SceneNodeDataVal::OverrideRedirect(or as *mut _),
291     );
292 
293     ffi::river_wlr_surface_set_data(surface, surface_tree as *mut ffi::wlr_scene_node as *mut _);
294 
295     (*or).place();
296     (*or).apply_x11_scale();
297 
298     connect_listener(&mut (*(*or).xsurface).events.set_geometry, &mut (*or).set_geometry, handle_set_geometry);
299     // After the scene's subsurface tree, so this runs after its reset.
300     connect_listener(ffi::river_wlr_surface_get_commit_signal(surface), &mut (*or).commit, handle_commit);
301 
302     (*or).focus_if_desired();
303 }
304 
305 unsafe extern "C" fn handle_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
306     let or = crate::container_of!(listener, XwaylandOverrideRedirect, unmap);
307 
308     wl_listener_remove_safe(&mut (*or).set_geometry);
309     wl_listener_remove_safe(&mut (*or).commit);
310 
311     let surface = (*(*or).xsurface).surface;
312     if !surface.is_null() {
313         ffi::river_wlr_surface_set_data(surface, std::ptr::null_mut());
314     }
315 
316     if !(*or).surface_tree.is_null() {
317         ffi::wlr_scene_node_destroy((*or).surface_tree as *mut ffi::wlr_scene_node);
318         (*or).surface_tree = std::ptr::null_mut();
319     }
320 
321     let default_seat = (*(*or).server).input_manager.default_seat;
322     if !default_seat.is_null() {
323         if let crate::seat::Focus::Window(window) = (*default_seat).focused {
324             if !window.is_null() && matches!((*window).impl_type, crate::window::WindowImpl::Xwayland(_)) {
325                 let parent_xwindow = (*window).impl_type;
326                 if let crate::window::WindowImpl::Xwayland(xwindow) = parent_xwindow {
327                     if !xwindow.is_null()
328                         && (*(*xwindow).xsurface).pid == (*(*or).xsurface).pid
329                         && ffi::river_wlr_seat_get_keyboard_focused_surface((*default_seat).wlr_seat) == surface
330                     {
331                         (*default_seat).keyboard_enter_or_leave((*window).root_surface());
332                     }
333                 }
334             }
335         }
336     }
337 
338     (*(*or).server).wm.dirty_windowing();
339 }
340 
341 unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
342     let or = crate::container_of!(listener, XwaylandOverrideRedirect, commit);
343     (*or).apply_x11_scale();
344 }
345 
346 unsafe extern "C" fn handle_set_geometry(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
347     let or = crate::container_of!(listener, XwaylandOverrideRedirect, set_geometry);
348     (*or).place();
349 }
350 
351 unsafe extern "C" fn handle_set_override_redirect(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
352     let or = crate::container_of!(listener, XwaylandOverrideRedirect, set_override_redirect);
353     let xsurface = (*or).xsurface;
354     log::debug!("xwayland surface unset override redirect");
355     assert!(!(*xsurface).override_redirect);
356 
357     let surface = (*xsurface).surface;
358     if !surface.is_null() {
359         if ffi::river_wlr_surface_is_mapped(surface) {
360             // handle unmap inline
361             wl_listener_remove_safe(&mut (*or).set_geometry);
362     wl_listener_remove_safe(&mut (*or).commit);
363             ffi::river_wlr_surface_set_data(surface, std::ptr::null_mut());
364             if !(*or).surface_tree.is_null() {
365                 ffi::wlr_scene_node_destroy((*or).surface_tree as *mut ffi::wlr_scene_node);
366                 (*or).surface_tree = std::ptr::null_mut();
367             }
368         }
369         wl_listener_remove_safe(&mut (*or).map);
370         wl_listener_remove_safe(&mut (*or).unmap);
371     }
372 
373     let server = (*or).server;
374 
375     // Destroy this OR instance. Drop it from the list first, as
376     // `handle_destroy` does: the per-frame `apply_x11_scale` pass walks
377     // `override_redirects`, and a freed entry left there crashed the
378     // compositor on its next frame (2026-09-26, a Wine tray icon handed
379     // back by the XEmbed bridge and remapped as a managed window;
380     // `verify/clients` `or-flip` reproduces it).
381     (*server).wm.override_redirects.retain(|&p| p != or);
382     wl_listener_remove_safe(&mut (*or).request_configure);
383     wl_listener_remove_safe(&mut (*or).destroy);
384     wl_listener_remove_safe(&mut (*or).associate);
385     wl_listener_remove_safe(&mut (*or).dissociate);
386     wl_listener_remove_safe(&mut (*or).set_override_redirect);
387     let _ = Box::from_raw(or);
388 
389     if let Err(e) = XwaylandWindow::create(xsurface, server) {
390         log::error!("Failed to transition OR to XwaylandWindow: {}", e);
391     }
392 }