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 }