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