Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/xwayland_window.rs (50.5K)
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::window::{Window, WindowImpl, WindowState};
7 use crate::xwayland_override_redirect::XwaylandOverrideRedirect;
8
9 #[repr(C)]
10 pub struct XwaylandWindow {
11 pub window: *mut Window,
12 pub xsurface: *mut ffi::wlr_xwayland_surface,
13 pub surface_tree: *mut ffi::wlr_scene_tree,
14
15 pub destroy: ffi::wl_listener,
16 pub request_configure: ffi::wl_listener,
17 pub set_override_redirect: ffi::wl_listener,
18 pub associate: ffi::wl_listener,
19 pub dissociate: ffi::wl_listener,
20 pub set_size_hints: ffi::wl_listener,
21 pub set_title: ffi::wl_listener,
22 pub set_class: ffi::wl_listener,
23 pub set_parent: ffi::wl_listener,
24 pub set_decorations: ffi::wl_listener,
25 pub request_maximize: ffi::wl_listener,
26 pub request_fullscreen: ffi::wl_listener,
27 pub request_minimize: ffi::wl_listener,
28
29 pub map: ffi::wl_listener,
30 pub unmap: ffi::wl_listener,
31
32 /// The last geometry this compositor handed to X through
33 /// `send_configure`, physical pixels; `None` until the first one. See
34 /// `needs_configure` for why this is kept apart from the wlroots mirror.
35 pub sent_geom: Option<X11Geom>,
36 }
37
38 /// A window geometry in X11 root coordinates — physical pixels under
39 /// `xwayland_hidpi` (see `x11_scale`), the same units as the
40 /// `wlr_xwayland_surface` fields it is compared against.
41 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
42 pub struct X11Geom {
43 pub x: i16,
44 pub y: i16,
45 pub width: u16,
46 pub height: u16,
47 }
48
49 /// Whether `wanted` has to be sent to X, given what wlroots reports the
50 /// window's geometry to be (`reported`) and the last geometry this
51 /// compositor sent (`sent`).
52 ///
53 /// Comparing against the wlroots mirror alone was the bug: the saved-state
54 /// restore (`Window::try_restore`) pre-writes the mirror's width/height to
55 /// the saved size so the first frame renders at it, which makes the mirror
56 /// a statement of what the compositor wants X to have, not what X has. A
57 /// window saved fullscreen restored at exactly the fullscreen size then
58 /// looked already-configured, the configure was skipped, and the real X
59 /// window stayed at its natural size. So a geometry is also considered
60 /// unsent until this compositor has actually sent it once; after that the
61 /// mirror is what catches X changing the geometry on its own (a
62 /// ConfigureNotify updates it), which the sent record cannot see.
63 pub fn needs_configure(wanted: X11Geom, reported: X11Geom, sent: Option<X11Geom>) -> bool {
64 wanted != reported || sent != Some(wanted)
65 }
66
67 unsafe fn connect_listener(
68 signal: *mut ffi::wl_signal,
69 listener: *mut ffi::wl_listener,
70 callback: unsafe extern "C" fn(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void),
71 ) {
72 let wl_lis = listener as *mut WlListener;
73 (*wl_lis).notify = Some(callback);
74 wl_signal_add(signal, listener);
75 }
76
77 unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
78 let prev = (*listener).link.prev;
79 let next = (*listener).link.next;
80 if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
81 ffi::wl_list_remove(&mut (*listener).link);
82 (*listener).link.prev = std::ptr::null_mut();
83 (*listener).link.next = std::ptr::null_mut();
84 }
85 }
86
87 /// Wine draws its own frame in a margin around the window; the compositor
88 /// hides it by oversizing and offsetting the X window. Logical pixels.
89 pub const WINE_MARGIN: i32 = 16;
90
91 /// The factor between X11 root coordinates and the logical layout.
92 ///
93 /// With `xwayland_hidpi` on (the default) the xdg-output global is hidden
94 /// from Xwayland (`server.rs`), so it sizes its screen from the wl_output
95 /// MODE — the physical pixel grid — and X11 is a physical-pixel world: a
96 /// HiDPI-aware X11 app (Houdini, any Qt 6 app reading Xft.dpi) renders at
97 /// full resolution and its surfaces are drawn at 1/scale
98 /// (`Window::x11_buffer_scale`), sharp instead of upscaled from logical size.
99 /// Every position and size crossing into or out of X11 converts through
100 /// `to_x11` / `from_x11`; the window's own geometry stays logical.
101 ///
102 /// Off, X11 is the logical layout (Xwayland reads xdg-output) and the
103 /// factor is 1. Multi-output with differing scales is not a case X11 can
104 /// express — the first output's scale stands for the screen. A single
105 /// window can opt out through `xwayland_hidpi_except` — see `x11_scale_for`,
106 /// which every per-window caller goes through; this screen-wide value is
107 /// only for what has no window, like the Xft.dpi pushed at Xwayland-ready.
108 ///
109 /// The factor must survive the output going away. A lid-close suspend
110 /// destroys the DRM output and re-creates it on resume, and X11 windows
111 /// live on through it: any geometry read back from X while no output
112 /// exists (`Window::render_finish`, `XwaylandWindow::configure`) is still
113 /// in physical pixels, and dividing it by 1 instead of the panel's scale
114 /// records a window twice its logical size — which the first configure
115 /// after resume then multiplies by the real scale again, handing X a
116 /// window four times too big. So the last scale an output reported is
117 /// remembered and stands in while there is none.
118 pub unsafe fn x11_scale(server: *mut crate::server::Server) -> f32 {
119 if server.is_null() || !(*server).wm.xwayland_hidpi {
120 return 1.0;
121 }
122 let mut current = None;
123 let link = (*server).om.outputs.next;
124 if link != &mut (*server).om.outputs as *mut ffi::wl_list {
125 let output = crate::container_of!(link, crate::output::Output, link);
126 let scale = (*output).current.scale;
127 if scale > 0.0 {
128 current = Some(scale);
129 }
130 }
131 resolve_x11_scale(current, &LAST_X11_SCALE)
132 }
133
134 /// The scale of the last output `x11_scale` saw, as `f32` bits; 0 until an
135 /// output has reported one.
136 static LAST_X11_SCALE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
137
138 /// `x11_scale` without the FFI: the live output's scale when there is one
139 /// (remembering it in `last`), else the remembered one, else 1.
140 pub fn resolve_x11_scale(current: Option<f32>, last: &std::sync::atomic::AtomicU32) -> f32 {
141 use std::sync::atomic::Ordering;
142 if let Some(scale) = current {
143 last.store(scale.to_bits(), Ordering::Relaxed);
144 return scale;
145 }
146 let remembered = f32::from_bits(last.load(Ordering::Relaxed));
147 if remembered > 0.0 { remembered } else { 1.0 }
148 }
149
150 /// X11 clients answer an output coming or going — Xwayland re-creates its
151 /// screen and RandR tells them — by re-asserting a geometry of their own:
152 /// Houdini (Qt) asks for the whole panel after a resume from suspend, and a
153 /// floating window's unsolicited size request is otherwise honoured
154 /// verbatim (`handle_request_configure`). For a moment after any output
155 /// change those requests are answered with the window's own geometry
156 /// instead, the way a tiled window's always are, so the size the user set
157 /// survives the screen change.
158 static OUTPUT_CHANGE_GRACE_UNTIL: std::sync::Mutex<Option<std::time::Instant>> = std::sync::Mutex::new(None);
159
160 /// How long after an output is created or destroyed to hold floating X11
161 /// windows at their own size. Xwayland's RandR update and the client's
162 /// reaction land within the same second in practice; this leaves room for
163 /// a slow client.
164 const OUTPUT_CHANGE_GRACE: std::time::Duration = std::time::Duration::from_secs(3);
165
166 /// Called when an output is created or destroyed: (re)starts the grace
167 /// window during which floating X11 windows keep their size.
168 pub fn note_output_change() {
169 if let Ok(mut deadline) = OUTPUT_CHANGE_GRACE_UNTIL.lock() {
170 *deadline = Some(std::time::Instant::now() + OUTPUT_CHANGE_GRACE);
171 }
172 }
173
174 /// True while inside the grace window begun by `note_output_change`.
175 pub fn in_output_change_grace() -> bool {
176 OUTPUT_CHANGE_GRACE_UNTIL
177 .lock()
178 .ok()
179 .and_then(|deadline| *deadline)
180 .is_some_and(|deadline| std::time::Instant::now() < deadline)
181 }
182
183 /// `x11_scale` for one X11 surface: 1 when the window is named in
184 /// `window_manager { xwayland_hidpi_except }`, else the screen's factor.
185 ///
186 /// The screen Xwayland shows is one thing for every client, so an exempt
187 /// window still SEES a physical-pixel root; what changes is what the
188 /// compositor does with it. Its configures go out in logical pixels and its
189 /// buffer is drawn at 1 (`Window::x11_buffer_scale`), the pre-`xwayland_hidpi`
190 /// arrangement — so a borderless-fullscreen game that asks for the whole
191 /// root is answered with the logical size and renders that many pixels,
192 /// not scale² as many. Matched by WM_CLASS class, WM_CLASS instance or
193 /// title (`hidpi_exempt`), re-evaluated on every use so a title that
194 /// arrives after the first configure still takes effect.
195 pub unsafe fn x11_scale_for(
196 server: *mut crate::server::Server,
197 xsurface: *const ffi::wlr_xwayland_surface,
198 ) -> f32 {
199 if server.is_null() || !(*server).wm.xwayland_hidpi {
200 return 1.0;
201 }
202 if !xsurface.is_null() && !(*server).wm.xwayland_hidpi_except.is_empty() {
203 let text = |p: *const libc::c_char| -> String {
204 if p.is_null() { String::new() } else { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
205 };
206 let class = text((*xsurface).class);
207 let instance = text((*xsurface).instance);
208 let title = text((*xsurface).title);
209 if hidpi_exempt(&(*server).wm.xwayland_hidpi_except, &class, &instance, &title) {
210 return 1.0;
211 }
212 }
213 x11_scale(server)
214 }
215
216 /// The factor an X11 client's CURSOR is drawn at: the screen's X11 factor
217 /// for any X11 surface, 1 for anything that is not X11.
218 ///
219 /// What a cursor request needs. Deliberately NOT `x11_scale_for`: the
220 /// `xwayland_hidpi_except` exemption is about a game's window pixels — it
221 /// sizes itself to the root ignoring DPI, so its buffer is drawn at 1 —
222 /// but its cursor comes from the toolkit or Wine underneath, which follow
223 /// the DPI this compositor publishes (Xft.dpi / Xcursor.size at 96×scale
224 /// and 24×scale). Wine at LogPixels 192 hands Trackmania a 64px arrow;
225 /// drawn in the logical world with the window it was twice the desktop's
226 /// cursor. Every X11 cursor is a physical-pixel bitmap, exempt window or
227 /// not.
228 pub unsafe fn x11_scale_for_surface(
229 server: *mut crate::server::Server,
230 surface: *mut ffi::wlr_surface,
231 ) -> f32 {
232 if surface.is_null() {
233 return 1.0;
234 }
235 let root = ffi::wlr_surface_get_root_surface(surface);
236 if root.is_null() {
237 return 1.0;
238 }
239 let xsurface = ffi::wlr_xwayland_surface_try_from_wlr_surface(root);
240 if xsurface.is_null() {
241 return 1.0;
242 }
243 x11_scale(server)
244 }
245
246 /// Whether any of `patterns` names this window: each is tried against the
247 /// WM_CLASS class, the WM_CLASS instance and the title with the
248 /// `app_id_matches` rules (case-insensitive, `*` wildcards). Empty fields
249 /// never match.
250 /// Whether `window` is an X11 window named in `xwayland_hidpi_except` — a
251 /// full-screen X11 game, by the key's definition. Such a window sizes and
252 /// places itself to the screen, and the compositor stays out of its way:
253 /// no saved-state restore (`Window::try_restore`); its own requests are
254 /// granted, clamped to the output's logical box since it sees a
255 /// physical-pixel root (`handle_request_configure`); a compositor
256 /// fullscreen overrides its size and survives Wine's withdrawal of the
257 /// state (`handle_request_fullscreen`). Its cursor is NOT exempt — see
258 /// `x11_scale_for_surface`.
259 pub unsafe fn window_is_hidpi_exempt(window: *const crate::window::Window) -> bool {
260 if window.is_null() {
261 return false;
262 }
263 let crate::window::WindowImpl::Xwayland(xwindow) = (*window).impl_type else {
264 return false;
265 };
266 if xwindow.is_null() || (*xwindow).xsurface.is_null() {
267 return false;
268 }
269 let server = (*window).server;
270 if server.is_null() || !(*server).wm.xwayland_hidpi || (*server).wm.xwayland_hidpi_except.is_empty() {
271 return false;
272 }
273 let xsurface = (*xwindow).xsurface;
274 let text = |p: *const libc::c_char| -> String {
275 if p.is_null() { String::new() } else { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
276 };
277 hidpi_exempt(
278 &(*server).wm.xwayland_hidpi_except,
279 &text((*xsurface).class),
280 &text((*xsurface).instance),
281 &text((*xsurface).title),
282 )
283 }
284
285 pub fn hidpi_exempt(patterns: &[String], class: &str, instance: &str, title: &str) -> bool {
286 use crate::window_manager::app_id_matches;
287 patterns.iter().any(|p| {
288 [class, instance, title]
289 .iter()
290 .any(|field| !field.is_empty() && app_id_matches(p, field))
291 })
292 }
293
294 pub fn to_x11(logical: i32, scale: f32) -> i32 {
295 (logical as f32 * scale).round() as i32
296 }
297
298 pub fn from_x11(x11: i32, scale: f32) -> i32 {
299 (x11 as f32 / scale).round() as i32
300 }
301
302 /// The nearest X11 value the LOGICAL grid can express — `to_x11` of
303 /// `from_x11`.
304 ///
305 /// At scale 2 an odd X11 coordinate has no logical integer: 181 reads back as
306 /// 91 and goes out again as 182. The window's geometry is logical, so
307 /// granting a client's request verbatim and storing the rounded logical means
308 /// the next configure hands X a value a pixel off the one it asked for —
309 /// which the client reads as an unrequested move and answers with another
310 /// request, a pixel further along each time. Granting the snapped value makes
311 /// the geometry sent and the geometry the compositor's own model reproduces
312 /// the same number, so the round trip is stable however often it repeats.
313 ///
314 /// At scale 1 this is the identity, so nothing outside `xwayland_hidpi`
315 /// changes.
316 pub fn snap_x11(x11: i32, scale: f32) -> i32 {
317 to_x11(from_x11(x11, scale), scale)
318 }
319
320 impl XwaylandWindow {
321 pub unsafe fn create(
322 xsurface: *mut ffi::wlr_xwayland_surface,
323 server: *mut Server,
324 ) -> Result<(), &'static str> {
325 let title_ptr = (*xsurface).title;
326 let class_ptr = (*xsurface).class;
327 log::debug!(
328 "new xwayland window: title='{:?}', class='{:?}'",
329 if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") },
330 if class_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(class_ptr).to_str().unwrap_or("") }
331 );
332
333 let window = Window::create(WindowImpl::Xwayland(std::ptr::null_mut()), server)?;
334
335 let xwindow = Box::new(XwaylandWindow {
336 window,
337 xsurface,
338 surface_tree: std::ptr::null_mut(),
339 destroy: std::mem::zeroed(),
340 request_configure: std::mem::zeroed(),
341 set_override_redirect: std::mem::zeroed(),
342 associate: std::mem::zeroed(),
343 dissociate: std::mem::zeroed(),
344 set_size_hints: std::mem::zeroed(),
345 set_title: std::mem::zeroed(),
346 set_class: std::mem::zeroed(),
347 set_parent: std::mem::zeroed(),
348 set_decorations: std::mem::zeroed(),
349 request_maximize: std::mem::zeroed(),
350 request_fullscreen: std::mem::zeroed(),
351 request_minimize: std::mem::zeroed(),
352 map: std::mem::zeroed(),
353 unmap: std::mem::zeroed(),
354 sent_geom: None,
355 });
356
357 let raw = Box::into_raw(xwindow);
358 (*window).set_impl(WindowImpl::Xwayland(raw));
359
360 (*xsurface).data = raw as *mut std::ffi::c_void;
361
362 connect_listener(&mut (*xsurface).events.destroy, &mut (*raw).destroy, handle_destroy);
363 connect_listener(&mut (*xsurface).events.associate, &mut (*raw).associate, handle_associate);
364 connect_listener(&mut (*xsurface).events.dissociate, &mut (*raw).dissociate, handle_dissociate);
365 connect_listener(&mut (*xsurface).events.request_configure, &mut (*raw).request_configure, handle_request_configure);
366 connect_listener(&mut (*xsurface).events.set_override_redirect, &mut (*raw).set_override_redirect, handle_set_override_redirect);
367 // connect_listener(&mut (*xsurface).events.set_size_hints, &mut (*raw).set_size_hints, handle_set_size_hints);
368 connect_listener(&mut (*xsurface).events.set_title, &mut (*raw).set_title, handle_set_title);
369 connect_listener(&mut (*xsurface).events.set_class, &mut (*raw).set_class, handle_set_class);
370 connect_listener(&mut (*xsurface).events.set_parent, &mut (*raw).set_parent, handle_set_parent);
371 connect_listener(&mut (*xsurface).events.set_decorations, &mut (*raw).set_decorations, handle_set_decorations);
372 connect_listener(&mut (*xsurface).events.request_maximize, &mut (*raw).request_maximize, handle_request_maximize);
373 connect_listener(&mut (*xsurface).events.request_fullscreen, &mut (*raw).request_fullscreen, handle_request_fullscreen);
374 connect_listener(&mut (*xsurface).events.request_minimize, &mut (*raw).request_minimize, handle_request_minimize);
375
376 if !(*xsurface).surface.is_null() {
377 handle_associate_impl(raw);
378 if ffi::river_wlr_surface_is_mapped((*xsurface).surface) {
379 handle_map_impl(raw);
380 }
381 }
382
383 Ok(())
384 }
385
386 pub unsafe fn configure(&mut self) -> bool {
387 let window = self.window;
388 let scheduled = &mut (*window).configure_scheduled;
389 let sent = &mut (*window).configure_sent;
390 let s = x11_scale_for((*window).server, self.xsurface);
391
392 if scheduled.width == Some(0) {
393 scheduled.width = Some(from_x11((*self.xsurface).width as i32, s) as u32);
394 }
395 if scheduled.height == Some(0) {
396 scheduled.height = Some(from_x11((*self.xsurface).height as i32, s) as u32);
397 }
398
399 let mut phys_width = if let Some(w) = scheduled.width {
400 to_x11(w as i32, s) as u16
401 } else {
402 (*self.xsurface).width
403 };
404
405 let mut phys_height = if let Some(h) = scheduled.height {
406 to_x11(h as i32, s) as u16
407 } else {
408 (*self.xsurface).height
409 };
410
411 // X11 root coordinates: see `x11_scale`. Everything sent to X goes
412 // through `to_x11`, everything read back through `from_x11`; the
413 // window's own geometry stays logical.
414 let mut phys_x = to_x11((*window).box_geom.x, s) as i16;
415 let mut phys_y = to_x11((*window).box_geom.y, s) as i16;
416
417 let has_parent = !(*self.xsurface).parent.is_null();
418
419 if (*window).is_wine() && !has_parent && !(*window).is_fullscreen() {
420 if scheduled.width.is_some() {
421 phys_width += to_x11(WINE_MARGIN * 2, s) as u16;
422 }
423 if scheduled.height.is_some() {
424 phys_height += to_x11(WINE_MARGIN * 2, s) as u16;
425 }
426 phys_x -= to_x11(WINE_MARGIN, s) as i16;
427 phys_y -= to_x11(WINE_MARGIN, s) as i16;
428 }
429
430 let wanted = X11Geom { x: phys_x, y: phys_y, width: phys_width, height: phys_height };
431 if needs_configure(wanted, self.reported_geom(), self.sent_geom) {
432 self.send_configure(wanted);
433 }
434
435 if scheduled.activated != sent.activated {
436 self.set_activated(scheduled.activated);
437 }
438 if scheduled.maximized != sent.maximized {
439 ffi::wlr_xwayland_surface_set_maximized(self.xsurface, scheduled.maximized, scheduled.maximized);
440 }
441 if scheduled.inform_fullscreen != sent.inform_fullscreen {
442 ffi::wlr_xwayland_surface_set_fullscreen(self.xsurface, scheduled.inform_fullscreen);
443 }
444
445 let mut width = scheduled.width.unwrap_or(from_x11((*self.xsurface).width as i32, s) as u32);
446 let mut height = scheduled.height.unwrap_or(from_x11((*self.xsurface).height as i32, s) as u32);
447
448 if (*window).is_wine() && !has_parent && !(*window).is_fullscreen() {
449 if scheduled.width.is_none() {
450 width = width.saturating_sub((WINE_MARGIN * 2) as u32);
451 }
452 if scheduled.height.is_none() {
453 height = height.saturating_sub((WINE_MARGIN * 2) as u32);
454 }
455 }
456
457 (*window).configure_sent = (*window).configure_scheduled.clone();
458 (*window).configure_sent.width = Some(width);
459 (*window).configure_sent.height = Some(height);
460 (*window).configure_scheduled.width = None;
461 (*window).configure_scheduled.height = None;
462
463 false
464 }
465
466 /// The geometry wlroots currently reports for the X window.
467 pub unsafe fn reported_geom(&self) -> X11Geom {
468 X11Geom {
469 x: (*self.xsurface).x,
470 y: (*self.xsurface).y,
471 width: (*self.xsurface).width,
472 height: (*self.xsurface).height,
473 }
474 }
475
476 /// The one path to `wlr_xwayland_surface_configure`: every geometry
477 /// handed to X is recorded in `sent_geom` so `needs_configure` can tell
478 /// a geometry X has from one the compositor merely mirrored.
479 pub unsafe fn send_configure(&mut self, g: X11Geom) {
480 ffi::wlr_xwayland_surface_configure(self.xsurface, g.x, g.y, g.width, g.height);
481 self.sent_geom = Some(g);
482 }
483
484 pub unsafe fn set_activated(&self, activated: bool) {
485 if activated && (*self.xsurface).minimized {
486 ffi::wlr_xwayland_surface_set_minimized(self.xsurface, false);
487 }
488 ffi::wlr_xwayland_surface_activate(self.xsurface, activated);
489 }
490 }
491
492 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
493 let xwindow = crate::container_of!(listener, XwaylandWindow, destroy);
494 handle_destroy_impl(xwindow);
495 }
496
497 unsafe fn handle_destroy_impl(xwindow: *mut XwaylandWindow) {
498 wl_listener_remove_safe(&mut (*xwindow).destroy);
499 wl_listener_remove_safe(&mut (*xwindow).associate);
500 wl_listener_remove_safe(&mut (*xwindow).dissociate);
501 wl_listener_remove_safe(&mut (*xwindow).request_configure);
502 wl_listener_remove_safe(&mut (*xwindow).set_override_redirect);
503 wl_listener_remove_safe(&mut (*xwindow).set_size_hints);
504 wl_listener_remove_safe(&mut (*xwindow).set_title);
505 wl_listener_remove_safe(&mut (*xwindow).set_class);
506 wl_listener_remove_safe(&mut (*xwindow).set_parent);
507 wl_listener_remove_safe(&mut (*xwindow).set_decorations);
508 wl_listener_remove_safe(&mut (*xwindow).request_maximize);
509 wl_listener_remove_safe(&mut (*xwindow).request_fullscreen);
510 wl_listener_remove_safe(&mut (*xwindow).request_minimize);
511
512 (*(*xwindow).xsurface).data = std::ptr::null_mut();
513
514 let window = (*xwindow).window;
515 (*window).impl_destroying();
516
517 let _ = Box::from_raw(xwindow);
518 }
519
520 unsafe extern "C" fn handle_associate(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
521 let xwindow = crate::container_of!(listener, XwaylandWindow, associate);
522 handle_associate_impl(xwindow);
523 }
524
525 unsafe fn handle_associate_impl(xwindow: *mut XwaylandWindow) {
526 let surface = (*(*xwindow).xsurface).surface;
527 if !surface.is_null() {
528 connect_listener(
529 ffi::river_wlr_surface_get_map_signal(surface),
530 &mut (*xwindow).map,
531 handle_map,
532 );
533 connect_listener(
534 ffi::river_wlr_surface_get_unmap_signal(surface),
535 &mut (*xwindow).unmap,
536 handle_unmap,
537 );
538 }
539 }
540
541 unsafe extern "C" fn handle_dissociate(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
542 let xwindow = crate::container_of!(listener, XwaylandWindow, dissociate);
543 handle_dissociate_impl(xwindow);
544 }
545
546 unsafe fn handle_dissociate_impl(xwindow: *mut XwaylandWindow) {
547 wl_listener_remove_safe(&mut (*xwindow).map);
548 wl_listener_remove_safe(&mut (*xwindow).unmap);
549 }
550
551 unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
552 let xwindow = crate::container_of!(listener, XwaylandWindow, map);
553 handle_map_impl(xwindow);
554 }
555
556 unsafe fn handle_map_impl(xwindow: *mut XwaylandWindow) {
557 let surfaces_tree = (*(*xwindow).window).surfaces.tree;
558 let surface = (*(*xwindow).xsurface).surface;
559 let surface_tree = ffi::wlr_scene_subsurface_tree_create(surfaces_tree, surface);
560 if surface_tree.is_null() {
561 log::error!("out of memory creating subsurface tree");
562 let surface_resource = ffi::river_wlr_surface_get_resource(surface);
563 let client = ffi::wl_resource_get_client(surface_resource);
564 ffi::wl_client_post_no_memory(client);
565 return;
566 }
567 (*xwindow).surface_tree = surface_tree;
568
569 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
570
571 if (*(*xwindow).window).is_wine() && !has_parent && !(*(*xwindow).window).is_fullscreen() {
572 ffi::wlr_scene_node_set_position(surface_tree as *mut ffi::wlr_scene_node, -WINE_MARGIN, -WINE_MARGIN);
573 }
574
575 ffi::river_wlr_surface_set_data(surface, &mut (*(*xwindow).window).node as *mut crate::wm_node::WmNode as *mut _);
576
577 let capture_tree = &mut (*(*(*xwindow).window).capture_scene).tree as *mut ffi::wlr_scene_tree;
578 let capture_surface = ffi::wlr_scene_surface_create(capture_tree, surface);
579 if capture_surface.is_null() {
580 log::error!("out of memory creating capture surface");
581 let surface_resource = ffi::river_wlr_surface_get_resource(surface);
582 let client = ffi::wl_resource_get_client(surface_resource);
583 ffi::wl_client_post_no_memory(client);
584 return;
585 }
586
587 if (*(*xwindow).xsurface).fullscreen {
588 (*(*xwindow).window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
589 }
590
591 place_transient_where_it_asked(xwindow);
592 place_shy_where_it_is(xwindow);
593
594 (*(*xwindow).window).state = WindowState::Initialized;
595 if let Err(e) = (*(*xwindow).window).map() {
596 log::error!("out of memory mapping window: {}", e);
597 let surface_resource = ffi::river_wlr_surface_get_resource(surface);
598 let client = ffi::wl_resource_get_client(surface_resource);
599 ffi::wl_client_post_no_memory(client);
600 }
601 (*(*(*xwindow).window).server).wm.dirty_windowing();
602 }
603
604 /// A transient that asked for a position before mapping maps there.
605 ///
606 /// `handle_request_configure` grants a request that arrives before the
607 /// window is mapped verbatim, but records nothing: the window has no
608 /// geometry yet and the arrange pass has not placed it. The grant updates
609 /// the X surface's x/y, and nothing read them back at map, so a dialog that
610 /// positioned itself before showing -- Qt's `move()` before `show()`, which
611 /// is how Houdini's HC Panel centres itself on the pane it was opened over
612 /// -- mapped at the constructor's default origin instead, a hundred pixels
613 /// in from the desk corner. The client never asks again, since X told it
614 /// the request was granted, so the dialog sat there for good.
615 ///
616 /// Only a window with a parent, and only when the client says the position
617 /// is its own: ICCCM's `USPosition` / `PPosition` flags in WM_NORMAL_HINTS
618 /// are what toolkits set for an explicit move before map. A transient
619 /// without them is at whatever the X server defaulted to, and stays on the
620 /// compositor's placement. Top-level windows keep theirs too: restore and
621 /// the placement hints own those, and a transient is the one kind of window
622 /// `try_restore` refuses to touch.
623 unsafe fn place_transient_where_it_asked(xwindow: *mut XwaylandWindow) {
624 let xsurface = (*xwindow).xsurface;
625 if (*xsurface).parent.is_null() {
626 return;
627 }
628 let Some(asked) = (*xwindow).sent_geom else {
629 return;
630 };
631 let hints = (*xsurface).size_hints;
632 if hints.is_null() {
633 return;
634 }
635 let position_flags = ffi::xcb_icccm_size_hints_flags_t_XCB_ICCCM_SIZE_HINT_US_POSITION
636 | ffi::xcb_icccm_size_hints_flags_t_XCB_ICCCM_SIZE_HINT_P_POSITION;
637 if (*hints).flags & position_flags == 0 {
638 return;
639 }
640
641 let window = (*xwindow).window;
642 let s = x11_scale_for((*window).server, xsurface);
643 let log_x = from_x11(asked.x as i32, s);
644 let log_y = from_x11(asked.y as i32, s);
645 let (vx, vy) = (*window).screen_to_virtual(log_x, log_y);
646 (*window).virtual_x = vx;
647 (*window).virtual_y = vy;
648 // Placed by the client, like a picker placed by its hint: the camera
649 // must not pan to it on spawn or first focus.
650 (*window).hint_placed = true;
651 log::info!(
652 "XWayland transient mapped where it asked: title='{}' x11=({}, {}) logical=({}, {}) virtual=({:.1}, {:.1})",
653 (*window).get_title_string().unwrap_or_default(),
654 asked.x, asked.y, log_x, log_y, vx, vy,
655 );
656 }
657
658 /// A shy helper window (`Window::is_shy`) maps where its app put it: the
659 /// X window's own geometry, which Wine set from the app's CreateWindow
660 /// position — for Ubisoft Connect's shadow window, exactly its main
661 /// window's rect. The compositor's spawn placement would put it at the
662 /// default origin, in front of everything, as a blank white window.
663 unsafe fn place_shy_where_it_is(xwindow: *mut XwaylandWindow) {
664 let window = (*xwindow).window;
665 if !(*window).is_shy() {
666 return;
667 }
668 let xsurface = (*xwindow).xsurface;
669 let s = x11_scale_for((*window).server, xsurface);
670 let log_x = from_x11((*xsurface).x as i32, s);
671 let log_y = from_x11((*xsurface).y as i32, s);
672 let (vx, vy) = (*window).screen_to_virtual(log_x, log_y);
673 (*window).virtual_x = vx;
674 (*window).virtual_y = vy;
675 (*window).box_geom.x = log_x;
676 (*window).box_geom.y = log_y;
677 (*window).rendering_requested.x = log_x;
678 (*window).rendering_requested.y = log_y;
679 // Placed by the client: no spawn pan to it, ever.
680 (*window).hint_placed = true;
681 log::info!(
682 "XWayland no-activate helper window mapped where it is: title='{}' class='{}' x11=({}, {}) logical=({}, {}) virtual=({:.1}, {:.1})",
683 (*window).get_title_string().unwrap_or_default(),
684 (*window).get_app_id_string().unwrap_or_default(),
685 (*xsurface).x, (*xsurface).y, log_x, log_y, vx, vy,
686 );
687 }
688
689 unsafe extern "C" fn handle_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
690 let xwindow = crate::container_of!(listener, XwaylandWindow, unmap);
691 handle_unmap_impl(xwindow);
692 }
693
694 unsafe fn handle_unmap_impl(xwindow: *mut XwaylandWindow) {
695 let surface = (*(*xwindow).xsurface).surface;
696 if !surface.is_null() {
697 ffi::river_wlr_surface_set_data(surface, std::ptr::null_mut());
698 }
699 (*(*xwindow).window).unmap();
700 if !(*xwindow).surface_tree.is_null() {
701 ffi::wlr_scene_node_destroy((*xwindow).surface_tree as *mut ffi::wlr_scene_node);
702 (*xwindow).surface_tree = std::ptr::null_mut();
703 }
704 }
705
706 unsafe extern "C" fn handle_request_configure(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
707 let xwindow = crate::container_of!(listener, XwaylandWindow, request_configure);
708 let event = data as *mut ffi::wlr_xwayland_surface_configure_event;
709
710 let surface = (*(*xwindow).xsurface).surface;
711 if surface.is_null() || !ffi::river_wlr_surface_is_mapped(surface) {
712 (*xwindow).send_configure(X11Geom {
713 x: (*event).x,
714 y: (*event).y,
715 width: (*event).width,
716 height: (*event).height,
717 });
718 return;
719 }
720
721 let class_ptr = (*(*xwindow).xsurface).class;
722 let class = if class_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(class_ptr).to_str().unwrap_or("") };
723 let title_ptr = (*(*xwindow).xsurface).title;
724 let title = if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") };
725 let window = (*xwindow).window;
726 let is_wine = (*window).is_wine();
727
728 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
729 let s = x11_scale_for((*window).server, (*xwindow).xsurface);
730 log::info!(
731 "XWayland configure request: title='{}' class='{}' has_parent={} is_wine={} event=({}, {}, {}, {}) xsurface=({}, {}, {}, {})",
732 title,
733 class,
734 has_parent,
735 is_wine,
736 (*event).x, (*event).y, (*event).width, (*event).height,
737 (*(*xwindow).xsurface).x, (*(*xwindow).xsurface).y, (*(*xwindow).xsurface).width, (*(*xwindow).xsurface).height,
738 );
739
740 let is_tiled = unsafe {
741 (*window).wm_requested.tiled != 0 || !matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Utility)
742 };
743 let is_fullscreen = unsafe { (*window).is_fullscreen() };
744
745 // A window named in `xwayland_hidpi_except` is a full-screen X11 game
746 // that sizes AND places itself to the screen (Trackmania's
747 // "windowedfull" asks for (0, 0) at the desktop size). Refusing the
748 // position — answering every request with the compositor's placement —
749 // had Wine re-asking ~170 times a second for as long as the window was
750 // up. It gets the parented treatment: position and size granted, the
751 // virtual origin moved with it. Not while the compositor has it
752 // fullscreen or tiled: then the size is the compositor's (below).
753 let exempt_self_placed = !has_parent && !is_fullscreen && !is_tiled && window_is_hidpi_exempt(window);
754 // A shy helper window (`Window::is_shy`) is placed by its app, which
755 // moves it to track its main window: position and size granted.
756 let shy_self_placed = !has_parent && !is_fullscreen && !is_tiled && (*window).is_shy();
757
758 if has_parent || exempt_self_placed || shy_self_placed {
759 // Granted on the logical grid rather than verbatim — see `snap_x11`.
760 // The logical values below are what the window's geometry becomes, so
761 // handing X anything else is handing it a number this compositor
762 // cannot reproduce.
763 let (mut ex, mut ey, mut ew, mut eh) =
764 ((*event).x as i32, (*event).y as i32, (*event).width as i32, (*event).height as i32);
765 if exempt_self_placed {
766 // The game SEES the physical-pixel root and asks for all of it
767 // (Trackmania's windowedfull: 3840x2160 at (0, 0)), but its
768 // pixels are logical here — granted verbatim that is a window
769 // twice the screen, which the arrange pass then keeps pulling
770 // back on-desk while the game keeps asking, ~60 requests a
771 // second. So the request is answered with at most the output's
772 // logical box, kept on that output: the whole root becomes the
773 // whole screen, which is what the game meant.
774 let out = (*window).fullscreen_output();
775 if !out.is_null() {
776 let ob = (*out).sent.box_layout();
777 let (ox, oy, ow, oh) = (ob.x, ob.y, ob.width, ob.height);
778 ew = from_x11(ew, s).min(ow).max(1);
779 eh = from_x11(eh, s).min(oh).max(1);
780 ex = from_x11(ex, s).clamp(ox, (ox + ow - ew).max(ox));
781 ey = from_x11(ey, s).clamp(oy, (oy + oh - eh).max(oy));
782 ex = to_x11(ex, s);
783 ey = to_x11(ey, s);
784 ew = to_x11(ew, s);
785 eh = to_x11(eh, s);
786 if (ex, ey, ew, eh) != ((*event).x as i32, (*event).y as i32, (*event).width as i32, (*event).height as i32) {
787 log::info!(
788 "XWayland configure request: '{}' is hidpi-exempt; ({}, {}, {}x{}) clamped to the output's logical box as ({}, {}, {}x{})",
789 title, (*event).x, (*event).y, (*event).width, (*event).height, ex, ey, ew, eh,
790 );
791 }
792 }
793 }
794 (*xwindow).send_configure(X11Geom {
795 x: snap_x11(ex, s) as i16,
796 y: snap_x11(ey, s) as i16,
797 width: snap_x11(ew, s) as u16,
798 height: snap_x11(eh, s) as u16,
799 });
800 let log_x = from_x11(ex, s);
801 let log_y = from_x11(ey, s);
802 let log_width = from_x11(ew, s) as u32;
803 let log_height = from_x11(eh, s) as u32;
804
805 (*window).box_geom.x = log_x;
806 (*window).box_geom.y = log_y;
807 (*window).box_geom.width = log_width as i32;
808 (*window).box_geom.height = log_height as i32;
809 (*window).rendering_requested.x = log_x;
810 (*window).rendering_requested.y = log_y;
811 (*window).rendering_sent.width = log_width;
812 (*window).rendering_sent.height = log_height;
813 // The screen origin above is only half the move: the arrange pass
814 // places a floating window from its VIRTUAL origin, so leaving that
815 // stale meant the very next transaction recomputed the window back
816 // to where it was. An X11 client reads that as its move being
817 // refused and asks again from the position it was pushed to, which
818 // is a runaway: Houdini's Edit Theme dialog walked 270px left across
819 // one tab switch, re-requesting 15 times in a second and never
820 // converging on a size either.
821 let (vx, vy) = (*window).screen_to_virtual(log_x, log_y);
822 (*window).virtual_x = vx;
823 (*window).virtual_y = vy;
824 if exempt_self_placed || shy_self_placed {
825 // Placed by the client: the camera must not pan to it on spawn.
826 (*window).hint_placed = true;
827 }
828 (*window).set_dimensions(log_width, log_height);
829 return;
830 }
831
832 // A floating window normally gets the size it asks for; not while an
833 // output is coming or going (see `note_output_change`), and not while
834 // the compositor has it FULLSCREEN: Wine syncs a window's
835 // _NET_WM_STATE from its own idea of the window rect, so a game whose
836 // fixed-size hints were granted here shrank the X window back the
837 // instant the fullscreen configure went out, then withdrew the
838 // fullscreen state Wine no longer saw as true — every Fullscreen press
839 // on Trackmania undid itself within the same frame. Held at the
840 // fullscreen size, Wine sees a screen-sized rect and keeps the state.
841 let hold_size = is_tiled || is_fullscreen || in_output_change_grace();
842 if hold_size && !is_tiled {
843 log::info!(
844 "XWayland configure request: holding floating '{}' at its own size during output-change grace",
845 title,
846 );
847 }
848
849 let (phys_width, phys_height) = if hold_size {
850 let log_w = (*window).configure_sent.width.unwrap_or((*window).box_geom.width as u32);
851 let log_h = (*window).configure_sent.height.unwrap_or((*window).box_geom.height as u32);
852 if log_w > 0 && log_h > 0 {
853 let mut w = log_w;
854 let mut h = log_h;
855 if is_wine && !has_parent && !is_fullscreen {
856 w += (WINE_MARGIN * 2) as u32;
857 h += (WINE_MARGIN * 2) as u32;
858 }
859 (to_x11(w as i32, s) as u16, to_x11(h as i32, s) as u16)
860 } else {
861 (snap_x11((*event).width as i32, s) as u16, snap_x11((*event).height as i32, s) as u16)
862 }
863 } else {
864 // Snapped for the same reason as the parented branch above: the size
865 // stored below is `from_x11` of what goes out here, and the next
866 // configure sends `to_x11` of that back.
867 (snap_x11((*event).width as i32, s) as u16, snap_x11((*event).height as i32, s) as u16)
868 };
869
870 let mut phys_x = to_x11((*window).box_geom.x, s) as i16;
871 let mut phys_y = to_x11((*window).box_geom.y, s) as i16;
872
873 if is_wine && !has_parent && !is_fullscreen {
874 phys_x -= to_x11(WINE_MARGIN, s) as i16;
875 phys_y -= to_x11(WINE_MARGIN, s) as i16;
876 }
877
878 (*xwindow).send_configure(X11Geom { x: phys_x, y: phys_y, width: phys_width, height: phys_height });
879 let mut log_width = from_x11(phys_width as i32, s) as u32;
880 let mut log_height = from_x11(phys_height as i32, s) as u32;
881 if is_wine && !has_parent && !is_fullscreen {
882 log_width = log_width.saturating_sub((WINE_MARGIN * 2) as u32);
883 log_height = log_height.saturating_sub((WINE_MARGIN * 2) as u32);
884 }
885 (*window).set_dimensions(log_width, log_height);
886 }
887
888 unsafe extern "C" fn handle_set_override_redirect(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
889 let xwindow = crate::container_of!(listener, XwaylandWindow, set_override_redirect);
890 let xsurface = (*xwindow).xsurface;
891 log::info!("xwayland surface set override redirect: val={}", (*xsurface).override_redirect);
892 assert!((*xsurface).override_redirect);
893
894 let surface = (*xsurface).surface;
895 if !surface.is_null() {
896 if ffi::river_wlr_surface_is_mapped(surface) {
897 handle_unmap_impl(xwindow);
898 }
899 handle_dissociate_impl(xwindow);
900 }
901 let server = (*(*xwindow).window).server;
902 handle_destroy_impl(xwindow);
903
904 if let Err(e) = XwaylandOverrideRedirect::create(xsurface, server) {
905 log::error!("Failed to create XwaylandOverrideRedirect: {}", e);
906 }
907 }
908
909 #[allow(dead_code)]
910 unsafe extern "C" fn handle_set_size_hints(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
911 let xwindow = crate::container_of!(listener, XwaylandWindow, set_size_hints);
912 let size_hints = (*(*xwindow).xsurface).size_hints;
913 if !size_hints.is_null() {
914 let min_width = std::cmp::max(0, (*size_hints).min_width) as u32;
915 let min_height = std::cmp::max(0, (*size_hints).min_height) as u32;
916 let max_width = if (*size_hints).max_width <= 0 {
917 0
918 } else {
919 std::cmp::max(min_width, (*size_hints).max_width as u32)
920 };
921 let max_height = if (*size_hints).max_height <= 0 {
922 0
923 } else {
924 std::cmp::max(min_height, (*size_hints).max_height as u32)
925 };
926 let hint = crate::window::DimensionsHint {
927 min_width,
928 max_width,
929 min_height,
930 max_height,
931 };
932 (*(*xwindow).window).set_dimensions_hint(hint);
933 }
934 }
935
936 unsafe extern "C" fn handle_set_title(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
937 let xwindow = crate::container_of!(listener, XwaylandWindow, set_title);
938 (*(*xwindow).window).notify_title();
939 }
940
941 unsafe extern "C" fn handle_set_class(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
942 let xwindow = crate::container_of!(listener, XwaylandWindow, set_class);
943 (*(*xwindow).window).notify_app_id();
944 }
945
946 unsafe extern "C" fn handle_set_parent(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
947 let xwindow = crate::container_of!(listener, XwaylandWindow, set_parent);
948 (*(*(*xwindow).window).server).wm.dirty_windowing();
949 }
950
951 unsafe extern "C" fn handle_set_decorations(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
952 let xwindow = crate::container_of!(listener, XwaylandWindow, set_decorations);
953 let prefers_csd = ((*(*xwindow).xsurface).decorations
954 & (ffi::wlr_xwayland_surface_decorations_WLR_XWAYLAND_SURFACE_DECORATIONS_NO_BORDER
955 | ffi::wlr_xwayland_surface_decorations_WLR_XWAYLAND_SURFACE_DECORATIONS_NO_TITLE) as u32)
956 != 0;
957
958 let hint = if prefers_csd {
959 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_CSD
960 } else {
961 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_SSD
962 };
963 (*(*xwindow).window).set_decoration_hint(hint);
964 }
965
966 unsafe extern "C" fn handle_request_maximize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
967 let xwindow = crate::container_of!(listener, XwaylandWindow, request_maximize);
968 let maximized = (*(*xwindow).xsurface).maximized_vert || (*(*xwindow).xsurface).maximized_horz;
969 let window = (*xwindow).window;
970 if maximized {
971 (*window).tiling_mode = crate::tiling::TilingMode::Tiled;
972 (*window).mode_locked = true;
973 } else {
974 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
975 (*window).mode_locked = true;
976 }
977 (*window).wm_scheduled.maximize_requested = if maximized {
978 crate::window::MaximizeRequest::Maximize
979 } else {
980 crate::window::MaximizeRequest::Unmaximize
981 };
982 (*(*window).server).wm.dirty_windowing();
983 }
984
985 unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
986 let xwindow = crate::container_of!(listener, XwaylandWindow, request_fullscreen);
987 let fullscreen = (*(*xwindow).xsurface).fullscreen;
988 log::info!(
989 "XWayland fullscreen request: title='{}' fullscreen={}",
990 (*(*xwindow).window).get_title_string().unwrap_or_default(),
991 fullscreen,
992 );
993 // An exempt game's pixels are logical, so the compositor's fullscreen
994 // is a 1920x1200 X window on a 3840x2400 root. Wine syncs
995 // _NET_WM_STATE from its own idea of the screen and withdraws
996 // FULLSCREEN the moment it sees a window that does not cover its
997 // root — every Fullscreen press on Trackmania was undone by this
998 // request within the frame. The user's fullscreen stands; the key that
999 // set it clears it.
1000 if !fullscreen
1001 && (*(*xwindow).window).is_fullscreen()
1002 && window_is_hidpi_exempt((*xwindow).window)
1003 {
1004 log::info!("XWayland fullscreen request: ignored — the window is hidpi-exempt and fullscreen by the compositor");
1005 return;
1006 }
1007 (*(*xwindow).window).wm_scheduled.fullscreen_requested = if fullscreen {
1008 crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut())
1009 } else {
1010 crate::window::FullscreenRequest::Exit
1011 };
1012 (*(*(*xwindow).window).server).wm.dirty_windowing();
1013 (*(*(*xwindow).window).server).wm.apply_client_fullscreen((*xwindow).window, fullscreen);
1014 }
1015
1016 unsafe extern "C" fn handle_request_minimize(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1017 let xwindow = crate::container_of!(listener, XwaylandWindow, request_minimize);
1018 let event = data as *mut ffi::wlr_xwayland_minimize_event;
1019 ffi::wlr_xwayland_surface_set_minimized((*xwindow).xsurface, (*event).minimize);
1020 (*(*xwindow).window).wm_scheduled.minimize_requested = true;
1021 (*(*(*xwindow).window).server).wm.dirty_windowing();
1022 }
1023
1024 #[cfg(test)]
1025 mod tests {
1026 use super::*;
1027 use std::sync::atomic::AtomicU32;
1028
1029 #[test]
1030 fn scale_from_live_output_is_remembered() {
1031 let last = AtomicU32::new(0);
1032 assert_eq!(resolve_x11_scale(Some(2.0), &last), 2.0);
1033 // Output gone (suspend): the remembered scale stands in, not 1.
1034 assert_eq!(resolve_x11_scale(None, &last), 2.0);
1035 // A new output with another scale takes over and is remembered.
1036 assert_eq!(resolve_x11_scale(Some(1.5), &last), 1.5);
1037 assert_eq!(resolve_x11_scale(None, &last), 1.5);
1038 }
1039
1040 fn pats(list: &[&str]) -> Vec<String> {
1041 list.iter().map(|s| s.to_string()).collect()
1042 }
1043
1044 #[test]
1045 fn exempt_matches_class_instance_or_title() {
1046 // Proton: every window is class steam_proton, so the game is told
1047 // apart by its instance (the exe) or its title.
1048 let p = pats(&["Trackmania"]);
1049 assert!(hidpi_exempt(&p, "steam_proton", "trackmania.exe", "Trackmania"));
1050 assert!(hidpi_exempt(&p, "steam_proton", "", "Trackmania"));
1051 assert!(hidpi_exempt(&p, "Trackmania", "", ""));
1052 assert!(!hidpi_exempt(&p, "steam_proton", "upc.exe", "Ubisoft Connect"));
1053 // Wildcards and case follow app_id_matches.
1054 let p = pats(&["trackmania*"]);
1055 assert!(hidpi_exempt(&p, "steam_proton", "Trackmania.exe", ""));
1056 assert!(!hidpi_exempt(&p, "steam_proton", "", "My Trackmania"));
1057 }
1058
1059 #[test]
1060 fn exempt_ignores_empty_fields_and_lists() {
1061 assert!(!hidpi_exempt(&[], "steam_proton", "trackmania.exe", "Trackmania"));
1062 // An empty field must not match a pattern that is itself empty-ish.
1063 assert!(!hidpi_exempt(&pats(&["*"]), "", "", ""));
1064 assert!(hidpi_exempt(&pats(&["*"]), "x", "", ""));
1065 }
1066
1067 #[test]
1068 fn scale_before_any_output_is_one() {
1069 let last = AtomicU32::new(0);
1070 assert_eq!(resolve_x11_scale(None, &last), 1.0);
1071 }
1072
1073 #[test]
1074 fn x11_round_trip_holds_at_remembered_scale() {
1075 // The suspend case: physical 3712 read back while no output exists
1076 // must come back as logical 1856, and go out again as 3712.
1077 let last = AtomicU32::new(0);
1078 let _ = resolve_x11_scale(Some(2.0), &last);
1079 let s = resolve_x11_scale(None, &last);
1080 let logical = from_x11(3712, s);
1081 assert_eq!(logical, 1856);
1082 assert_eq!(to_x11(logical, resolve_x11_scale(Some(2.0), &last)), 3712);
1083 }
1084
1085 #[test]
1086 fn snap_x11_is_what_the_logical_grid_can_express() {
1087 // An odd X11 coordinate at scale 2 has no logical integer, so it
1088 // moves by one; the point is that it then STAYS there. Granting the
1089 // raw value instead is what let Houdini's dialog gain a pixel per
1090 // request: 181 -> 91 -> 182 -> 91 -> 182 ...
1091 assert_eq!(from_x11(181, 2.0), 91);
1092 assert_eq!(to_x11(91, 2.0), 182);
1093 assert_eq!(snap_x11(181, 2.0), 182);
1094
1095 // Idempotent: snapping a snapped value is a no-op, which is what
1096 // makes repeated configures converge instead of drifting.
1097 for x in [-91, -90, -1, 0, 1, 180, 181, 757, 1300, 3712] {
1098 let once = snap_x11(x, 2.0);
1099 assert_eq!(snap_x11(once, 2.0), once, "not idempotent at x={x}");
1100 }
1101
1102 // Even values — and every value at scale 1 — are untouched, so
1103 // nothing outside xwayland_hidpi changes.
1104 for x in [-90, 0, 180, 720, 1360, 3712] {
1105 assert_eq!(snap_x11(x, 2.0), x, "even value moved at x={x}");
1106 }
1107 for x in [-91, -1, 0, 1, 181, 757, 1301] {
1108 assert_eq!(snap_x11(x, 1.0), x, "scale 1 moved at x={x}");
1109 }
1110 }
1111
1112 #[test]
1113 fn configure_is_sent_until_it_has_actually_been_sent_once() {
1114 let g = |x, y, w, h| X11Geom { x, y, width: w, height: h };
1115 let fullscreen = g(0, 0, 1280, 720);
1116 // The restore case: the mirror already says 1280x720 (pre-written by
1117 // try_restore) but nothing was ever sent — X is at its natural size.
1118 assert!(needs_configure(fullscreen, fullscreen, None));
1119 // Something else was sent (the client's own pre-map request).
1120 assert!(needs_configure(fullscreen, fullscreen, Some(g(0, 0, 103, 36))));
1121 // Sent once and X reports it: nothing to do.
1122 assert!(!needs_configure(fullscreen, fullscreen, Some(fullscreen)));
1123 // X moved or resized itself since: the mirror disagrees, resend.
1124 assert!(needs_configure(fullscreen, g(10, 10, 1280, 720), Some(fullscreen)));
1125 assert!(needs_configure(fullscreen, g(0, 0, 640, 360), Some(fullscreen)));
1126 // A different wanted geometry always goes out.
1127 assert!(needs_configure(g(0, 0, 640, 360), fullscreen, Some(fullscreen)));
1128 }
1129
1130 #[test]
1131 fn output_change_grace_begins_and_is_re_armed() {
1132 note_output_change();
1133 assert!(in_output_change_grace());
1134 // Expire it by hand, then a second change re-arms it.
1135 *OUTPUT_CHANGE_GRACE_UNTIL.lock().unwrap() =
1136 Some(std::time::Instant::now() - std::time::Duration::from_secs(1));
1137 assert!(!in_output_change_grace());
1138 note_output_change();
1139 assert!(in_output_change_grace());
1140 }
1141 }