Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/xdg_toplevel.rs (60.3K)
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, WlList, wl_listener_remove, wl_signal_add};
6 use crate::window::Window;
7
8 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
9 pub enum ConfigureState {
10 Idle,
11 Inflight(u32),
12 Acked,
13 Committed,
14 TimedOut(u32),
15 TimedOutAcked,
16 }
17
18 pub struct XdgToplevel {
19 pub window: *mut Window,
20 pub wlr_toplevel: *mut ffi::wlr_xdg_toplevel,
21 pub decoration: *mut XdgDecoration,
22 pub geometry: ffi::wlr_box,
23 /// Surface extent as of the last commit — with `geometry`, the mapping
24 /// change detector for the deferred pointer refresh (see `handle_commit`).
25 pub last_surface_size: (i32, i32),
26 pub configure_state: ConfigureState,
27 /// Has the client ever answered a configure? Until it has,
28 /// `geometry` is the size the CLIENT asked for, not a response to one
29 /// — see `Window::render_start`, which refuses to adopt a wish over a
30 /// restored size.
31 pub acked_once: bool,
32
33 pub destroy: ffi::wl_listener,
34 pub ack_configure: ffi::wl_listener,
35 pub map: ffi::wl_listener,
36 pub unmap: ffi::wl_listener,
37 pub commit: ffi::wl_listener,
38 pub new_popup: ffi::wl_listener,
39 pub request_show_window_menu: ffi::wl_listener,
40 pub request_fullscreen: ffi::wl_listener,
41 pub request_maximize: ffi::wl_listener,
42 pub request_minimize: ffi::wl_listener,
43 pub request_move: ffi::wl_listener,
44 pub request_resize: ffi::wl_listener,
45 pub set_parent: ffi::wl_listener,
46 pub set_title: ffi::wl_listener,
47 pub set_app_id: ffi::wl_listener,
48 }
49
50 pub struct XdgDecoration {
51 pub wlr_decoration: *mut ffi::wlr_xdg_toplevel_decoration_v1,
52 pub destroy: ffi::wl_listener,
53 pub request_mode: ffi::wl_listener,
54 }
55
56 impl XdgToplevel {
57 pub unsafe fn create(
58 wlr_toplevel: *mut ffi::wlr_xdg_toplevel,
59 server: *mut Server,
60 ) -> Result<(), &'static str> {
61 log::debug!("new xdg_toplevel");
62
63 let window = Window::create(crate::window::WindowImpl::Toplevel(std::ptr::null_mut()), server)?;
64
65 let toplevel = Box::new(XdgToplevel {
66 window,
67 wlr_toplevel,
68 decoration: std::ptr::null_mut(),
69 geometry: std::mem::zeroed(),
70 last_surface_size: (0, 0),
71 configure_state: ConfigureState::Idle,
72 acked_once: false,
73
74 destroy: std::mem::zeroed(),
75 ack_configure: std::mem::zeroed(),
76 map: std::mem::zeroed(),
77 unmap: std::mem::zeroed(),
78 commit: std::mem::zeroed(),
79 new_popup: std::mem::zeroed(),
80 request_show_window_menu: std::mem::zeroed(),
81 request_fullscreen: std::mem::zeroed(),
82 request_maximize: std::mem::zeroed(),
83 request_minimize: std::mem::zeroed(),
84 request_move: std::mem::zeroed(),
85 request_resize: std::mem::zeroed(),
86 set_parent: std::mem::zeroed(),
87 set_title: std::mem::zeroed(),
88 set_app_id: std::mem::zeroed(),
89 });
90
91 let raw = Box::into_raw(toplevel);
92 (*window).set_impl(crate::window::WindowImpl::Toplevel(raw));
93
94 let base = ffi::river_wlr_xdg_toplevel_get_base(wlr_toplevel);
95 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
96
97 let unmap_listener = &mut (*raw).unmap as *mut ffi::wl_listener as *mut WlListener;
98 (*unmap_listener).notify = Some(handle_unmap);
99 wl_signal_add(ffi::river_wlr_surface_get_unmap_signal(surface), &mut (*raw).unmap);
100
101 let surfaces_tree = (*window).surfaces.tree;
102 let capture_tree = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree;
103
104 let scene_xdg = ffi::wlr_scene_xdg_surface_create(surfaces_tree, base);
105 if scene_xdg.is_null() {
106 let _ = Box::from_raw(raw);
107 return Err("wlr_scene_xdg_surface_create failed");
108 }
109 let capture_xdg = ffi::wlr_scene_xdg_surface_create(capture_tree, base);
110 if capture_xdg.is_null() {
111 // Already added unmap listener, but let's at least try to free raw
112 let _ = Box::from_raw(raw);
113 return Err("wlr_scene_xdg_surface_create for capture tree failed");
114 }
115
116 ffi::river_wlr_xdg_surface_set_data(base, raw as *mut _);
117 ffi::river_wlr_surface_set_data(surface, (*window).tree as *mut ffi::wlr_scene_node as *mut _);
118
119 let destroy_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
120 (*destroy_listener).notify = Some(handle_destroy);
121 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_destroy_signal(wlr_toplevel), &mut (*raw).destroy);
122
123 let ack_listener = &mut (*raw).ack_configure as *mut ffi::wl_listener as *mut WlListener;
124 (*ack_listener).notify = Some(handle_ack_configure);
125 wl_signal_add(ffi::river_wlr_xdg_surface_get_ack_configure_signal(base), &mut (*raw).ack_configure);
126
127 let map_listener = &mut (*raw).map as *mut ffi::wl_listener as *mut WlListener;
128 (*map_listener).notify = Some(handle_map);
129 wl_signal_add(ffi::river_wlr_surface_get_map_signal(surface), &mut (*raw).map);
130
131 let commit_listener = &mut (*raw).commit as *mut ffi::wl_listener as *mut WlListener;
132 (*commit_listener).notify = Some(handle_commit);
133 wl_signal_add(ffi::river_wlr_surface_get_commit_signal(surface), &mut (*raw).commit);
134
135 let popup_listener = &mut (*raw).new_popup as *mut ffi::wl_listener as *mut WlListener;
136 (*popup_listener).notify = Some(handle_new_popup);
137 wl_signal_add(ffi::river_wlr_xdg_surface_get_new_popup_signal(base), &mut (*raw).new_popup);
138
139 let menu_listener = &mut (*raw).request_show_window_menu as *mut ffi::wl_listener as *mut WlListener;
140 (*menu_listener).notify = Some(handle_request_show_window_menu);
141 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_show_window_menu_signal(wlr_toplevel), &mut (*raw).request_show_window_menu);
142
143 let fs_listener = &mut (*raw).request_fullscreen as *mut ffi::wl_listener as *mut WlListener;
144 (*fs_listener).notify = Some(handle_request_fullscreen);
145 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_fullscreen_signal(wlr_toplevel), &mut (*raw).request_fullscreen);
146
147 let max_listener = &mut (*raw).request_maximize as *mut ffi::wl_listener as *mut WlListener;
148 (*max_listener).notify = Some(handle_request_maximize);
149 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_maximize_signal(wlr_toplevel), &mut (*raw).request_maximize);
150
151 let min_listener = &mut (*raw).request_minimize as *mut ffi::wl_listener as *mut WlListener;
152 (*min_listener).notify = Some(handle_request_minimize);
153 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_minimize_signal(wlr_toplevel), &mut (*raw).request_minimize);
154
155 let move_listener = &mut (*raw).request_move as *mut ffi::wl_listener as *mut WlListener;
156 (*move_listener).notify = Some(handle_request_move);
157 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_move_signal(wlr_toplevel), &mut (*raw).request_move);
158
159 let resize_listener = &mut (*raw).request_resize as *mut ffi::wl_listener as *mut WlListener;
160 (*resize_listener).notify = Some(handle_request_resize);
161 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_request_resize_signal(wlr_toplevel), &mut (*raw).request_resize);
162
163 let parent_listener = &mut (*raw).set_parent as *mut ffi::wl_listener as *mut WlListener;
164 (*parent_listener).notify = Some(handle_set_parent);
165 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_parent_signal(wlr_toplevel), &mut (*raw).set_parent);
166
167 let title_listener = &mut (*raw).set_title as *mut ffi::wl_listener as *mut WlListener;
168 (*title_listener).notify = Some(handle_set_title);
169 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_title_signal(wlr_toplevel), &mut (*raw).set_title);
170
171 let app_listener = &mut (*raw).set_app_id as *mut ffi::wl_listener as *mut WlListener;
172 (*app_listener).notify = Some(handle_set_app_id);
173 wl_signal_add(ffi::river_wlr_xdg_toplevel_get_set_app_id_signal(wlr_toplevel), &mut (*raw).set_app_id);
174
175 Ok(())
176 }
177
178 pub unsafe fn destroy_popups(&self) {
179 let base = ffi::river_wlr_xdg_toplevel_get_base(self.wlr_toplevel);
180 let list_head = ffi::river_wlr_xdg_surface_get_popups(base) as *mut WlList;
181 let mut curr = (*list_head).next;
182 while curr != list_head {
183 let next = (*curr).next;
184 let popup = crate::container_of!(curr, ffi::wlr_xdg_popup, link);
185 ffi::wl_resource_destroy((*popup).resource);
186 curr = next;
187 }
188 }
189
190 pub unsafe fn configure(&mut self) -> bool {
191 match self.configure_state {
192 ConfigureState::Idle
193 | ConfigureState::Inflight(..)
194 | ConfigureState::Acked
195 | ConfigureState::Committed
196 | ConfigureState::TimedOut(..)
197 | ConfigureState::TimedOutAcked => {}
198 }
199
200 let scheduled = &(*self.window).configure_scheduled;
201 let sent = &(*self.window).configure_sent;
202
203 if !self.needs_configure() {
204 match self.configure_state {
205 ConfigureState::Idle => return false,
206 ConfigureState::TimedOut(serial) => {
207 self.configure_state = ConfigureState::Inflight(serial);
208 return true;
209 }
210 ConfigureState::TimedOutAcked => {
211 self.configure_state = ConfigureState::Acked;
212 return true;
213 }
214 ConfigureState::Inflight(..) | ConfigureState::Acked | ConfigureState::Committed => {
215 return false;
216 }
217 }
218 }
219
220 // Absorb a size-only ECHO: the scheduled size merely restates what the
221 // client has already committed (its current geometry) and nothing else
222 // changed. Sending it anyway hands a self-sizing client a stale size
223 // one commit later — and when the client's content width flaps (the
224 // cpu module's text crossing 10%), that stale echo re-triggers a
225 // resize on both sides and the pair ping-pongs at frame rate (the
226 // status-bar jitter: ~3300 alternating 95/104 configures in 5min).
227 // Agree with reality instead and send nothing. A configure whose size
228 // DIFFERS from the committed geometry — a real compositor-driven
229 // resize — always goes through.
230 {
231 let echo_w = scheduled.width.or(sent.width);
232 let echo_h = scheduled.height.or(sent.height);
233 // Bounds are part of the echo, not a separate signal, when they
234 // merely track the echoed size: the arrange schedules a status
235 // window's bounds equal to its own box, so a self-resize ALWAYS
236 // carries a matching bounds delta — requiring bounds equality
237 // here would keep the absorb permanently disabled for exactly
238 // the windows that loop. A bounds change that differs from the
239 // echoed size (a real available-area change) still forces a
240 // configure.
241 let bounds_ok = (scheduled.bounds.width == sent.bounds.width
242 && scheduled.bounds.height == sent.bounds.height)
243 || (echo_w == Some(scheduled.bounds.width as u32)
244 && echo_h == Some(scheduled.bounds.height as u32));
245 let non_size_equal = scheduled.activated == sent.activated
246 && scheduled.ssd == sent.ssd
247 && scheduled.tiled == sent.tiled
248 && scheduled.capabilities == sent.capabilities
249 && scheduled.maximized == sent.maximized
250 && scheduled.inform_fullscreen == sent.inform_fullscreen
251 && scheduled.resizing == sent.resizing;
252 // Idle AND Committed: after any completed configure round-trip
253 // the state machine RESTS in Committed (Acked → Committed on
254 // commit; only the timeout path returns to Idle), so gating on
255 // Idle alone leaves this absorb dead in steady state — the exact
256 // moment the echo loop runs. Inflight/Acked stay excluded: a
257 // real configure is mid-flight and the scheduled size may need
258 // to supersede it.
259 let size_is_echo = self.geometry.width > 0
260 && self.geometry.height > 0
261 && echo_w == Some(self.geometry.width as u32)
262 && echo_h == Some(self.geometry.height as u32);
263 // Timeout recovery states absorb too: a hot echo loop drives the
264 // machine into TimedOut/TimedOutAcked, and an absorb that disarms
265 // there switches itself off at exactly the moment it exists for
266 // (observed live: a title-flapping window module sustained a
267 // 372↔456 storm at state=TimedOutAcked). Only Inflight/Acked stay
268 // excluded — a real configure is mid-flight there. Absorbing
269 // leaves the timeout recovery untouched: a late ack or the next
270 // commit still walks the state back to Idle.
271 if size_is_echo
272 && non_size_equal
273 && bounds_ok
274 && !matches!(
275 self.configure_state,
276 ConfigureState::Inflight(..) | ConfigureState::Acked
277 )
278 {
279 let absorbed_bounds = scheduled.bounds;
280 (*self.window).configure_sent.width = echo_w;
281 (*self.window).configure_sent.height = echo_h;
282 (*self.window).configure_sent.bounds = absorbed_bounds;
283 (*self.window).configure_scheduled.width = None;
284 (*self.window).configure_scheduled.height = None;
285 return false;
286 }
287 if size_is_echo && log::log_enabled!(log::Level::Debug) {
288 // The size restates committed geometry yet the absorb
289 // declined — name the blocker (the state, or which non-size
290 // field), so a live echo loop is diagnosable from the log.
291 log::debug!(
292 "XdgToplevel::configure: echo NOT absorbed: state={:?} non_size_equal={} \
293 (bounds {}x{}/{}x{} act {}/{} ssd {}/{} tiled {:?}/{:?} caps {:?}/{:?} max {}/{} fs {}/{} rsz {}/{})",
294 self.configure_state, non_size_equal,
295 scheduled.bounds.width, scheduled.bounds.height, sent.bounds.width, sent.bounds.height,
296 scheduled.activated, sent.activated,
297 scheduled.ssd, sent.ssd,
298 scheduled.tiled, sent.tiled,
299 scheduled.capabilities, sent.capabilities,
300 scheduled.maximized, sent.maximized,
301 scheduled.inform_fullscreen, sent.inform_fullscreen,
302 scheduled.resizing, sent.resizing,
303 );
304 }
305 }
306
307 ffi::wlr_xdg_toplevel_set_activated(self.wlr_toplevel, scheduled.activated);
308 ffi::wlr_xdg_toplevel_set_tiled(
309 self.wlr_toplevel,
310 scheduled.tiled,
311 );
312 ffi::wlr_xdg_toplevel_set_wm_capabilities(
313 self.wlr_toplevel,
314 scheduled.capabilities,
315 );
316 ffi::wlr_xdg_toplevel_set_maximized(self.wlr_toplevel, scheduled.maximized);
317 ffi::wlr_xdg_toplevel_set_fullscreen(self.wlr_toplevel, scheduled.inform_fullscreen);
318 ffi::wlr_xdg_toplevel_set_resizing(self.wlr_toplevel, scheduled.resizing);
319
320 if !self.decoration.is_null() {
321 let mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
322 ffi::wlr_xdg_toplevel_decoration_v1_set_mode((*self.decoration).wlr_decoration, mode);
323 }
324
325 if scheduled.bounds.width != sent.bounds.width || scheduled.bounds.height != sent.bounds.height {
326 ffi::wlr_xdg_toplevel_set_bounds(self.wlr_toplevel, scheduled.bounds.width as i32, scheduled.bounds.height as i32);
327 }
328
329 let width = if let Some(w) = scheduled.width {
330 w
331 } else if let Some(w) = (*self.window).configure_sent.width {
332 w
333 } else {
334 self.geometry.width as u32
335 };
336
337 let height = if let Some(h) = scheduled.height {
338 h
339 } else if let Some(h) = (*self.window).configure_sent.height {
340 h
341 } else {
342 self.geometry.height as u32
343 };
344
345 if log::log_enabled!(log::Level::Debug) {
346 log::debug!(
347 "XdgToplevel::configure: sending size {}x{} (scheduled={:?}, sent={:?}, geometry={:?}) to client '{}'",
348 width, height, scheduled.width, sent.width, (self.geometry.width, self.geometry.height), (*self.window).get_title_string().unwrap_or_else(|| "None".to_string())
349 );
350 }
351
352 let configure_serial = ffi::wlr_xdg_toplevel_set_size(self.wlr_toplevel, width as i32, height as i32);
353
354 (*self.window).configure_sent = (*self.window).configure_scheduled.clone();
355 (*self.window).configure_sent.width = Some(width);
356 (*self.window).configure_sent.height = Some(height);
357 (*self.window).configure_scheduled.width = None;
358 (*self.window).configure_scheduled.height = None;
359
360 if width != 0 && height != 0 &&
361 width == self.geometry.width as u32 && height == self.geometry.height as u32 &&
362 matches!(self.configure_state, ConfigureState::Idle) {
363 return false;
364 }
365
366 self.configure_state = ConfigureState::Inflight(configure_serial);
367 true
368 }
369
370 pub unsafe fn needs_configure(&self) -> bool {
371 let scheduled = &(*self.window).configure_scheduled;
372 let sent = &(*self.window).configure_sent;
373
374 if scheduled.width.is_some() && scheduled.width != sent.width {
375 return true;
376 }
377 if scheduled.height.is_some() && scheduled.height != sent.height {
378 return true;
379 }
380 if scheduled.bounds.width != sent.bounds.width || scheduled.bounds.height != sent.bounds.height {
381 return true;
382 }
383 if scheduled.activated != sent.activated {
384 return true;
385 }
386 if scheduled.ssd != sent.ssd {
387 return true;
388 }
389 if scheduled.tiled != sent.tiled {
390 return true;
391 }
392 if scheduled.capabilities != sent.capabilities {
393 return true;
394 }
395 if scheduled.maximized != sent.maximized {
396 return true;
397 }
398 if scheduled.inform_fullscreen != sent.inform_fullscreen {
399 return true;
400 }
401 if scheduled.resizing != sent.resizing {
402 return true;
403 }
404
405 false
406 }
407 }
408
409 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
410 let toplevel = crate::container_of!(listener, XdgToplevel, destroy);
411
412 if !(*toplevel).decoration.is_null() {
413 XdgDecoration::deinit((*toplevel).decoration);
414 }
415
416 wl_listener_remove(&mut (*toplevel).destroy);
417 wl_listener_remove(&mut (*toplevel).ack_configure);
418 wl_listener_remove(&mut (*toplevel).map);
419 wl_listener_remove(&mut (*toplevel).unmap);
420 wl_listener_remove(&mut (*toplevel).commit);
421 wl_listener_remove(&mut (*toplevel).new_popup);
422 wl_listener_remove(&mut (*toplevel).request_show_window_menu);
423 wl_listener_remove(&mut (*toplevel).request_fullscreen);
424 wl_listener_remove(&mut (*toplevel).request_maximize);
425 wl_listener_remove(&mut (*toplevel).request_minimize);
426 wl_listener_remove(&mut (*toplevel).request_move);
427 wl_listener_remove(&mut (*toplevel).request_resize);
428 wl_listener_remove(&mut (*toplevel).set_parent);
429 wl_listener_remove(&mut (*toplevel).set_title);
430 wl_listener_remove(&mut (*toplevel).set_app_id);
431
432 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
433 ffi::river_wlr_xdg_surface_set_data(base, std::ptr::null_mut());
434 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
435 ffi::river_wlr_surface_set_data(surface, std::ptr::null_mut());
436
437 let window = (*toplevel).window;
438 (*window).impl_destroying();
439 match (*window).state {
440 crate::window::WindowState::Init | crate::window::WindowState::Closing => {}
441 crate::window::WindowState::Ready | crate::window::WindowState::Initialized | crate::window::WindowState::Mapped => {
442 (*window).set_closing();
443 (*(*window).server).wm.dirty_windowing();
444 }
445 }
446
447 let _ = Box::from_raw(toplevel);
448 }
449
450 unsafe extern "C" fn handle_unmap(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
451 let toplevel = crate::container_of!(listener, XdgToplevel, unmap);
452 (*(*toplevel).window).unmap();
453 }
454
455 unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
456 let toplevel = crate::container_of!(listener, XdgToplevel, map);
457 if let Err(e) = (*(*toplevel).window).map() {
458 log::error!("Window map failed: {}", e);
459 let client = ffi::wl_resource_get_client((*(*toplevel).wlr_toplevel).resource);
460 ffi::wl_client_post_no_memory(client);
461 return;
462 }
463
464 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
465 let mut new_geometry = std::mem::zeroed();
466 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
467 (*toplevel).geometry = new_geometry;
468
469 // A fullscreen state the client set before its first commit never raises
470 // request_fullscreen (wlroots only records it); honour it now that the
471 // window is mapped. A fresh floating spawn has no box yet — the arrange
472 // pass leaves it 0x0 so the acked-commit path adopts the natural size —
473 // but that next commit will be the fullscreen one, so seed the box with
474 // the size the window mapped at, or leaving fullscreen restores an
475 // output-sized window.
476 if ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel) {
477 let window = (*toplevel).window;
478 if (*window).box_geom.width <= 0 && (*window).box_geom.height <= 0 && new_geometry.width > 0 && new_geometry.height > 0 {
479 (*window).box_geom.width = new_geometry.width;
480 (*window).box_geom.height = new_geometry.height;
481 }
482 (*(*window).server).wm.apply_client_fullscreen(window, true);
483 }
484 // Status segments and Utility windows are SELF-sizing: their bounds
485 // track their own box, so the committed geometry is adopted as the box.
486 let is_self_sized = matches!(
487 (*(*toplevel).window).tiling_mode,
488 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
489 ) || (*(*toplevel).window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
490 if is_self_sized {
491 (*(*toplevel).window).box_geom.width = new_geometry.width;
492 (*(*toplevel).window).box_geom.height = new_geometry.height;
493 // A view-centered modal that is ALSO self-sizing was centered at map
494 // against a size it had not committed yet; redo it now that it has.
495 (*(*toplevel).window).take_pending_view_center();
496 (*(*(*toplevel).window).server).wm.dirty_windowing();
497 } else if (*(*toplevel).window).pending_view_center
498 && new_geometry.width > 0
499 && new_geometry.height > 0
500 {
501 // A view-centered FLOATING window whose first size just arrived (the
502 // file chooser): adopt the geometry and redo the centering, or the
503 // map-time 400x400-floor placement stands for a 900x500 dialog.
504 (*(*toplevel).window).box_geom.width = new_geometry.width;
505 (*(*toplevel).window).box_geom.height = new_geometry.height;
506 (*(*toplevel).window).take_pending_view_center();
507 (*(*(*toplevel).window).server).wm.dirty_windowing();
508 }
509 }
510
511 unsafe extern "C" fn handle_new_popup(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
512 let toplevel = crate::container_of!(listener, XdgToplevel, new_popup);
513 let wlr_xdg_popup = data as *mut ffi::wlr_xdg_popup;
514
515 let window = (*toplevel).window;
516 let capture_node = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree;
517 if let Err(e) = crate::xdg_popup::XdgPopup::create(
518 wlr_xdg_popup,
519 (*window).popup_tree,
520 capture_node,
521 (*window).popup_tree,
522 ) {
523 log::error!("Failed to create popup: {}", e);
524 ffi::wl_resource_post_no_memory((*wlr_xdg_popup).resource);
525 return;
526 }
527 // Opening a menu changes nothing the reorder pass's order hash can see,
528 // so it schedules no transaction: without this raise a tiled window's
529 // menu would stay under the floating plane until some unrelated restack
530 // came along. The pass re-applies it from then on.
531 (*(*window).server).wm.raise_focused_popups(window);
532 }
533
534 unsafe extern "C" fn handle_ack_configure(
535 listener: *mut ffi::wl_listener,
536 data: *mut std::ffi::c_void,
537 ) {
538 let toplevel = crate::container_of!(listener, XdgToplevel, ack_configure);
539 let acked_configure = data as *mut ffi::wlr_xdg_surface_configure;
540 let serial = (*acked_configure).serial;
541
542 // Any ack, matching serial or not, proves the client is answering
543 // configures: from here its geometry is a response, and `render_start`
544 // may adopt it again.
545 (*toplevel).acked_once = true;
546
547 match (*toplevel).configure_state {
548 ConfigureState::Inflight(s) => {
549 if serial == s {
550 (*toplevel).configure_state = ConfigureState::Acked;
551 }
552 }
553 ConfigureState::TimedOut(s) => {
554 if serial == s {
555 (*toplevel).configure_state = ConfigureState::TimedOutAcked;
556 }
557 }
558 _ => {}
559 }
560
561 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
562 let mut new_geometry = std::mem::zeroed();
563 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
564 (*toplevel).geometry = new_geometry;
565
566 // Status segments and Utility windows are SELF-sizing: their bounds
567 // track their own box, so the committed geometry is adopted as the box.
568 let is_self_sized = matches!(
569 (*(*toplevel).window).tiling_mode,
570 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
571 ) || (*(*toplevel).window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
572 if is_self_sized {
573 (*(*toplevel).window).box_geom.width = new_geometry.width;
574 (*(*toplevel).window).box_geom.height = new_geometry.height;
575 // A view-centered modal that is ALSO self-sizing was centered at map
576 // against a size it had not committed yet; redo it now that it has.
577 (*(*toplevel).window).take_pending_view_center();
578 (*(*(*toplevel).window).server).wm.dirty_windowing();
579 } else if (*(*toplevel).window).pending_view_center
580 && new_geometry.width > 0
581 && new_geometry.height > 0
582 {
583 // A view-centered FLOATING window whose first size just arrived (the
584 // file chooser): adopt the geometry and redo the centering, or the
585 // map-time 400x400-floor placement stands for a 900x500 dialog.
586 (*(*toplevel).window).box_geom.width = new_geometry.width;
587 (*(*toplevel).window).box_geom.height = new_geometry.height;
588 (*(*toplevel).window).take_pending_view_center();
589 (*(*(*toplevel).window).server).wm.dirty_windowing();
590 }
591 }
592
593 unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
594 let toplevel = crate::container_of!(listener, XdgToplevel, commit);
595 let window = (*toplevel).window;
596 // Grid-patch latch: the first commit after ack_grid_patch carries the
597 // buffer rendered for that patch — anchor to it from this commit on.
598 // Latching here (not at ack time) means an in-flight older buffer is
599 // never shown at the new anchor.
600 if let Some((serial, patch)) = (*window).grid_patch_acked.take() {
601 log::info!("[Grid] latched patch #{serial} on commit");
602 (*window).grid_patch_current = Some(patch);
603 // The commit swaps the BUFFER in the scene immediately, but the
604 // anchor (position/scale/box) otherwise waits for the next arrange
605 // pass — up to a frame of the new buffer drawn at the OLD patch's
606 // anchor, the occasional one-frame cell flicker at a patch swap.
607 // Apply the new anchor inline, mirroring the arrange Grid arm
608 // exactly (virtual_to_screen's truncating cast included); the next
609 // arrange re-affirms the same values.
610 {
611 let wm = &(*(*window).server).wm;
612 let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
613 let (mut ox, mut oy) = (0i32, 0i32);
614 let outputs_list = &(*(*window).server).om.outputs as *const ffi::wl_list
615 as *mut WlList;
616 let mut curr_out = (*outputs_list).next;
617 while curr_out != outputs_list {
618 let output = crate::container_of!(curr_out, crate::output::Output, link);
619 if (*output).sent.state == crate::output::OutputStateValue::Enabled {
620 let b = (*output).sent.box_layout();
621 ox = b.x;
622 oy = b.y;
623 break;
624 }
625 curr_out = (*curr_out).next;
626 }
627 if patch.scale > 0.0 {
628 // Rounded like every other virtual->screen placement (the
629 // arrange pass and the fallback lattice); truncation here put
630 // the latched patch a pixel off the lattice it replaces.
631 let (lcam, _, _) = wm.layout_camera();
632 let sx = ox + ((patch.x - lcam.pan_x) * zoom).round() as i32;
633 let sy = oy + ((patch.y - lcam.pan_y) * zoom).round() as i32;
634 (*window).rendering_requested.x = sx;
635 (*window).rendering_requested.y = sy;
636 (*window).scale = zoom / patch.scale;
637 (*window).box_geom.x = sx;
638 (*window).box_geom.y = sy;
639 (*window).box_geom.width = (patch.w * patch.scale).round() as i32;
640 (*window).box_geom.height = (patch.h * patch.scale).round() as i32;
641 }
642 }
643 // The grid sits in the optimized-blur capture set (backdrop layers):
644 // new patch content invalidates the shared blurred-backdrop cache,
645 // which nothing else re-bakes when the latch lands after the
646 // viewport has settled — translucent windows keep showing a blur of
647 // the pre-latch desktop.
648 ffi::river_scene_mark_optimized_blur_dirty((*(*window).server).scene.wlr_scene);
649 (*(*window).server).wm.dirty_windowing();
650 }
651 let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
652 let old_geometry = (*toplevel).geometry;
653 let mut new_geometry = std::mem::zeroed();
654 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
655 (*toplevel).geometry = new_geometry;
656
657 // A commit that changes the window-geometry box or the surface extent
658 // changes the surface↔frame mapping under a STATIONARY cursor (the scene
659 // helper re-anchors the subtree by -geometry; a grown buffer adds
660 // hoverable area): pointer focus and surface-local coords go stale with
661 // no motion to fix them, and the next click is dispatched against the old
662 // mapping or dropped — the cce-ui overflow-rim popovers exposed this.
663 // Deferred to idle: wlroots' own scene commit listeners re-anchor AFTER
664 // this handler, so an inline refresh would query the stale scene.
665 {
666 let surface = ffi::river_wlr_xdg_surface_get_surface(base);
667 let surf_size = (
668 ffi::river_wlr_surface_get_width(surface),
669 ffi::river_wlr_surface_get_height(surface),
670 );
671 let mapping_changed = old_geometry.x != new_geometry.x
672 || old_geometry.y != new_geometry.y
673 || old_geometry.width != new_geometry.width
674 || old_geometry.height != new_geometry.height
675 || surf_size != (*toplevel).last_surface_size;
676 (*toplevel).last_surface_size = surf_size;
677 if mapping_changed
678 && matches!((*window).state, crate::window::WindowState::Mapped)
679 {
680 (*(*window).server).input_manager.schedule_pointer_refresh();
681 }
682 }
683
684 // Borrowed: this runs on every commit of every client.
685 let app_id = (*window).app_id_str().unwrap_or_default();
686 let mut ignore_transparent = (*(*window).server).wm.layout.window_backdrop_blur_ignore_transparent;
687 if app_id.starts_with("cce-status") {
688 ignore_transparent = (*(*window).server).wm.layout.status_backdrop_blur_ignore_transparent;
689 }
690 let scale = (*window).scale;
691 let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
692 app_id.starts_with("cce-status");
693 let is_decorated = (*(*window).server).wm.is_decorated_app(&app_id);
694 // Status segments are SELF-sizing (their bounds track their own box), so
695 // the geometry of the commit being handled is the truth. `rendering_sent`
696 // is a render-start snapshot that lags a contract commit by a render pass
697 // — sizing the blur from it left a menu-sized blur ghost hanging below
698 // the strip until the next commit re-ran this path.
699 let (actual_w, actual_h) = if is_status && (*toplevel).geometry.width > 0 && (*toplevel).geometry.height > 0 {
700 ((*toplevel).geometry.width as u32, (*toplevel).geometry.height as u32)
701 } else {
702 (
703 if (*window).rendering_sent.width > 0 { (*window).rendering_sent.width } else { (*toplevel).geometry.width as u32 },
704 if (*window).rendering_sent.height > 0 { (*window).rendering_sent.height } else { (*toplevel).geometry.height as u32 },
705 )
706 };
707 let geom_w = (actual_w as f64 * scale) as i32;
708 let geom_h = (actual_h as f64 * scale) as i32;
709 // The same radius Window::set_rendering_state applies — the one
710 // `root_plate_radius_base`, not a mirrored copy: both paths drive the same
711 // blur node, and when two copies disagreed the corners flipped between
712 // rounded and square depending on which one ran last.
713 let radius = (*window).root_plate_radius_base();
714 // Same span widening as Window::set_rendering_state (part of the mirror).
715 let radius = if (*window).rendering_requested.circular {
716 radius
717 } else {
718 crate::window::widen_corner_radius(radius, actual_w as i32, actual_h as i32)
719 };
720 // Rounded corners do NOT require live blur: the corner shape is applied by the
721 // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
722 // the optimized node only re-bakes the shared offscreen cache
723 // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
724 // screen. The old `radius > 0` opt-out silently disabled the optimization for
725 // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
726 // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
727 let use_optimized = if is_status {
728 false
729 } else {
730 (*(*window).server).wm.layout.scenefx_optimized_blur
731 };
732 let blur_enabled = (*window).rendering_requested.blur && ((*window).wm_requested.ssd || is_decorated || is_status);
733 ffi::river_scene_node_enable_blur(
734 (*window).tree as *mut ffi::wlr_scene_node,
735 blur_enabled,
736 use_optimized,
737 ignore_transparent,
738 0,
739 0,
740 geom_w,
741 geom_h,
742 // geom_w/h are already scaled to device px; the radius must match.
743 (radius as f64 * scale) as i32,
744 );
745 (*window).sync_backdrop_compress();
746
747 let capture_node = &mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node;
748 let mut geom = std::mem::zeroed();
749 ffi::river_wlr_xdg_surface_get_geometry(base, &mut geom);
750 ffi::wlr_scene_subsurface_tree_set_clip(capture_node, &geom);
751
752 let mut min_w = 0;
753 let mut min_h = 0;
754 let mut max_w = 0;
755 let mut max_h = 0;
756 ffi::river_wlr_xdg_toplevel_get_requested_min_max_size((*toplevel).wlr_toplevel, &mut min_w, &mut min_h, &mut max_w, &mut max_h);
757
758 (*window).set_dimensions_hint(crate::window::DimensionsHint {
759 min_width: min_w as u32,
760 min_height: min_h as u32,
761 max_width: max_w as u32,
762 max_height: max_h as u32,
763 });
764
765 if ffi::river_wlr_xdg_surface_get_initial_commit(base) {
766 assert!((*window).state != crate::window::WindowState::Ready);
767 if (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
768 log::debug!("[LinkDbg] initial commit -> ready app={:?} was_state={:?} linked={}",
769 (*window).get_app_id_string(), (*window).state, (*window).is_linked());
770 }
771 (*window).state = crate::window::WindowState::Ready;
772 let mut new_geometry = std::mem::zeroed();
773 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
774 (*toplevel).geometry = new_geometry;
775
776 let is_self_sized = matches!(
777 (*window).tiling_mode,
778 crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Utility
779 ) || (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
780 if is_self_sized {
781 (*window).box_geom.width = new_geometry.width;
782 (*window).box_geom.height = new_geometry.height;
783 }
784
785 (*(*window).server).wm.dirty_windowing();
786 return;
787 }
788
789 if (*window).state != crate::window::WindowState::Mapped && (*window).state != crate::window::WindowState::Ready {
790 return;
791 }
792
793 // A self-sizing overlay (cce-cloud) repaints at a new size on its own, with no
794 // configure round trip. The size-change branches below can't catch it: they
795 // compare against (*toplevel).geometry, which already holds the new value by
796 // the time they run, so size_changed is never true. Track the live geometry
797 // here instead and move the border with it, in the same commit that puts the
798 // new buffer on screen — waiting for the WM cycle (which round-trips out to
799 // the external window-manager client) leaves the border a size behind.
800 // Utility windows self-size the same way (the arrange pass only ever
801 // sends them the "you choose" 0x0, so every size change originates in a
802 // client commit like this one). So do Popups: cce-cloud's launcher flags
803 // itself one via set_popup and auto-sizes to its filtered list, and the
804 // WM's Popup arm echoes box_geom back as the size — so without this the
805 // border and blur stayed at the size the popup opened at while the list
806 // shrank and grew under them as the user typed.
807 if matches!(
808 (*window).tiling_mode,
809 crate::tiling::TilingMode::Overlay
810 | crate::tiling::TilingMode::Utility
811 | crate::tiling::TilingMode::Popup
812 ) {
813 let mut live = std::mem::zeroed();
814 ffi::river_wlr_xdg_surface_get_geometry(base, &mut live);
815 if live.width > 0 && live.height > 0
816 && (live.width != (*window).box_geom.width || live.height != (*window).box_geom.height)
817 {
818 (*window).box_geom.width = live.width;
819 (*window).box_geom.height = live.height;
820 // render_finish would otherwise reset box_geom from the render-start
821 // snapshot (rendering_sent) and snap the border back to the old size.
822 (*window).self_resized = true;
823 (*window).draw_borders();
824 (*window).set_dimensions(live.width as u32, live.height as u32);
825 (*(*window).server).wm.dirty_windowing();
826 }
827 }
828
829 // Status segments self-size the same way when an in-surface menu grows or
830 // contracts the surface (no configure round trip). Track the live
831 // geometry in the same commit: box_geom feeds the expanded-state order
832 // hash that restacks the segment into the popups layer, so a lagging
833 // box_geom left the freshly opened menu stacked UNDER its sibling
834 // segments (their text drew sharp over the menu's blur) for a beat.
835 // Unlike the overlay branch, no set_dimensions — the WM deliberately
836 // leaves status segment sizes to the client.
837 if is_status {
838 let mut live = std::mem::zeroed();
839 ffi::river_wlr_xdg_surface_get_geometry(base, &mut live);
840 if live.width > 0 && live.height > 0
841 && (live.width != (*window).box_geom.width || live.height != (*window).box_geom.height)
842 {
843 (*window).box_geom.width = live.width;
844 (*window).box_geom.height = live.height;
845 (*window).self_resized = true;
846 (*(*window).server).wm.dirty_windowing();
847 }
848 }
849
850 match (*toplevel).configure_state {
851 ConfigureState::Idle | ConfigureState::Committed | ConfigureState::TimedOut(..) => {
852 // Nothing to do: client-initiated size/position changes CANNOT
853 // be detected here. The top of handle_commit already refreshed
854 // (*toplevel).geometry from this commit, so any comparison
855 // against it never fires (the branch that used to live here was
856 // dead for that reason). Self-sizing overlays are handled by the
857 // live-geometry sync above; clients resizing through a configure
858 // round trip land in the Acked arm.
859 }
860 ConfigureState::Inflight(..) => {
861 (*window).send_frame_done();
862 }
863 ConfigureState::Acked | ConfigureState::TimedOutAcked => {
864 let mut new_geometry = std::mem::zeroed();
865 ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
866 (*toplevel).geometry = new_geometry;
867
868 (*window).rendering_scheduled.width = new_geometry.width as u32;
869 (*window).rendering_scheduled.height = new_geometry.height as u32;
870
871 let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
872 (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
873 // Overlay included so its scheduled size tracks the client's own; the
874 // border itself is handled by the live-geometry sync above.
875 let is_overlay = (*window).tiling_mode == crate::tiling::TilingMode::Overlay;
876 let is_utility = (*window).tiling_mode == crate::tiling::TilingMode::Utility;
877 if matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup) || is_status || is_overlay || is_utility {
878 (*window).set_dimensions(new_geometry.width as u32, new_geometry.height as u32);
879 if is_status {
880 // Only a size that actually moved re-arranges. A status
881 // segment acks a configure and commits at its old size
882 // for every content refresh; each of those used to run
883 // a full manage/arrange/render transaction.
884 let (w, h) = (new_geometry.width, new_geometry.height);
885 if w != (*window).box_geom.width || h != (*window).box_geom.height {
886 (*window).box_geom.width = w;
887 (*window).box_geom.height = h;
888 (*(*window).server).wm.dirty_windowing();
889 }
890 }
891 }
892
893 let (dec_w, dec_h) = (*window).get_decorations_size();
894 if dec_w != (*window).last_decor_w || dec_h != (*window).last_decor_h {
895 (*window).last_decor_w = dec_w;
896 (*window).last_decor_h = dec_h;
897 (*(*window).server).wm.dirty_windowing();
898 }
899
900 if let Some(sent_w) = (*window).configure_sent.width {
901 if !(*window).wm_requested.ssd && dec_w > 0 && new_geometry.width as u32 == sent_w.saturating_sub(dec_w as u32) {
902 if !(*window).csd_buffer_size_bug {
903 (*window).csd_buffer_size_bug = true;
904 log::info!("Detected CSD buffer size bug for window '{}'. Activating workaround.", (*window).get_title_string().unwrap_or_default());
905 (*(*window).server).wm.dirty_windowing();
906 }
907 }
908 }
909
910 match (*toplevel).configure_state {
911 ConfigureState::Acked => {
912 (*toplevel).configure_state = ConfigureState::Committed;
913 (*(*window).server).wm.notify_configured();
914 }
915 ConfigureState::TimedOutAcked => {
916 (*toplevel).configure_state = ConfigureState::Idle;
917 (*(*window).server).wm.dirty_rendering();
918 }
919 _ => unreachable!(),
920 }
921 }
922 }
923
924 // Left/top-edge anchoring on the committed geometry — the shared
925 // Window::anchor_resize_commit; Xwayland windows take the same path from
926 // handle_window_commit.
927 let geometry = (*toplevel).geometry;
928 if let Some((final_x, final_y)) = (*window).anchor_resize_commit(geometry.width, geometry.height) {
929 let server = (*window).server;
930 // Keep the displayed buffer and the compensating position atomic.
931 // Live buffer (no configure in flight): the commit is already on
932 // screen, so move the scene tree in the same commit — waiting for
933 // the next render pass lets a frame composite the new size at the
934 // old position, jittering the anchored edges. Frozen buffer (saved
935 // for an in-flight configure): moving the tree now would shift the
936 // OLD-size buffer instead, so leave the position to the render pass
937 // (which restores the new buffer and applies it together) and make
938 // sure that pass runs promptly.
939 if !(*window).surfaces.saved {
940 ffi::river_scene_node_set_position_if_changed((*window).tree as *mut ffi::wlr_scene_node, final_x, final_y);
941 ffi::river_scene_node_set_position_if_changed((*window).popup_tree as *mut ffi::wlr_scene_node, final_x, final_y);
942 (*window).box_geom.width = geometry.width;
943 (*window).box_geom.height = geometry.height;
944 (*window).draw_borders();
945 } else {
946 (*server).wm.dirty_rendering();
947 }
948 }
949 }
950
951 unsafe extern "C" fn handle_request_show_window_menu(
952 listener: *mut ffi::wl_listener,
953 data: *mut std::ffi::c_void,
954 ) {
955 let toplevel = crate::container_of!(listener, XdgToplevel, request_show_window_menu);
956 let event = data as *mut ffi::wlr_xdg_toplevel_show_window_menu_event;
957 let window = (*toplevel).window;
958
959 (*window).wm_scheduled.show_window_menu_requested = Some(crate::window::ShowWindowMenuRequest {
960 x: (*event).x - (*toplevel).geometry.x,
961 y: (*event).y - (*toplevel).geometry.y,
962 });
963 (*(*window).server).wm.dirty_windowing();
964 }
965
966 unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
967 let toplevel = crate::container_of!(listener, XdgToplevel, request_fullscreen);
968 let window = (*toplevel).window;
969
970 if ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel) {
971 let wlr_output = ffi::river_wlr_xdg_toplevel_get_requested_fullscreen_output((*toplevel).wlr_toplevel);
972 if !wlr_output.is_null() {
973 let output = ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output;
974 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(output);
975 } else {
976 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
977 }
978 } else {
979 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Exit;
980 }
981 (*(*window).server).wm.dirty_windowing();
982 let enter = ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel);
983 (*(*window).server).wm.apply_client_fullscreen(window, enter);
984 }
985
986 unsafe extern "C" fn handle_request_maximize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
987 let toplevel = crate::container_of!(listener, XdgToplevel, request_maximize);
988 let window = (*toplevel).window;
989
990 // A Utility window declared itself content-shaped; a maximize would hand
991 // sizing back to the compositor. Explicitly ignored, not just unmapped
992 // from any affordance.
993 if (*window).tiling_mode == crate::tiling::TilingMode::Utility {
994 return;
995 }
996
997 if ffi::river_wlr_xdg_toplevel_get_requested_maximized((*toplevel).wlr_toplevel) {
998 (*window).tiling_mode = crate::tiling::TilingMode::Tiled;
999 (*window).mode_locked = true;
1000 (*window).wm_scheduled.maximize_requested = crate::window::MaximizeRequest::Maximize;
1001 } else {
1002 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1003 (*window).mode_locked = true;
1004 (*window).wm_scheduled.maximize_requested = crate::window::MaximizeRequest::Unmaximize;
1005 }
1006 (*(*window).server).wm.dirty_windowing();
1007 }
1008
1009 unsafe extern "C" fn handle_request_minimize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1010 let toplevel = crate::container_of!(listener, XdgToplevel, request_minimize);
1011 let window = (*toplevel).window;
1012
1013 (*window).wm_scheduled.minimize_requested = true;
1014 (*(*window).server).wm.dirty_windowing();
1015 }
1016
1017 unsafe extern "C" fn handle_request_move(
1018 listener: *mut ffi::wl_listener,
1019 data: *mut std::ffi::c_void,
1020 ) {
1021 let toplevel = crate::container_of!(listener, XdgToplevel, request_move);
1022 let event = data as *mut ffi::wlr_xdg_toplevel_move_event;
1023 let window = (*toplevel).window;
1024 let seat = ffi::river_wlr_seat_get_data((*(*event).seat).seat) as *mut crate::seat::Seat;
1025
1026 if ffi::wlr_seat_validate_pointer_grab_serial((*seat).wlr_seat, std::ptr::null_mut(), (*event).serial) {
1027 let initial_mode = (*window).tiling_mode;
1028 // A client's move (a cce-ui window-plate drag) is the bound edit-mode
1029 // drag in every way that matters: read the Tiled grab BEFORE the
1030 // un-tile below, so the drag snaps to whole cells and lands grid-
1031 // aligned, which re-tiles it (op_end's `settle_tiling`). Read after,
1032 // as it was until 2026-10-06, it was always false: the drag moved
1033 // freely and the window stayed floating wherever it was dropped.
1034 let grabbed_tiled = initial_mode == crate::tiling::TilingMode::Tiled;
1035 if initial_mode != crate::tiling::TilingMode::Floating
1036 && initial_mode != crate::tiling::TilingMode::Popup
1037 && initial_mode != crate::tiling::TilingMode::Fullscreen
1038 && initial_mode != crate::tiling::TilingMode::Overlay
1039 // A move must not cost a window its Utility mode.
1040 && initial_mode != crate::tiling::TilingMode::Utility
1041 {
1042 // As the bound drag: un-tile for the drag but keep the
1043 // cell-quantized geometry (cursor.rs).
1044 (*window).was_tiled = false;
1045 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1046 (*window).mode_locked = true;
1047 }
1048
1049 (*seat).focus(crate::seat::Focus::Window(window));
1050 (*(*window).server).wm.stop_panning_animation();
1051 let cursor = &mut (*seat).cursor;
1052 let cursor_x = (*cursor.wlr_cursor).x;
1053 let cursor_y = (*cursor.wlr_cursor).y;
1054
1055 (*seat).op = Some(crate::seat::SeatOp {
1056 sent_release: false,
1057 input: crate::seat::SeatOpInput::Pointer,
1058 start_x: cursor_x as i32,
1059 start_y: cursor_y as i32,
1060 x: cursor_x as i32,
1061 y: cursor_y as i32,
1062 window_ptr: window,
1063 op_type: crate::seat::PointerOpType::Move,
1064 start_win_x: (*window).box_geom.x,
1065 start_win_y: (*window).box_geom.y,
1066 start_win_w: (*window).box_geom.width as u32,
1067 start_win_h: (*window).box_geom.height as u32,
1068 start_win_virtual_x: (*window).virtual_x,
1069 start_win_virtual_y: (*window).virtual_y,
1070 start_tiling_mode: (*window).tiling_mode,
1071 start_was_tiled: grabbed_tiled,
1072 start_mode_locked: (*window).mode_locked,
1073 start_pan_x: (*(*window).server).wm.desk_pan_x,
1074 start_pan_y: (*(*window).server).wm.desk_pan_y,
1075 started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
1076 });
1077 cursor.op_start_pointer();
1078 cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
1079
1080 (*window).wm_scheduled.pointer_move_requested = seat;
1081 (*(*window).server).wm.dirty_windowing();
1082 }
1083 }
1084
1085 unsafe extern "C" fn handle_request_resize(
1086 listener: *mut ffi::wl_listener,
1087 data: *mut std::ffi::c_void,
1088 ) {
1089 let toplevel = crate::container_of!(listener, XdgToplevel, request_resize);
1090 let event = data as *mut ffi::wlr_xdg_toplevel_resize_event;
1091 let window = (*toplevel).window;
1092
1093 // Nothing may interactively resize a Utility window — its size is the
1094 // client's `settings()` data, not a drag. Rejected at the request, not
1095 // just left without an affordance.
1096 if (*window).tiling_mode == crate::tiling::TilingMode::Utility {
1097 return;
1098 }
1099
1100 let seat = ffi::river_wlr_seat_get_data((*(*event).seat).seat) as *mut crate::seat::Seat;
1101
1102 if ffi::wlr_seat_validate_pointer_grab_serial((*seat).wlr_seat, std::ptr::null_mut(), (*event).serial) {
1103 let initial_mode = (*window).tiling_mode;
1104 // As in `handle_request_move`: read the Tiled grab before the
1105 // un-tile, so a client's edge resize snaps to whole cells like the
1106 // bound one and re-tiles where it lands, instead of leaving the
1107 // window floating.
1108 let grabbed_tiled = initial_mode == crate::tiling::TilingMode::Tiled;
1109 if initial_mode != crate::tiling::TilingMode::Floating
1110 && initial_mode != crate::tiling::TilingMode::Popup
1111 && initial_mode != crate::tiling::TilingMode::Fullscreen
1112 {
1113 (*window).was_tiled = false;
1114 (*window).tiling_mode = crate::tiling::TilingMode::Floating;
1115 (*window).mode_locked = true;
1116 }
1117
1118 (*seat).focus(crate::seat::Focus::Window(window));
1119 (*(*window).server).wm.stop_panning_animation();
1120 let cursor = &mut (*seat).cursor;
1121 let cursor_x = (*cursor.wlr_cursor).x;
1122 let cursor_y = (*cursor.wlr_cursor).y;
1123
1124 let edges = crate::window::Edges::from_u32((*event).edges);
1125 (*seat).op = Some(crate::seat::SeatOp {
1126 sent_release: false,
1127 input: crate::seat::SeatOpInput::Pointer,
1128 start_x: cursor_x as i32,
1129 start_y: cursor_y as i32,
1130 x: cursor_x as i32,
1131 y: cursor_y as i32,
1132 window_ptr: window,
1133 op_type: crate::seat::PointerOpType::Resize {
1134 edges,
1135 },
1136 start_win_x: (*window).box_geom.x,
1137 start_win_y: (*window).box_geom.y,
1138 start_win_w: (*window).box_geom.width as u32,
1139 start_win_h: (*window).box_geom.height as u32,
1140 start_win_virtual_x: (*window).virtual_x,
1141 start_win_virtual_y: (*window).virtual_y,
1142 start_tiling_mode: (*window).tiling_mode,
1143 start_was_tiled: grabbed_tiled,
1144 start_mode_locked: (*window).mode_locked,
1145 start_pan_x: (*(*window).server).wm.desk_pan_x,
1146 start_pan_y: (*(*window).server).wm.desk_pan_y,
1147 started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
1148 });
1149 cursor.op_start_pointer();
1150 let cursor_name = crate::cursor::get_resize_cursor_name(edges);
1151 cursor.set_xcursor(cursor_name.as_ptr() as *const _);
1152
1153 (*window).wm_scheduled.pointer_resize_requested = Some(crate::window::PointerResizeRequest {
1154 seat,
1155 edges: (*event).edges,
1156 });
1157 (*(*window).server).wm.dirty_windowing();
1158 }
1159 }
1160
1161 unsafe extern "C" fn handle_set_parent(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1162 let toplevel = crate::container_of!(listener, XdgToplevel, set_parent);
1163 let window = (*toplevel).window;
1164 (*(*window).server).wm.dirty_windowing();
1165 }
1166
1167 unsafe extern "C" fn handle_set_title(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1168 let toplevel = crate::container_of!(listener, XdgToplevel, set_title);
1169 let window = (*toplevel).window;
1170 (*window).notify_title();
1171 }
1172
1173 unsafe extern "C" fn handle_set_app_id(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1174 let toplevel = crate::container_of!(listener, XdgToplevel, set_app_id);
1175 let window = (*toplevel).window;
1176 (*window).notify_app_id();
1177 }
1178
1179 unsafe extern "C" fn handle_decoration_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1180 let decoration = crate::container_of!(listener, XdgDecoration, destroy);
1181 XdgDecoration::deinit(decoration);
1182 }
1183
1184 unsafe extern "C" fn handle_decoration_request_mode(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1185 let decoration = crate::container_of!(listener, XdgDecoration, request_mode);
1186
1187 let base = ffi::river_wlr_xdg_toplevel_get_base((*(*decoration).wlr_decoration).toplevel);
1188 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1189 let window = (*toplevel).window;
1190
1191 let hint = match (*(*decoration).wlr_decoration).requested_mode {
1192 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_NONE => {
1193 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_NO_PREFERENCE
1194 }
1195 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE => {
1196 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_CSD
1197 }
1198 ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE => {
1199 ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_PREFERS_SSD
1200 }
1201 _ => ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_NO_PREFERENCE,
1202 };
1203 (*window).set_decoration_hint(hint);
1204
1205 if ffi::river_wlr_xdg_surface_get_initialized(base) {
1206 let mut mode = (*(*decoration).wlr_decoration).requested_mode;
1207 if mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_NONE {
1208 let server = (*window).server;
1209 let rule_ssd = (*server).wm.get_rule_for_window(window).and_then(|r| r.ssd);
1210 if let Some(true) = rule_ssd {
1211 mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
1212 } else {
1213 mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
1214 }
1215 }
1216 ffi::wlr_xdg_toplevel_decoration_v1_set_mode((*decoration).wlr_decoration, mode);
1217 (*window).wm_requested.ssd = mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
1218 (*(*window).server).wm.dirty_windowing();
1219 }
1220 }
1221
1222 impl XdgDecoration {
1223 pub unsafe fn init(wlr_decoration: *mut ffi::wlr_xdg_toplevel_decoration_v1) -> *mut Self {
1224 let base = ffi::river_wlr_xdg_toplevel_get_base((*wlr_decoration).toplevel);
1225 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1226
1227 let decoration = Box::into_raw(Box::new(XdgDecoration {
1228 wlr_decoration,
1229 destroy: std::mem::zeroed(),
1230 request_mode: std::mem::zeroed(),
1231 }));
1232
1233 (*toplevel).decoration = decoration;
1234
1235 let destroy_ptr = &mut (*decoration).destroy as *mut ffi::wl_listener as *mut WlListener;
1236 (*destroy_ptr).notify = Some(handle_decoration_destroy);
1237 wl_signal_add(&mut (*wlr_decoration).events.destroy, &mut (*decoration).destroy);
1238
1239 let req_mode_ptr = &mut (*decoration).request_mode as *mut ffi::wl_listener as *mut WlListener;
1240 (*req_mode_ptr).notify = Some(handle_decoration_request_mode);
1241 wl_signal_add(&mut (*wlr_decoration).events.request_mode, &mut (*decoration).request_mode);
1242
1243 if ffi::river_wlr_xdg_surface_get_initialized(base) {
1244 handle_decoration_request_mode(&mut (*decoration).request_mode, std::ptr::null_mut());
1245 }
1246
1247 decoration
1248 }
1249
1250 pub unsafe fn deinit(decoration: *mut XdgDecoration) {
1251 let base = ffi::river_wlr_xdg_toplevel_get_base((*(*decoration).wlr_decoration).toplevel);
1252 let toplevel = ffi::river_wlr_xdg_surface_get_data(base) as *mut XdgToplevel;
1253
1254 wl_listener_remove(&mut (*decoration).destroy);
1255 wl_listener_remove(&mut (*decoration).request_mode);
1256
1257 assert!(!(*toplevel).decoration.is_null());
1258 (*toplevel).decoration = std::ptr::null_mut();
1259
1260 let _ = Box::from_raw(decoration);
1261 }
1262 }
1263
1264