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

src/server/window.rs (260.2K)

   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, WlList, wl_list_insert, wl_list_remove, wl_list_remove_and_reinit, WlListener, wl_signal_add};
   6 use crate::wm_node::WmNode;
   7 use crate::xdg_toplevel::ConfigureState;
   8 
   9 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  10 pub enum WindowState {
  11     Init,
  12     Ready,
  13     Initialized,
  14     Mapped,
  15     Closing,
  16 }
  17 
  18 #[derive(Clone, Copy)]
  19 pub enum WindowImpl {
  20     Toplevel(*mut crate::xdg_toplevel::XdgToplevel),
  21     Xwayland(*mut crate::xwayland_window::XwaylandWindow),
  22     Destroying,
  23 }
  24 
  25 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  26 pub enum FullscreenRequest {
  27     NoRequest,
  28     Fullscreen(*mut crate::output::Output),
  29     Exit,
  30 }
  31 
  32 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  33 pub enum MaximizeRequest {
  34     NoRequest,
  35     Maximize,
  36     Unmaximize,
  37 }
  38 
  39 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  40 pub struct Dimensions {
  41     pub width: u32,
  42     pub height: u32,
  43 }
  44 
  45 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  46 pub struct DimensionsHint {
  47     pub min_width: u32,
  48     pub min_height: u32,
  49     pub max_width: u32,
  50     pub max_height: u32,
  51 }
  52 
  53 impl DimensionsHint {
  54     /// Clamp a requested content size to the client's declared range; a
  55     /// zero bound is "unset" (xdg-shell's convention) and leaves that side
  56     /// alone. A max below the min is the client's own contradiction and
  57     /// the min wins.
  58     pub fn clamp(&self, width: u32, height: u32) -> (u32, u32) {
  59         let mut w = width;
  60         let mut h = height;
  61         if self.max_width > 0 {
  62             w = w.min(self.max_width);
  63         }
  64         if self.max_height > 0 {
  65             h = h.min(self.max_height);
  66         }
  67         if self.min_width > 0 {
  68             w = w.max(self.min_width);
  69         }
  70         if self.min_height > 0 {
  71             h = h.max(self.min_height);
  72         }
  73         (w, h)
  74     }
  75 }
  76 
  77 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  78 pub struct Edges {
  79     pub top: bool,
  80     pub bottom: bool,
  81     pub left: bool,
  82     pub right: bool,
  83 }
  84 
  85 impl Edges {
  86     pub fn new() -> Self {
  87         Self { top: false, bottom: false, left: false, right: false }
  88     }
  89     pub fn from_u32(val: u32) -> Self {
  90         Self {
  91             top: (val & 1) != 0,
  92             bottom: (val & 2) != 0,
  93             left: (val & 4) != 0,
  94             right: (val & 8) != 0,
  95         }
  96     }
  97 }
  98 
  99 #[derive(Clone, Copy, Debug, PartialEq)]
 100 pub struct Border {
 101     pub edges: Edges,
 102     pub width: u32,
 103     /// Premultiplied-alpha RGBA, 0.0–1.0 per channel (scenefx convention).
 104     pub color: [f32; 4],
 105     /// Color while the pointer hovers the border (the grab surface).
 106     pub hover_color: [f32; 4],
 107     pub corner_radius: i32,
 108 }
 109 
 110 impl Border {
 111     pub fn none() -> Self {
 112         Self { edges: Edges::new(), width: 0, color: [0.0; 4], hover_color: [0.0; 4], corner_radius: 0 }
 113     }
 114 }
 115 
 116 /// A window-scale corner radius as scenefx should consume it: the configured
 117 /// nominal (circle-equivalent) radius widened by the curvature-match span
 118 /// factor, capped at half the smaller content extent so opposite corners
 119 /// can't overlap — the exact counterpart of cce-ui's
 120 /// `VkRenderer::clip_corner_radius`, which widens the clients' plate/clip
 121 /// corners the same way. `width`/`height` and the returned radius are in
 122 /// logical px; callers scale to device px where they already do.
 123 pub fn widen_corner_radius(nominal: i32, width: i32, height: i32) -> i32 {
 124     if nominal <= 0 {
 125         return nominal;
 126     }
 127     let widened = (nominal as f64 * crate::config::corner_span_factor()).round() as i32;
 128     widened.min(width.min(height) / 2)
 129 }
 130 
 131 /// One of the 8 interactive border zones. Each draws as its own visual
 132 /// element (corners as two-rect Ls) and highlights independently on hover.
 133 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 134 pub enum BorderElement {
 135     Top,
 136     Bottom,
 137     Left,
 138     Right,
 139     TopLeft,
 140     TopRight,
 141     BottomLeft,
 142     BottomRight,
 143 }
 144 
 145 impl BorderElement {
 146     /// Every zone, in `index()` order.
 147     pub const ALL: [BorderElement; 8] = [
 148         BorderElement::Top,
 149         BorderElement::Bottom,
 150         BorderElement::Left,
 151         BorderElement::Right,
 152         BorderElement::TopLeft,
 153         BorderElement::TopRight,
 154         BorderElement::BottomLeft,
 155         BorderElement::BottomRight,
 156     ];
 157 
 158     /// Index into `Window::border_reveal`. Declaration order; kept in one
 159     /// place so the reveal array and the enum can't drift apart.
 160     pub fn index(self) -> usize {
 161         match self {
 162             BorderElement::Top => 0,
 163             BorderElement::Bottom => 1,
 164             BorderElement::Left => 2,
 165             BorderElement::Right => 3,
 166             BorderElement::TopLeft => 4,
 167             BorderElement::TopRight => 5,
 168             BorderElement::BottomLeft => 6,
 169             BorderElement::BottomRight => 7,
 170         }
 171     }
 172 }
 173 
 174 /// Per-frame step of the hover fade, as a fraction of the remaining distance
 175 /// to the target (the same exponential-approach shape the viewport pan uses).
 176 pub const BORDER_FADE_STEP: f32 = 0.15;
 177 /// Below this the fade is treated as finished and snapped to its target.
 178 pub const BORDER_FADE_EPSILON: f32 = 0.004;
 179 
 180 /// Per-tick step of the fullscreen-toggle animation, as a fraction of the
 181 /// remaining distance to the target rect (the pan/border-fade shape).
 182 pub const FS_ANIM_STEP: f64 = 0.22;
 183 /// A channel within this many screen px of its target counts as settled.
 184 pub const FS_ANIM_EPSILON: f64 = 0.5;
 185 /// Hard cap on animation lifetime (~3s at 16ms) so a client that never
 186 /// commits its new size can't leave the window stuck mid-stretch.
 187 pub const FS_ANIM_MAX_TICKS: u32 = 180;
 188 
 189 /// State of an in-flight fullscreen-toggle animation, in screen px.
 190 #[derive(Clone, Copy)]
 191 pub struct FsAnim {
 192     pub x: f64,
 193     pub y: f64,
 194     pub w: f64,
 195     pub h: f64,
 196     /// The target only becomes real once the next arrange/configure lands;
 197     /// until the target has moved off the start rect the animation must not
 198     /// declare itself settled (start == target on the first ticks).
 199     pub moved: bool,
 200     pub ticks: u32,
 201 }
 202 
 203 /// Length of a corner zone, measured from the outer corner along each band.
 204 /// Shared by the visual segments (draw_borders) and the pointer zones
 205 /// (cursor.rs get_border_zone) so they always agree. `configured` comes from
 206 /// `border { corner_length= }`; 0 picks the auto formula. Never shorter than
 207 /// the band width, so a corner is at least its diagonal square. `r_out` is
 208 /// the corner ring's OUTER arc radius (the window silhouette radius plus the
 209 /// band; 0 for square windows): the zone must reach past the arc plus half a
 210 /// band of straight arm, or the widened window corners (~35 logical px)
 211 /// overflow the corner piece and the arc gets truncated mid-sweep.
 212 pub fn border_corner_len(bw: f64, configured: i32, r_out: f64) -> f64 {
 213     let cl = if configured > 0 {
 214         // Configured lengths predate the band doubling — scale them the same
 215         // way, and keep at least half a band of straight arm (arm = cl − bw)
 216         // so a corner can never collapse to a bare square. (corner_length=16
 217         // with the doubled 16px band used to yield arm = 0: corners vanished.)
 218         (configured as f64 * 2.0).max(1.5 * bw)
 219     } else {
 220         // 3× band: the corner arms reach well down each edge (they also
 221         // carry the rounded-corner arc, which eats into the straight run).
 222         (3.0 * bw).max(24.0)
 223     };
 224     cl.max(r_out + 0.5 * bw)
 225 }
 226 
 227 /// Floor on the border grab/reveal band, in unscaled layout pixels. Borders
 228 /// rest invisible until hovered, so the band is the only thing to aim at; a
 229 /// narrow target would be unusable.
 230 pub const HOVER_BAND_MIN: f64 = 16.0;
 231 
 232 /// Effective interactive border band width (unscaled): the configured border
 233 /// width DOUBLED — the hover/grab band runs twice the classic border — with
 234 /// the HOVER_BAND_MIN floor. Shared by the visual segments (draw_borders),
 235 /// the hit catchers, and the pointer zones (cursor::get_border_zone) so they
 236 /// can never drift apart.
 237 pub fn border_band_width(configured_width: u32) -> f64 {
 238     (configured_width as f64 * 2.0).max(HOVER_BAND_MIN)
 239 }
 240 
 241 /// Where the eight resize handles sit — one disc per zone, in
 242 /// `BorderElement::index()` order — for a window whose content is `w`×`h`
 243 /// ON SCREEN, with silhouette corner radius `r_in` and handle diameter `d`
 244 /// (all screen px). Returns the centres and the disc radius. The side discs
 245 /// are tangent to their side; a corner disc sits on the corner's diagonal,
 246 /// tangent to the rounded corner arc when that arc is wider than the disc
 247 /// and tucked into the two straight edges otherwise. The frame shader
 248 /// (scenefx `frame.frag`) lays out the same discs from the same inputs;
 249 /// `draw_borders` (the catchers) and `cursor::get_border_zone` (the hit
 250 /// test) both call this, so what is drawn is what grabs. Keep the shader
 251 /// and this in step.
 252 pub fn handle_disc_layout(w: f64, h: f64, r_in: f64, d: f64) -> ([(f64, f64); 8], f64) {
 253     let r = 0.5 * d;
 254     let t = if r_in > r { r_in - (r_in - r) / std::f64::consts::SQRT_2 } else { r };
 255     let centres = [
 256         (0.5 * w, r),     // Top
 257         (0.5 * w, h - r), // Bottom
 258         (r, 0.5 * h),     // Left
 259         (w - r, 0.5 * h), // Right
 260         (t, t),           // TopLeft
 261         (w - t, t),       // TopRight
 262         (t, h - t),       // BottomLeft
 263         (w - t, h - t),   // BottomRight
 264     ];
 265     (centres, r)
 266 }
 267 
 268 pub struct BorderRects {
 269     /// The old full-band hit catchers. Retired by the disc handles — the
 270     /// pointer between two discs must reach the app, not a catcher — and
 271     /// kept disabled.
 272     pub left: *mut ffi::wlr_scene_rect,
 273     pub right: *mut ffi::wlr_scene_rect,
 274     pub top: *mut ffi::wlr_scene_rect,
 275     pub bottom: *mut ffi::wlr_scene_rect,
 276     /// Invisible square catchers, one per handle disc, indexed by
 277     /// `BorderElement::index()`: they make a scene hit on a disc resolve to
 278     /// this window even where the client's input region does not cover it.
 279     pub segments: [*mut ffi::wlr_scene_rect; 8],
 280     /// The resize handles: all eight discs in one shader-drawn node.
 281     pub frame: *mut ffi::wlr_scene_frame,
 282     /// Parent of `segments`, living in the global border overlay layer rather
 283     /// than in the window tree. Tracks the window tree's position so the
 284     /// segments keep their window-local coordinates.
 285     pub tree: *mut ffi::wlr_scene_tree,
 286 }
 287 
 288 pub struct ShowWindowMenuRequest {
 289     pub x: i32,
 290     pub y: i32,
 291 }
 292 
 293 pub struct PointerResizeRequest {
 294     pub seat: *mut crate::seat::Seat,
 295     pub edges: u32,
 296 }
 297 
 298 pub struct WmScheduledState {
 299     pub dimensions_hint: DimensionsHint,
 300     pub decoration_hint: ffi::zcce_window_v1_decoration_hint,
 301     pub show_window_menu_requested: Option<ShowWindowMenuRequest>,
 302     pub fullscreen_requested: FullscreenRequest,
 303     pub maximize_requested: MaximizeRequest,
 304     pub minimize_requested: bool,
 305     pub dirty_app_id: bool,
 306     pub dirty_title: bool,
 307     pub pointer_move_requested: *mut crate::seat::Seat,
 308     pub pointer_resize_requested: Option<PointerResizeRequest>,
 309 }
 310 
 311 pub struct WmSentState {
 312     pub dimensions_hint: DimensionsHint,
 313     pub decoration_hint: ffi::zcce_window_v1_decoration_hint,
 314     pub parent: Option<crate::slotmap::Key>,
 315 }
 316 
 317 pub struct WmRequestedState {
 318     pub dimensions: Option<Dimensions>,
 319     pub bounds: Dimensions,
 320     pub ssd: bool,
 321     pub tiled: u32,
 322     pub capabilities: u32,
 323     pub resizing: bool,
 324     pub maximized: bool,
 325     pub fullscreen: *mut crate::output::Output,
 326     pub inform_fullscreen: bool,
 327     pub close: bool,
 328 }
 329 
 330 #[derive(Clone, Debug, PartialEq, Eq)]
 331 pub struct Configure {
 332     pub width: Option<u32>,
 333     pub height: Option<u32>,
 334     pub bounds: Dimensions,
 335     pub activated: bool,
 336     pub ssd: bool,
 337     pub tiled: u32,
 338     pub capabilities: u32,
 339     pub maximized: bool,
 340     pub inform_fullscreen: bool,
 341     pub resizing: bool,
 342 }
 343 
 344 impl Configure {
 345     pub fn new() -> Self {
 346         Self {
 347             width: None,
 348             height: None,
 349             bounds: Dimensions { width: 0, height: 0 },
 350             activated: false,
 351             ssd: false,
 352             tiled: 0,
 353             capabilities: 0,
 354             maximized: false,
 355             inform_fullscreen: false,
 356             resizing: false,
 357         }
 358     }
 359 }
 360 
 361 pub struct WindowRenderingScheduled {
 362     pub width: u32,
 363     pub height: u32,
 364     pub resend_dimensions: bool,
 365 }
 366 
 367 pub struct WindowRenderingSent {
 368     pub width: u32,
 369     pub height: u32,
 370     pub presentation_hint: ffi::zcce_output_v1_presentation_mode,
 371 }
 372 
 373 pub struct WindowRenderingRequested {
 374     pub x: i32,
 375     pub y: i32,
 376     pub hidden: bool,
 377     pub border: Border,
 378     pub clip: ffi::wlr_box,
 379     pub content_clip: ffi::wlr_box,
 380     pub opacity: f32,
 381     pub circular: bool,
 382     pub blur: bool,
 383 }
 384 
 385 pub struct Window {
 386     pub ref_key: crate::slotmap::Key,
 387     pub server: *mut Server,
 388     pub object: *mut ffi::wl_resource, // zcce_window_v1
 389     pub node: WmNode,
 390     pub state: WindowState,
 391     pub impl_type: WindowImpl,
 392     /// Where a two-finger scroll over this window becomes an emulated
 393     /// view drag (see `cursor::ViewDrag`), when the app has said so through
 394     /// `touchpad-view-regions`: rectangles in surface-local pixels, `[x, y,
 395     /// w, h]`. `None` means the whole window, which is what an app that
 396     /// never sends any gets. Outside the rectangles the scroll reaches the
 397     /// client untouched — Houdini's parameter editor scrolls, its 3D
 398     /// viewports tumble.
 399     pub view_regions: Option<Vec<[f64; 4]>>,
 400 
 401     pub tree: *mut ffi::wlr_scene_tree,
 402     pub fullscreen_background: *mut ffi::wlr_scene_rect,
 403     pub window_background: *mut ffi::wlr_scene_rect,
 404     /// scenefx drop shadow, first child of `tree` so it renders beneath
 405     /// everything else in the window; null if creation failed (shadow skipped).
 406     pub shadow: *mut ffi::wlr_scene_shadow,
 407     /// scenefx bevel node: the lit chamfer around the inside of the window's
 408     /// edge. Created LAST in the window tree so it draws over the surface —
 409     /// the rim overlays the client's outermost pixels. Null if creation
 410     /// failed (the effect is then simply absent).
 411     pub bevel: *mut ffi::wlr_scene_bevel,
 412     /// scenefx droplet node: for droplet-styled status segments, the
 413     /// backdrop refracted through the drop's lens. Created BEFORE the
 414     /// surfaces so it draws beneath the client's translucent drop. Null if
 415     /// creation failed (the effect is then simply absent).
 416     pub droplet: *mut ffi::wlr_scene_droplet,
 417     pub decorations_below: ffi::wl_list,
 418     pub decorations_below_tree: *mut ffi::wlr_scene_tree,
 419     pub surfaces: crate::scene::SaveableSurfaces,
 420     pub border: BorderRects,
 421     /// The border zone the pointer is over (set by cursor.rs); that segment
 422     /// draws in `hover_color` while set.
 423     pub hovered_border_element: Option<BorderElement>,
 424     /// The zone the ring was last DRAWN with, so `step_border_fade` can tell
 425     /// a hover change from a settled ring. In overview every zone already
 426     /// sits at full reveal, so a hover swap moves no reveal value at all —
 427     /// and a step keyed on reveal alone never repainted, leaving the shader
 428     /// on whatever zone the last unrelated commit happened to push. The
 429     /// highlight lagged one hover behind: "the wrong handle lights up".
 430     pub border_hover_drawn: Option<BorderElement>,
 431     /// Per-zone reveal factor, 0.0 (fully hidden) to 1.0 (fully drawn),
 432     /// indexed by `BorderElement::index`. Borders rest invisible and only the
 433     /// zone under the pointer fades in. Deliberately NOT part of
 434     /// `rendering_requested.border`, which the arrange pass rewrites wholesale
 435     /// every pass and would otherwise clobber.
 436     pub border_reveal: [f32; 8],
 437     /// How far this window is dimmed for lying OVER the adjust target, 0.0
 438     /// (full opacity) to 1.0 (`border.overlap_opacity`): a Floating window
 439     /// overlapping the window whose handles are up would hide them, so it
 440     /// eases down while the mode is on and back up when it ends. Stepped by
 441     /// `step_adjust_dim` on the border-fade timer; applied through
 442     /// `effective_opacity`.
 443     pub adjust_dim: f32,
 444     /// The map/close fade, 0.0 (invisible) to 1.0 (fully drawn). A window
 445     /// starts at 0 when it maps and eases to 1; a client that asks to close
 446     /// (`fade-out` on the control socket) eases it back to 0 and then exits.
 447     /// Applied through `effective_opacity`, so it MULTIPLIES the arrange
 448     /// pass's own opacity and the adjust-mode dim rather than fighting them.
 449     /// Stepped by `step_map_fade` on the border-fade timer.
 450     pub map_fade: f32,
 451     /// Where `map_fade` is easing to: 1.0 while the window lives, 0.0 once a
 452     /// close fade has been asked for.
 453     pub map_fade_target: f32,
 454     /// Linear per-tick step for `map_fade`, derived from the configured
 455     /// duration at the moment the fade starts. Linear, not the borders'
 456     /// exponential approach: an exponential close fade never actually
 457     /// reaches zero, and the client is waiting on a deadline to exit.
 458     pub map_fade_step: f32,
 459     pub decorations_above: ffi::wl_list,
 460     pub decorations_above_tree: *mut ffi::wlr_scene_tree,
 461     pub popup_tree: *mut ffi::wlr_scene_tree,
 462     pub capture_scene: *mut ffi::wlr_scene,
 463     pub capture_source: *mut ffi::wlr_ext_image_capture_source_v1,
 464     pub tiling_mode: crate::tiling::TilingMode,
 465     pub mode_locked: bool,
 466     pub is_new: bool,
 467     pub restored: bool,
 468     /// Position was decided at map time rather than by history: a one-shot
 469     /// `place-next` hint (widget-spawned picker opening at its control) or a
 470     /// view-centered session modal. Either way it suppresses the spawn
 471     /// viewport pan — the window is already where the user is looking.
 472     pub hint_placed: bool,
 473     /// A view-centering that ran before the window's real size was known and
 474     /// must be redone once it lands. Only self-sizing modals set it: their
 475     /// geometry arrives on a commit, well after `map()`, so the centering at
 476     /// map sees `mapped_size_hint`'s fallback and misses by half the
 477     /// difference between that and the truth.
 478     pub pending_view_center: bool,
 479     /// True only when the restored geometry came out of the startup restore queue
 480     /// (`state.json`'s window list). A window reopened later in the session matches
 481     /// `last_window_states` instead and leaves this false, so it still counts as a
 482     /// fresh spawn for `center_on_spawn`.
 483     pub session_restored: bool,
 484     pub restored_focused: bool,
 485     pub closed: bool,
 486     /// Set when the compositor asks this window to close, so `unmap` can tell
 487     /// a departure someone requested from a client that simply vanished.
 488     pub close_requested: bool,
 489     pub has_parent: bool,
 490     pub minimized: bool,
 491     /// While Some, the window is mid fullscreen-toggle: its on-screen rect is
 492     /// this box, eased toward the arranged geometry by `step_fs_anim` on the
 493     /// border-fade tick. `render_finish` draws at this rect (position, buffer
 494     /// stretch, backdrop, clip) instead of the settled geometry.
 495     pub fs_anim: Option<FsAnim>,
 496     /// Mode and lock this window had when a `SetWindowMode` made it
 497     /// Fullscreen; the policy's fullscreen toggle restores both on exit.
 498     /// Cleared by any `SetWindowMode` to another mode.
 499     pub pre_fullscreen: Option<(crate::tiling::TilingMode, bool)>,
 500     pub circular: bool,
 501     pub blur: bool,
 502     pub scale: f64,
 503     pub last_applied_scale: f64,
 504     pub virtual_x: f64,
 505     pub virtual_y: f64,
 506     pub resize_start_vx: f64,
 507     pub resize_start_vy: f64,
 508     pub resize_start_w: u32,
 509     pub resize_start_h: u32,
 510     pub resize_edges: Option<Edges>,
 511     /// Client hint: an in-surface popover (menu/dropdown) covers this rect,
 512     /// surface-local logical px (zcce set_popover_region). The overview
 513     /// resize ring is clipped away beneath it and its band does not grab
 514     /// there — the menu reads as in front of the chrome.
 515     pub popover_region: Option<ffi::wlr_box>,
 516     /// The client resized itself and the new-size buffer is already on screen, so
 517     /// `render_finish` must take the size from the live commit rather than the
 518     /// render-start snapshot (`rendering_sent`), which still holds the previous
 519     /// size and would snap the border back. Cleared once consumed.
 520     pub self_resized: bool,
 521     /// Status segments: the along-bar length last seen while the segment was
 522     /// at bar thickness. Feeds WindowSnapshot::status_collapsed_len so an
 523     /// EXPANDED segment (surface grown into an in-surface menu) keeps its
 524     /// frozen slot in the arrange pass.
 525     pub status_collapsed_len: i32,
 526     /// Set by the commit listener, cleared by the window-manager stream
 527     /// timer after a capture: the damage gate for `stream_server` frames.
 528     /// Starts true so a fresh subscriber gets an immediate first frame.
 529     pub stream_dirty: bool,
 530     /// Surface size at the last commit of a status segment, so
 531     /// `handle_window_commit` re-arranges only when the segment actually
 532     /// changed size rather than on every content refresh.
 533     pub status_commit_size: (i32, i32),
 534     pub commit: ffi::wl_listener,
 535     pub was_fullscreen: bool,
 536     pub saved_width: i32,
 537     pub saved_height: i32,
 538     pub saved_virtual_x: f64,
 539     pub saved_virtual_y: f64,
 540     pub was_tiled: bool,
 541     /// Declared the desktop-grid layer via zcce_toplevel_v1.set_grid (the
 542     /// app_id "cce-grid" convention also maps the role; the flag makes the
 543     /// declaration explicit and app_id-independent).
 544     pub grid_declared: bool,
 545     /// Grid windows: patch sent to the client, awaiting ack_grid_patch.
 546     pub grid_patch_pending: Option<(u32, crate::policy::api::GridPatch)>,
 547     /// Acked patch awaiting the client's next commit (the rendered buffer).
 548     pub grid_patch_acked: Option<(u32, crate::policy::api::GridPatch)>,
 549     /// The patch the CURRENT buffer covers — what arrange anchors to.
 550     pub grid_patch_current: Option<crate::policy::api::GridPatch>,
 551     pub grid_patch_serial: u32,
 552     /// The current patch was rendered under a style config that has since
 553     /// changed (reload, or a `layout` change to the desktop keys): re-issue
 554     /// it on the next arrange even though its coverage is still fine. See
 555     /// `WindowManager::invalidate_grid_patches`.
 556     pub grid_patch_stale: bool,
 557     /// The patch last issued was sized for a camera FLIGHT's destination —
 558     /// small enough for the client to render before the ramp lands, not the
 559     /// roomy cap-filling rect a resting camera wants for pan headroom. Once
 560     /// the camera is at rest with that patch latched, `update_grid_patches`
 561     /// re-issues the roomy one and clears this.
 562     pub grid_patch_flight: bool,
 563     pub saved_floating_width: i32,
 564     pub saved_floating_height: i32,
 565     pub saved_floating_virtual_x: f64,
 566     pub saved_floating_virtual_y: f64,
 567 
 568     pub wm_scheduled: WmScheduledState,
 569     pub wm_sent: WmSentState,
 570     pub wm_requested: WmRequestedState,
 571     pub configure_scheduled: Configure,
 572     pub configure_sent: Configure,
 573     pub rendering_scheduled: WindowRenderingScheduled,
 574     pub rendering_sent: WindowRenderingSent,
 575     pub rendering_requested: WindowRenderingRequested,
 576     pub box_geom: ffi::wlr_box,
 577     pub margin_x: i32,
 578     pub margin_y: i32,
 579     pub last_decor_w: i32,
 580     pub last_decor_h: i32,
 581     pub foreign_toplevel_handle: *mut ffi::wlr_ext_foreign_toplevel_handle_v1,
 582     pub wlr_toplevel_handle: *mut ffi::wlr_foreign_toplevel_handle_v1,
 583     pub csd_buffer_size_bug: bool,
 584     pub status_edge: StatusEdge,
 585 }
 586 
 587 pub use crate::policy::arrange::StatusEdge;
 588 
 589 impl Window {
 590     pub unsafe fn is_wine(&self) -> bool {
 591         false
 592     }
 593 
 594     /// The `surface { shadow tiled=false }` switch: a Tiled window (which is
 595     /// also what Maximized resolves to) casts no drop shadow when it is off.
 596     /// Floating, popup and every other mode are unaffected. Evaluated on both
 597     /// render paths, so a float/tile toggle restyles on the next arrange.
 598     pub unsafe fn wants_tiled_shadow(&self) -> bool {
 599         (*self.server).wm.layout.shadow_tiled
 600             || self.tiling_mode != crate::tiling::TilingMode::Tiled
 601     }
 602 
 603     pub unsafe fn is_fullscreen(&self) -> bool {
 604         self.tiling_mode == crate::tiling::TilingMode::Fullscreen
 605             || !self.wm_requested.fullscreen.is_null()
 606     }
 607 
 608     pub unsafe fn role(&self) -> crate::policy::api::WindowRole {
 609         if self.grid_declared {
 610             return crate::policy::api::WindowRole::Grid;
 611         }
 612         // Borrowed, not `get_app_id_string()`: this runs several times per
 613         // pointer-motion event (`is_status_bar`/`is_grid`/`is_wallpaper` in
 614         // the cursor passthrough) and per window per transaction, and each
 615         // call used to heap-allocate a String just to prefix-match it.
 616         let ptr = self.get_app_id();
 617         let app_id = if ptr.is_null() { None } else { std::ffi::CStr::from_ptr(ptr).to_str().ok() };
 618         crate::policy::api::WindowRole::from_app_id(app_id)
 619     }
 620 
 621     pub unsafe fn is_grid(&self) -> bool {
 622         self.role() == crate::policy::api::WindowRole::Grid
 623     }
 624 
 625     pub unsafe fn is_status_bar(&self) -> bool {
 626         self.role() == crate::policy::api::WindowRole::StatusBar
 627     }
 628 
 629     pub unsafe fn is_wallpaper(&self) -> bool {
 630         self.role() == crate::policy::api::WindowRole::Background
 631     }
 632 
 633     pub unsafe fn is_linked(&self) -> bool {
 634         let prev = self.node.link.prev;
 635         let next = self.node.link.next;
 636         if prev.is_null() || next.is_null() {
 637             return false;
 638         }
 639         let self_ptr = &self.node.link as *const ffi::wl_list as *mut ffi::wl_list;
 640         prev != self_ptr
 641     }
 642 
 643 
 644     pub unsafe fn create(impl_type: WindowImpl, server: *mut Server) -> Result<*mut Self, &'static str> {
 645         let hidden_tree = (*server).scene.hidden_tree;
 646         let tree = ffi::wlr_scene_tree_create(hidden_tree);
 647         if tree.is_null() {
 648             return Err("Failed to create tree");
 649         }
 650 
 651         let popup_tree = ffi::wlr_scene_tree_create(hidden_tree);
 652         if popup_tree.is_null() {
 653             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 654             return Err("Failed to create popup_tree");
 655         }
 656 
 657         let capture_scene = ffi::wlr_scene_create();
 658         if capture_scene.is_null() {
 659             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 660             ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
 661             return Err("Failed to create capture_scene");
 662         }
 663         // SceneFX 0.4 does not support restack_xwayland_surfaces
 664         // (*capture_scene).restack_xwayland_surfaces = false;
 665 
 666         // Created first so it is the bottom-most child: the cast shadow must render
 667         // beneath the (translucent) window content and its backgrounds. Geometry and
 668         // color are synced per-frame in update_shadow; a null pointer just disables
 669         // the effect rather than failing window creation.
 670         let shadow_color = [0.0f32, 0.0f32, 0.0f32, 0.55f32];
 671         let shadow = ffi::wlr_scene_shadow_create(tree, 0, 0, 0, 22.0, shadow_color.as_ptr());
 672         if !shadow.is_null() {
 673             ffi::wlr_scene_node_set_enabled(&mut (*shadow).node, false);
 674         }
 675 
 676         // Beneath the surfaces like the shadow: the refracted backdrop must
 677         // render under the client's translucent drop, not over it. Synced in
 678         // update_droplet; enabled only for droplet-styled status segments.
 679         let droplet = ffi::wlr_scene_droplet_create(tree, 0, 0);
 680         if !droplet.is_null() {
 681             ffi::wlr_scene_node_set_enabled(&mut (*droplet).node, false);
 682         }
 683 
 684         let black_color = [0.0f32, 0.0f32, 0.0f32, 1.0f32];
 685         let fullscreen_background = ffi::wlr_scene_rect_create(tree, 0, 0, black_color.as_ptr());
 686         if fullscreen_background.is_null() {
 687             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 688             ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
 689             ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
 690             return Err("Failed to create fullscreen rect");
 691         }
 692 
 693         let decorations_below_tree = ffi::wlr_scene_tree_create(tree);
 694 
 695         let clear_color = [0.0f32, 0.0f32, 0.0f32, 0.0f32];
 696         let window_background = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
 697         if window_background.is_null() {
 698             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 699             ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
 700             ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
 701             return Err("Failed to create window background rect");
 702         }
 703 
 704         let surfaces = match crate::scene::SaveableSurfaces::init(tree) {
 705             Ok(s) => s,
 706             Err(e) => {
 707                 ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 708                 ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
 709                 ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
 710                 return Err(e);
 711             }
 712         };
 713 
 714         // Created after the surfaces so it is ABOVE them in the window tree:
 715         // the bevel is an inner rim drawn over the client's outermost pixels,
 716         // not something tucked behind them. Geometry, light and colour are
 717         // synced per frame in update_bevel; a null pointer disables the
 718         // effect rather than failing window creation.
 719         let bevel_color = [1.0f32, 1.0f32, 1.0f32, 1.0f32];
 720         let bevel = ffi::wlr_scene_bevel_create(tree, 0, 0, 0, 0.0, bevel_color.as_ptr());
 721         if !bevel.is_null() {
 722             ffi::wlr_scene_node_set_enabled(&mut (*bevel).node, false);
 723         }
 724 
 725         // The invisible hit catchers stay in the window tree so pointer
 726         // hit-testing and z-order are unchanged. The visible segments live in
 727         // a sibling tree parented to the global border overlay layer, so a
 728         // revealed edge draws over the neighbouring window it overhangs.
 729         let border_left = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
 730         let border_right = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
 731         let border_top = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
 732         let border_bottom = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
 733 
 734         let border_tree = ffi::wlr_scene_tree_create((*server).scene.layers.border_overlay);
 735         if border_tree.is_null() {
 736             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
 737             ffi::wlr_scene_node_destroy(popup_tree as *mut ffi::wlr_scene_node);
 738             ffi::wlr_scene_node_destroy(&mut (*capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
 739             return Err("Failed to create window border tree");
 740         }
 741         let mut border_segments = [std::ptr::null_mut(); 8];
 742         for seg in border_segments.iter_mut() {
 743             *seg = ffi::wlr_scene_rect_create(border_tree, 0, 0, clear_color.as_ptr());
 744         }
 745         // The resize handles: one node draws all eight discs (scenefx
 746         // frame.frag). Not eight rounded scene rects, because a scene rect
 747         // takes the renderer's global corner shape — a squircle — so a rect
 748         // with radius half its size would not be a circle. The eight rects
 749         // above are the discs' invisible hit catchers.
 750         let border_frame = ffi::wlr_scene_frame_create(border_tree, 0, 0, 0, clear_color.as_ptr());
 751 
 752         let decorations_above_tree = ffi::wlr_scene_tree_create(tree);
 753 
 754         let mut window = Box::new(Window {
 755             view_regions: None,
 756             ref_key: crate::slotmap::Key { generation: 0, index: 0 },
 757             server,
 758             object: std::ptr::null_mut(),
 759             node: std::mem::zeroed(),
 760             state: WindowState::Init,
 761             impl_type,
 762             tree,
 763             fullscreen_background,
 764             window_background,
 765             shadow,
 766             bevel,
 767             droplet,
 768             decorations_below: std::mem::zeroed(),
 769             decorations_below_tree,
 770             surfaces,
 771             border: BorderRects {
 772                 left: border_left,
 773                 right: border_right,
 774                 top: border_top,
 775                 bottom: border_bottom,
 776                 segments: border_segments,
 777                 frame: border_frame,
 778                 tree: border_tree,
 779             },
 780             hovered_border_element: None,
 781             border_hover_drawn: None,
 782             border_reveal: [0.0; 8],
 783             adjust_dim: 0.0,
 784             // 1.0, not 0.0: a window only starts its fade in `map()`, and
 785             // one that never fades (fading disabled, a status segment) must
 786             // render at full strength from its first frame.
 787             map_fade: 1.0,
 788             map_fade_target: 1.0,
 789             map_fade_step: 1.0,
 790             decorations_above: std::mem::zeroed(),
 791             decorations_above_tree,
 792             popup_tree,
 793             capture_scene,
 794             capture_source: std::ptr::null_mut(),
 795             tiling_mode: crate::tiling::TilingMode::Floating,
 796             mode_locked: false,
 797             is_new: true,
 798             restored: false,
 799             hint_placed: false,
 800             pending_view_center: false,
 801             session_restored: false,
 802             restored_focused: false,
 803             closed: false,
 804             close_requested: false,
 805             has_parent: false,
 806             minimized: false,
 807             fs_anim: None,
 808             pre_fullscreen: None,
 809             circular: false,
 810             blur: false,
 811             scale: 1.0,
 812             last_applied_scale: 1.0,
 813             virtual_x: unsafe { (*server).wm.desk_pan_x + 100.0 },
 814             virtual_y: unsafe { (*server).wm.desk_pan_y + 100.0 },
 815             resize_start_vx: 0.0,
 816             resize_start_vy: 0.0,
 817             resize_start_w: 0,
 818             resize_start_h: 0,
 819             resize_edges: None,
 820             popover_region: None,
 821             self_resized: false,
 822             status_collapsed_len: 0,
 823             stream_dirty: true,
 824             status_commit_size: (0, 0),
 825             commit: std::mem::zeroed(),
 826             was_fullscreen: false,
 827             saved_width: 0,
 828             saved_height: 0,
 829             saved_virtual_x: 0.0,
 830             saved_virtual_y: 0.0,
 831             was_tiled: false,
 832             grid_declared: false,
 833             grid_patch_pending: None,
 834             grid_patch_acked: None,
 835             grid_patch_current: None,
 836             grid_patch_serial: 0,
 837             grid_patch_stale: false,
 838             grid_patch_flight: false,
 839             saved_floating_width: 0,
 840             saved_floating_height: 0,
 841             saved_floating_virtual_x: 0.0,
 842             saved_floating_virtual_y: 0.0,
 843             wm_scheduled: WmScheduledState {
 844                 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
 845                 decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
 846                 show_window_menu_requested: None,
 847                 fullscreen_requested: FullscreenRequest::NoRequest,
 848                 maximize_requested: MaximizeRequest::NoRequest,
 849                 minimize_requested: false,
 850                 dirty_app_id: false,
 851                 dirty_title: false,
 852                 pointer_move_requested: std::ptr::null_mut(),
 853                 pointer_resize_requested: None,
 854             },
 855             wm_sent: WmSentState {
 856                 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
 857                 decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
 858                 parent: None,
 859             },
 860             wm_requested: WmRequestedState {
 861                 dimensions: None,
 862                 bounds: Dimensions { width: 0, height: 0 },
 863                 ssd: false,
 864                 tiled: 0,
 865                 capabilities: 1 | 2 | 4 | 8,
 866                 resizing: false,
 867                 maximized: false,
 868                 fullscreen: std::ptr::null_mut(),
 869                 inform_fullscreen: false,
 870                 close: false,
 871             },
 872             configure_scheduled: Configure::new(),
 873             configure_sent: Configure::new(),
 874             rendering_scheduled: WindowRenderingScheduled {
 875                 width: 0,
 876                 height: 0,
 877                 resend_dimensions: false,
 878             },
 879             rendering_sent: WindowRenderingSent {
 880                 width: 0,
 881                 height: 0,
 882                 presentation_hint: ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
 883             },
 884             rendering_requested: WindowRenderingRequested {
 885                 x: 0,
 886                 y: 0,
 887                 hidden: false,
 888                 border: Border::none(),
 889                 clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
 890                 content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
 891                 opacity: 1.0f32,
 892                 circular: false,
 893                 blur: false,
 894             },
 895             box_geom: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
 896             margin_x: 0,
 897             margin_y: 0,
 898             last_decor_w: 0,
 899             last_decor_h: 0,
 900             foreign_toplevel_handle: std::ptr::null_mut(),
 901             wlr_toplevel_handle: std::ptr::null_mut(),
 902             csd_buffer_size_bug: false,
 903             status_edge: StatusEdge::Unspecified,
 904         });
 905 
 906         ffi::wl_list_init(&mut window.decorations_below);
 907         ffi::wl_list_init(&mut window.decorations_above);
 908 
 909         let raw = Box::into_raw(window);
 910         let key = (*(*raw).server).wm.windows.put(raw);
 911         (*raw).ref_key = key;
 912         (*raw).node.init(crate::wm_node::WmNodeTag::Window);
 913 
 914         ffi::wlr_scene_node_set_enabled(tree as *mut ffi::wlr_scene_node, false);
 915         ffi::wlr_scene_node_set_enabled(popup_tree as *mut ffi::wlr_scene_node, false);
 916         ffi::wlr_scene_node_set_enabled(fullscreen_background as *mut ffi::wlr_scene_node, false);
 917 
 918         crate::scene_node_data::SceneNodeData::attach(
 919             tree as *mut ffi::wlr_scene_node,
 920             crate::scene_node_data::SceneNodeDataVal::Window(raw),
 921         );
 922         crate::scene_node_data::SceneNodeData::attach(
 923             popup_tree as *mut ffi::wlr_scene_node,
 924             crate::scene_node_data::SceneNodeDataVal::Window(raw),
 925         );
 926         // The border segments sit outside the window tree; without data of
 927         // their own a hit on a revealed segment would resolve to no window at
 928         // all, so tag them with the window they belong to.
 929         crate::scene_node_data::SceneNodeData::attach(
 930             border_tree as *mut ffi::wlr_scene_node,
 931             crate::scene_node_data::SceneNodeDataVal::Window(raw),
 932         );
 933         ffi::wlr_scene_node_set_enabled(border_tree as *mut ffi::wlr_scene_node, false);
 934 
 935         Ok(raw)
 936     }
 937 
 938     pub unsafe fn set_impl(&mut self, impl_type: WindowImpl) {
 939         self.impl_type = impl_type;
 940     }
 941 
 942     pub unsafe fn impl_destroying(&mut self) {
 943         self.impl_type = WindowImpl::Destroying;
 944     }
 945 
 946     pub unsafe fn get_title(&self) -> *const libc::c_char {
 947         match self.impl_type {
 948             WindowImpl::Toplevel(toplevel) => {
 949                 if toplevel.is_null() {
 950                     std::ptr::null()
 951                 } else {
 952                     ffi::river_wlr_xdg_toplevel_get_title((*toplevel).wlr_toplevel)
 953                 }
 954             }
 955             WindowImpl::Xwayland(xwindow) => {
 956                 if xwindow.is_null() {
 957                     std::ptr::null()
 958                 } else {
 959                     (*(*xwindow).xsurface).title
 960                 }
 961             }
 962             WindowImpl::Destroying => std::ptr::null(),
 963         }
 964     }
 965 
 966     pub unsafe fn get_app_id(&self) -> *const libc::c_char {
 967         match self.impl_type {
 968             WindowImpl::Toplevel(toplevel) => {
 969                 if toplevel.is_null() {
 970                     std::ptr::null()
 971                 } else {
 972                     ffi::river_wlr_xdg_toplevel_get_app_id((*toplevel).wlr_toplevel)
 973                 }
 974             }
 975             WindowImpl::Xwayland(xwindow) => {
 976                 if xwindow.is_null() {
 977                     std::ptr::null()
 978                 } else {
 979                     (*(*xwindow).xsurface).class
 980                 }
 981             }
 982             WindowImpl::Destroying => std::ptr::null(),
 983         }
 984     }
 985 
 986     pub unsafe fn get_app_id_string(&self) -> Option<String> {
 987         let ptr = self.get_app_id();
 988         if ptr.is_null() {
 989             None
 990         } else {
 991             Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
 992         }
 993     }
 994 
 995     pub unsafe fn get_title_string(&self) -> Option<String> {
 996         let ptr = self.get_title();
 997         if ptr.is_null() {
 998             None
 999         } else {
1000             Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
1001         }
1002     }
1003 
1004     /// Dest-size factor for this window's surface buffers on top of the
1005     /// overview zoom: 1/output-scale for an X11 window under
1006     /// `xwayland_hidpi`, whose buffer is physical pixels (see
1007     /// `xwayland_window::x11_scale_for`); 1 for everything else, including
1008     /// an X11 window named in `xwayland_hidpi_except`.
1009     pub unsafe fn x11_buffer_scale(&self) -> f64 {
1010         if let WindowImpl::Xwayland(xwindow) = self.impl_type {
1011             let xsurface = if xwindow.is_null() { std::ptr::null() } else { (*xwindow).xsurface as *const _ };
1012             1.0 / crate::xwayland_window::x11_scale_for(self.server, xsurface) as f64
1013         } else {
1014             1.0
1015         }
1016     }
1017 
1018     pub unsafe fn get_parent(&self) -> *mut Window {
1019         match self.impl_type {
1020             WindowImpl::Toplevel(toplevel) => {
1021                 if toplevel.is_null() {
1022                     std::ptr::null_mut()
1023                 } else {
1024                     let wlr_parent = ffi::river_wlr_xdg_toplevel_get_parent((*toplevel).wlr_toplevel);
1025                     if wlr_parent.is_null() {
1026                         std::ptr::null_mut()
1027                     } else {
1028                         let base = ffi::river_wlr_xdg_toplevel_get_base(wlr_parent);
1029                         let parent_xdg = ffi::river_wlr_xdg_surface_get_data(base) as *mut crate::xdg_toplevel::XdgToplevel;
1030                         if parent_xdg.is_null() {
1031                             std::ptr::null_mut()
1032                         } else {
1033                             (*parent_xdg).window
1034                         }
1035                     }
1036                 }
1037             }
1038             WindowImpl::Xwayland(xwindow) => {
1039                 if xwindow.is_null() {
1040                     std::ptr::null_mut()
1041                 } else {
1042                     let parent_xsurface = (*(*xwindow).xsurface).parent;
1043                     if parent_xsurface.is_null() {
1044                         std::ptr::null_mut()
1045                     } else {
1046                         let parent_data = (*parent_xsurface).data;
1047                         if parent_data.is_null() {
1048                             std::ptr::null_mut()
1049                         } else {
1050                             let parent_xwindow = parent_data as *mut crate::xwayland_window::XwaylandWindow;
1051                             (*parent_xwindow).window
1052                         }
1053                     }
1054                 }
1055             }
1056             WindowImpl::Destroying => std::ptr::null_mut(),
1057         }
1058     }
1059 
1060     pub unsafe fn unreliable_pid(&self) -> i32 {
1061         match self.impl_type {
1062             WindowImpl::Toplevel(toplevel) => {
1063                 if toplevel.is_null() {
1064                     0
1065                 } else {
1066                     let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
1067                     let surface = ffi::river_wlr_xdg_surface_get_surface(base);
1068                     if surface.is_null() {
1069                         0
1070                     } else {
1071                         let res = ffi::river_wlr_surface_get_resource(surface);
1072                         if res.is_null() {
1073                             0
1074                         } else {
1075                             let client = ffi::wl_resource_get_client(res);
1076                             if client.is_null() {
1077                                 0
1078                             } else {
1079                                 let mut pid = 0;
1080                                 let mut uid = 0;
1081                                 let mut gid = 0;
1082                                 ffi::wl_client_get_credentials(client, &mut pid, &mut uid, &mut gid);
1083                                 pid
1084                             }
1085                         }
1086                     }
1087                 }
1088             }
1089             WindowImpl::Xwayland(xwindow) => {
1090                 if xwindow.is_null() {
1091                     0
1092                 } else {
1093                     (*(*xwindow).xsurface).pid
1094                 }
1095             }
1096             WindowImpl::Destroying => 0,
1097         }
1098     }
1099 
1100     /// Overlay-mode UI (cce-cloud menus and the like): takes keyboard input
1101     /// while open, but is invisible to the window manager's notion of "the
1102     /// focused window" — persistence, camera follow, arrange focus styling
1103     /// and refocus rules all look through it to the real window underneath.
1104     pub unsafe fn is_overlay_ui(&self) -> bool {
1105         self.tiling_mode == crate::tiling::TilingMode::Overlay
1106             || self.get_app_id_string().as_deref() == Some("cce-cloud")
1107     }
1108 
1109     /// A "shy" X11 window: a top-level that declines input focus
1110     /// (WM_HINTS input = False) and asks to be skipped by the taskbar —
1111     /// what Wine emits for a WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW window.
1112     /// Apps use those as helpers they place themselves: Ubisoft Connect
1113     /// keeps an untitled one exactly behind its borderless main window
1114     /// (the shadow-window trick), where Windows never shows it. Managed
1115     /// like an app window it was restored to a saved spot, pulled on-desk
1116     /// and raised — a blank white window with the app icon, over
1117     /// everything. So it is left to the client: no saved-state restore, its
1118     /// own position honoured at map and on request, never focused, never
1119     /// raised, stacked at the bottom.
1120     pub unsafe fn is_shy(&self) -> bool {
1121         let WindowImpl::Xwayland(xwindow) = self.impl_type else {
1122             return false;
1123         };
1124         if xwindow.is_null() || (*xwindow).xsurface.is_null() {
1125             return false;
1126         }
1127         let xs = (*xwindow).xsurface;
1128         if !(*xs).parent.is_null() || !(*xs).skip_taskbar || (*xs).hints.is_null() {
1129             return false;
1130         }
1131         let hints = (*xs).hints;
1132         let input_flag = ffi::xcb_icccm_wm_t_XCB_ICCCM_WM_HINT_INPUT as i32;
1133         (*hints).flags & input_flag != 0 && (*hints).input == 0
1134     }
1135 
1136     pub unsafe fn try_restore(&mut self) {
1137         if self.restored {
1138             return;
1139         }
1140         // No geometry is ever saved for a Utility window, so none may be
1141         // restored over it — a pre-Utility state.json entry for the same
1142         // app_id would otherwise dictate a stale size to a self-sizing
1143         // client. (Belt over suspenders: the arrange pass restates the
1144         // "you choose" 0x0 for Utility anyway, so even a slipped-through
1145         // restore heals on the client's next commit.)
1146         if self.tiling_mode == crate::tiling::TilingMode::Utility {
1147             return;
1148         }
1149         // A transient — an xdg toplevel with a parent, or an X11 window with
1150         // WM_TRANSIENT_FOR — is a dialog of the window it hangs off, and is
1151         // never what a saved entry describes. It shares its app_id with the
1152         // main window, so the app_id-only third pass of the state matchers
1153         // (kept for a relaunched main window whose title has changed) would
1154         // hand it the MAIN window's geometry: Houdini's Preferences opened at
1155         // the full 1856x1141 of the session it belongs to, and hkey's
1156         // "Redeem Result" at the administrator's size. The save pass skips
1157         // transients for the same reason, so there is nothing of their own to
1158         // restore either; they size themselves.
1159         if !self.get_parent().is_null() {
1160             return;
1161         }
1162         // For an X11 window that check is only meaningful once its properties
1163         // are all in: they arrive one PropertyNotify at a time, and WM_CLASS
1164         // (the app_id) lands before WM_TRANSIENT_FOR, so on the app_id notify
1165         // a dialog still looks parentless and the app_id-only match below
1166         // restored it anyway — first match wins, and the restore overwrites
1167         // the client's own requested size, so it cannot be undone when the
1168         // parent turns up. (Waiting for the wl_surface was not enough: GTK's
1169         // dialog was still title-less and parentless at association.) Wait
1170         // for `map`, which calls back in here; by then every property the
1171         // client set before mapping has been read.
1172         if matches!(self.impl_type, WindowImpl::Xwayland(_)) && self.state != WindowState::Mapped {
1173             return;
1174         }
1175         // A full-screen X11 game (`xwayland_hidpi_except`) sizes itself to
1176         // the screen; restoring a saved size onto it is what shrank
1177         // Trackmania to the launcher's 1214x689 — the game then pinned that
1178         // size in its hints and no fullscreen could take. Mark it restored
1179         // so nothing else tries.
1180         if crate::xwayland_window::window_is_hidpi_exempt(self as *const Window) {
1181             log::info!(
1182                 "Not restoring saved state for {:?}: named in xwayland_hidpi_except, it places itself",
1183                 self.get_title_string().unwrap_or_default()
1184             );
1185             self.restored = true;
1186             return;
1187         }
1188         // A shy helper window (no-activate, skip-taskbar) is placed by its
1189         // app, relative to the app's own windows — see `is_shy`.
1190         if self.is_shy() {
1191             log::info!(
1192                 "Not restoring saved state for {:?} ({}): a no-activate helper window, its app places it",
1193                 self.get_title_string().unwrap_or_default(),
1194                 self.get_app_id_string().unwrap_or_default()
1195             );
1196             self.restored = true;
1197             return;
1198         }
1199         let app_id_str = self.get_app_id_string().unwrap_or_default();
1200         if app_id_str.is_empty()
1201             || app_id_str.starts_with("cce-status")
1202             || app_id_str == "cce-wallpaper"
1203             || app_id_str == "cce-grid"
1204         {
1205             return;
1206         }
1207         let title_str = self.get_title_string().unwrap_or_default();
1208         let mut saved_opt = (*self.server).wm.match_and_remove_restore_state(&app_id_str, &title_str);
1209         let from_session = saved_opt.is_some();
1210         if saved_opt.is_none() {
1211             saved_opt = (*self.server).wm.match_last_window_state(&app_id_str, &title_str);
1212         }
1213         if let Some(saved) = saved_opt {
1214             log::info!("Restoring saved state for window: app_id={}, title={}. Position: ({}, {}), Size: {}x{}", app_id_str, title_str, saved.virtual_x, saved.virtual_y, saved.width, saved.height);
1215             self.tiling_mode = saved.tiling_mode;
1216             // `minimized` is session state, not app memory: a window the
1217             // user just opened must never be born hidden. On a
1218             // `last_window_states` borrow the flag is whatever the sibling
1219             // (or the app's last incarnation) happened to be doing — and a
1220             // parentless dialog matched by app_id alone inherits it from
1221             // the LIVE main window, which the user may well have minimized
1222             // to get it out of the way. Focused, listed, and invisible.
1223             if from_session {
1224                 self.minimized = saved.minimized;
1225             }
1226             self.virtual_x = saved.virtual_x;
1227             self.virtual_y = saved.virtual_y;
1228             self.scale = saved.scale;
1229             self.box_geom.width = saved.width as i32;
1230             self.box_geom.height = saved.height as i32;
1231             
1232             self.wm_requested.dimensions = Some(crate::window::Dimensions {
1233                 width: saved.width,
1234                 height: saved.height,
1235             });
1236             self.wm_requested.bounds = crate::window::Dimensions {
1237                 width: saved.width,
1238                 height: saved.height,
1239             };
1240             
1241             self.rendering_scheduled.width = saved.width;
1242             self.rendering_scheduled.height = saved.height;
1243             self.rendering_sent.width = saved.width;
1244             self.rendering_sent.height = saved.height;
1245 
1246             match self.impl_type {
1247                 WindowImpl::Toplevel(toplevel) => {
1248                     if !toplevel.is_null() {
1249                         (*toplevel).geometry.width = saved.width as i32;
1250                         (*toplevel).geometry.height = saved.height as i32;
1251                     }
1252                 }
1253                 WindowImpl::Xwayland(xwindow) => {
1254                     // This pre-writes the wlroots mirror so `render_finish`
1255                     // reports the saved size from the first frame; X itself
1256                     // is still at the window's natural size until the
1257                     // arrange pass configures it. That configure must not
1258                     // be deduplicated against this mirror — see
1259                     // `xwayland_window::needs_configure`, which also checks
1260                     // the geometry the compositor has actually sent.
1261                     if !xwindow.is_null() && !(*xwindow).xsurface.is_null() {
1262                         let s = crate::xwayland_window::x11_scale_for(self.server, (*xwindow).xsurface);
1263                         (*(*xwindow).xsurface).width = crate::xwayland_window::to_x11(saved.width as i32, s) as u16;
1264                         (*(*xwindow).xsurface).height = crate::xwayland_window::to_x11(saved.height as i32, s) as u16;
1265                     }
1266                 }
1267                 _ => {}
1268             }
1269 
1270             // A restored non-Floating mode is EXPLICIT state, and has to be
1271             // latched to survive. `get_mode_for_window` returns the window's own
1272             // mode only when `mode_locked`; unlocked, it resolves from the config
1273             // rules and falls through to Floating — and the arrange pass writes
1274             // that resolution straight back into `tiling_mode`
1275             // (`window_manager.rs`, the `wp.tiling_mode` apply). So a window
1276             // restored Tiled but unlocked was demoted by the very next arrange,
1277             // which is why a relaunched app came back floating however exactly
1278             // its geometry had been restored: position, size and cell were all
1279             // right, and the mode was gone before the first frame.
1280             //
1281             // Both sibling promotions already pair the mode with the lock — the
1282             // seat's op_end detection, and the geometric one just below, which is
1283             // why a window saved Floating-but-aligned survived while one saved
1284             // Tiled did not. Only Floating is left unlatched here, so a window
1285             // with no explicit mode still resolves from the rules as before.
1286             if saved.tiling_mode != crate::tiling::TilingMode::Floating {
1287                 self.mode_locked = true;
1288             }
1289 
1290             // Geometric promotion at restore time: a window whose saved
1291             // geometry sits cell-aligned IS tiled, even if an older session
1292             // saved it as Floating (pre-rework state, or a session that
1293             // never touched it after it landed on the grid). Same test and
1294             // lock as the op_end detection. No demotion here — a saved
1295             // Tiled window off the current grid is re-snapped by the Tiled
1296             // arrange arm instead.
1297             if self.tiling_mode == crate::tiling::TilingMode::Floating {
1298                 let sp = (*self.server).wm.layout.snap_params();
1299                 if crate::policy::snap::is_cell_aligned(
1300                     self.virtual_x,
1301                     self.virtual_y,
1302                     saved.width as f64,
1303                     saved.height as f64,
1304                     &sp,
1305                     1.0,
1306                 ) {
1307                     self.tiling_mode = crate::tiling::TilingMode::Tiled;
1308                     self.mode_locked = true;
1309                 }
1310             }
1311 
1312             // A remembered FLOATING position is only worth keeping if it is
1313             // where the user can see it. The camera at restore is wherever
1314             // the session left it (or wherever the user has panned since a
1315             // relaunch), and a floating window a screen away from that is
1316             // lost, not remembered: Inkscape's start screen came back a full
1317             // viewport above the desk every login, at the cell its previous
1318             // incarnation had been saved in, with nothing on screen to say
1319             // it existed. Tiled windows are the grid's and stay put.
1320             //
1321             // Unless it is on the tiled desk: a window within a viewport of
1322             // the tiled windows' bounding box (`tiled_desk_bounds`, the
1323             // session's tiled entries still to restore plus the tiled
1324             // windows already up) is placed beside content the user pans
1325             // along, and stays where it was put — cce-data-editor parked
1326             // left of the first column came back mid-view every login.
1327             if self.tiling_mode == crate::tiling::TilingMode::Floating && !self.minimized {
1328                 let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
1329                 let wm = &(*self.server).wm;
1330                 let cam = crate::policy::camera::Camera {
1331                     pan_x: wm.desk_pan_x,
1332                     pan_y: wm.desk_pan_y,
1333                     zoom: wm.desk_zoom,
1334                 };
1335                 let desk = wm.tiled_desk_bounds();
1336                 if let Some((nx, ny)) = crate::policy::camera::recalled_origin(
1337                     self.virtual_x,
1338                     self.virtual_y,
1339                     saved.width as f64,
1340                     saved.height as f64,
1341                     cam,
1342                     vp_w,
1343                     vp_h,
1344                     desk,
1345                 ) {
1346                     log::info!(
1347                         "Recalling off-view floating window into view: app_id={} remembered=({:.0},{:.0}) -> ({:.0},{:.0}) (tiled desk: {:?})",
1348                         app_id_str, self.virtual_x, self.virtual_y, nx, ny, desk
1349                     );
1350                     self.virtual_x = nx;
1351                     self.virtual_y = ny;
1352                 }
1353             }
1354 
1355             // A borrowed origin is a live sibling's origin whenever the
1356             // app_id-only pass matched a window of an app that is still
1357             // running: a parentless dialog (1Password's CLI "Authorize"
1358             // prompt, a second browser window) lands exactly on the main
1359             // window's top-left corner, where it reads as part of that
1360             // window rather than a new one. Cascade it off any mapped
1361             // sibling already sitting there, the way every stacking WM
1362             // offsets a new window from the last. Session entries are
1363             // exempt: a restored layout is where the user left it.
1364             if !from_session && self.tiling_mode == crate::tiling::TilingMode::Floating {
1365                 let (nx, ny) = self.cascade_off_siblings(&app_id_str, self.virtual_x, self.virtual_y);
1366                 if (nx, ny) != (self.virtual_x, self.virtual_y) {
1367                     log::info!(
1368                         "Cascading new {} window off a sibling at ({:.0},{:.0}) -> ({:.0},{:.0})",
1369                         app_id_str, self.virtual_x, self.virtual_y, nx, ny
1370                     );
1371                     self.virtual_x = nx;
1372                     self.virtual_y = ny;
1373                 }
1374             }
1375 
1376             self.restored = true;
1377             self.session_restored = from_session;
1378             // The saved `focused` flag only means something for the startup
1379             // restore queue; on a `last_window_states` borrow it is stale
1380             // (whether the app happened to be focused when last closed) and
1381             // must not feed the settle-phase focus gates.
1382             self.restored_focused = from_session && saved.focused;
1383         }
1384     }
1385 
1386     /// Step an origin diagonally until no mapped sibling of `app_id` (any
1387     /// window but this one) has its top-left within a few pixels of it.
1388     /// Bounded, so a pathological pile of siblings cannot walk a window off
1389     /// the desk: after `MAX_STEPS` the last candidate is taken as is.
1390     unsafe fn cascade_off_siblings(&self, app_id: &str, x: f64, y: f64) -> (f64, f64) {
1391         const STEP: f64 = 40.0;
1392         const NEAR: f64 = 4.0;
1393         const MAX_STEPS: usize = 8;
1394         let me = self as *const Window;
1395         let origins: Vec<(f64, f64)> = (*self.server)
1396             .wm
1397             .windows
1398             .iter()
1399             .copied()
1400             .filter(|&w| !w.is_null() && w as *const Window != me && !(*w).closed)
1401             .filter(|&w| matches!((*w).state, WindowState::Mapped))
1402             .filter(|&w| (*w).get_app_id_string().as_deref() == Some(app_id))
1403             .map(|w| ((*w).virtual_x, (*w).virtual_y))
1404             .collect();
1405         let taken = |cx: f64, cy: f64| {
1406             origins
1407                 .iter()
1408                 .any(|&(ox, oy)| (ox - cx).abs() <= NEAR && (oy - cy).abs() <= NEAR)
1409         };
1410         let (mut cx, mut cy) = (x, y);
1411         for _ in 0..MAX_STEPS {
1412             if !taken(cx, cy) {
1413                 break;
1414             }
1415             cx += STEP;
1416             cy += STEP;
1417         }
1418         (cx, cy)
1419     }
1420 
1421     /// Apply a one-shot `place-next` hint: land the window's top-left just
1422     /// below-right of the hinted layout position (the control that spawned
1423     /// it), clamped to the output so it stays fully on-screen. Runs after
1424     /// `try_restore` so the remembered SIZE is kept — only the position is
1425     /// overridden — and marks `hint_placed` so the spawn viewport pan is
1426     /// skipped (the window is already under the user's pointer).
1427     /// Layout box of the first enabled output — `(phys_x, phys_y, width,
1428     /// height)`, the viewport every placement decision is measured against.
1429     /// Falls back to a 1920x1080 box at the origin before any output is up.
1430     unsafe fn first_enabled_output_box(&self) -> (f64, f64, f64, f64) {
1431         let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
1432         let mut curr_out = (*outputs_list).next;
1433         while curr_out != outputs_list {
1434             let output = crate::container_of!(curr_out, crate::output::Output, link);
1435             if (*output).sent.state == crate::output::OutputStateValue::Enabled {
1436                 let b = (*output).sent.box_layout();
1437                 return (b.x as f64, b.y as f64, b.width as f64, b.height as f64);
1438             }
1439             curr_out = (*curr_out).next;
1440         }
1441         (0.0, 0.0, 1920.0, 1080.0)
1442     }
1443 
1444     /// Virtual position to layout (screen) position, ROUNDED — the same
1445     /// conversion the arrange pass makes (`PlacementCtx::virtual_to_screen`).
1446     /// Every writer of a window's screen origin has to agree on the
1447     /// rounding: the seat op and the resize-commit anchoring truncated while
1448     /// the arrange pass rounds, so whenever the fractional part was .5 or
1449     /// more the window stepped a pixel back and forth between a commit and
1450     /// the next arrange — a twitch on every resize step at overview zoom,
1451     /// and a one-pixel hop on grab and release.
1452     pub unsafe fn virtual_to_screen(&self, vx: f64, vy: f64) -> (i32, i32) {
1453         let wm = &(*self.server).wm;
1454         let (cam, _, _) = wm.layout_camera();
1455         let (out_x, out_y, _, _) = self.first_enabled_output_box();
1456         (
1457             out_x as i32 + ((vx - cam.pan_x) * cam.zoom).round() as i32,
1458             out_y as i32 + ((vy - cam.pan_y) * cam.zoom).round() as i32,
1459         )
1460     }
1461 
1462     /// Layout (screen) position back to a virtual position — the inverse of
1463     /// `virtual_to_screen`. A client that repositions itself hands us a
1464     /// SCREEN origin, but the arrange pass places a floating window from its
1465     /// VIRTUAL one, so a screen origin written on its own survives exactly
1466     /// until the next transaction and is then recomputed away.
1467     pub unsafe fn screen_to_virtual(&self, sx: i32, sy: i32) -> (f64, f64) {
1468         let wm = &(*self.server).wm;
1469         let (cam, _, _) = wm.layout_camera();
1470         let zoom = cam.zoom.max(0.01);
1471         let (out_x, out_y, _, _) = self.first_enabled_output_box();
1472         (
1473             cam.pan_x + (sx as f64 - out_x) / zoom,
1474             cam.pan_y + (sy as f64 - out_y) / zoom,
1475         )
1476     }
1477 
1478     /// Best-known window size in VIRTUAL units at map time. `box_geom` is the
1479     /// render pass's size and is only filled in once a frame has been drawn
1480     /// (or by `try_restore` from the saved geometry), so a first-ever launch
1481     /// falls back to the client's committed toplevel geometry.
1482     unsafe fn mapped_size_hint(&self) -> (f64, f64) {
1483         if self.box_geom.width > 0 && self.box_geom.height > 0 {
1484             return (self.box_geom.width as f64, self.box_geom.height as f64);
1485         }
1486         if let WindowImpl::Toplevel(toplevel) = self.impl_type {
1487             if !toplevel.is_null() {
1488                 let g = (*toplevel).geometry;
1489                 if g.width > 0 && g.height > 0 {
1490                     return (g.width as f64, g.height as f64);
1491                 }
1492             }
1493         }
1494         (400.0, 400.0)
1495     }
1496 
1497     unsafe fn try_hint_placement(&mut self) {
1498         let app_id = self.get_app_id_string().unwrap_or_default();
1499         if app_id.is_empty() {
1500             return;
1501         }
1502         // Claimed before the mode is judged, so a hint aimed at this window
1503         // does not linger and land on the next one to open.
1504         let Some((hx, hy, cell_anchored)) = (*self.server).wm.take_pending_placement(&app_id)
1505         else {
1506             return;
1507         };
1508         if cell_anchored {
1509             // TILED IS THE POINT here, unlike the position-only hint below: a
1510             // window that reopens filling four squares is exactly the case
1511             // this exists for. Only the modes that do not own a position at
1512             // all are excluded.
1513             if matches!(
1514                 self.tiling_mode,
1515                 crate::tiling::TilingMode::Fullscreen
1516                     | crate::tiling::TilingMode::Popup
1517                     | crate::tiling::TilingMode::Overlay
1518                     | crate::tiling::TilingMode::Status
1519             ) {
1520                 return;
1521             }
1522             self.place_on_invocation_cell(&app_id, hx, hy);
1523             return;
1524         }
1525         // Utility included: the hint moves only the POSITION, which a utility
1526         // window does not own — only its size is the client's.
1527         if !matches!(
1528             self.tiling_mode,
1529             crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Utility
1530         ) {
1531             return;
1532         }
1533 
1534         let (phys_x, phys_y, vp_w, vp_h) = self.first_enabled_output_box();
1535 
1536         let wm = &(*self.server).wm;
1537         let zoom = wm.desk_zoom.max(0.01);
1538         let (vw, vh) = self.mapped_size_hint();
1539         let (w, h) = (vw * zoom, vh * zoom);
1540 
1541         const OFFSET: f64 = 12.0; // context-menu-style drop below-right of the control
1542         const MARGIN: f64 = 8.0;
1543         let sx = (hx + OFFSET)
1544             .min(phys_x + vp_w - w - MARGIN)
1545             .max(phys_x + MARGIN);
1546         let sy = (hy + OFFSET)
1547             .min(phys_y + vp_h - h - MARGIN)
1548             .max(phys_y + MARGIN);
1549 
1550         // screen = phys + (virtual - desk_pan) * zoom  →  invert for virtual.
1551         self.virtual_x = wm.desk_pan_x + (sx - phys_x) / zoom;
1552         self.virtual_y = wm.desk_pan_y + (sy - phys_y) / zoom;
1553         self.hint_placed = true;
1554         log::info!(
1555             "place-next hint applied: app_id={} screen=({:.0},{:.0}) virtual=({:.1},{:.1})",
1556             app_id, sx, sy, self.virtual_x, self.virtual_y
1557         );
1558     }
1559 
1560     /// Step a freshly-spawned TILED window off any tiled window it would open
1561     /// on top of, keeping its size and staying as close to its intended spot
1562     /// as possible (`policy::spawn::nearest_free`).
1563     ///
1564     /// The remembered-position path has no idea whether that position is still
1565     /// free — it was when the window closed, and something else may have taken
1566     /// it since. Two tiled windows stacked on the same squares is never what
1567     /// was meant: tiled windows are the ones laid out to sit side by side.
1568     ///
1569     /// Deliberately narrow:
1570     /// - Only TILED windows are moved, and only tiled windows count as
1571     ///   obstacles. Floating windows overlap by nature; that is the difference
1572     ///   between the two modes, not a fault to correct.
1573     /// - Session restore is exempt. A restored layout is a layout the user
1574     ///   arranged and saved, and mapping order is arbitrary, so nudging there
1575     ///   would rearrange a deliberate desktop at every login.
1576     unsafe fn avoid_tiled_overlap(&mut self) {
1577         if self.session_restored || self.tiling_mode != crate::tiling::TilingMode::Tiled {
1578             return;
1579         }
1580         let wm = &(*self.server).wm;
1581         let sp = wm.layout.snap_params();
1582         if sp.cell_w <= 0.5 || sp.cell_h <= 0.5 {
1583             return;
1584         }
1585         let (vw, vh) = self.mapped_size_hint();
1586         if vw <= 0.0 || vh <= 0.0 {
1587             return;
1588         }
1589         let (c0, r0, c1, r1) = crate::policy::cells::window_span(
1590             self.virtual_x, self.virtual_y, vw, vh, sp.cell_w, sp.cell_h, sp.gap_width,
1591         );
1592         let want = crate::policy::spawn::CellBlock::new(c0, r0, c1, r1);
1593 
1594         let mut occupied = Vec::new();
1595         for &w in wm.windows.iter() {
1596             if w.is_null() || w == (self as *mut Window) || (*w).closed || (*w).minimized {
1597                 continue;
1598             }
1599             if !matches!((*w).state, WindowState::Mapped) {
1600                 continue;
1601             }
1602             if (*w).tiling_mode != crate::tiling::TilingMode::Tiled {
1603                 continue;
1604             }
1605             let (ow, oh) = ((*w).box_geom.width as f64, (*w).box_geom.height as f64);
1606             if ow <= 0.0 || oh <= 0.0 {
1607                 continue;
1608             }
1609             let (oc0, or0, oc1, or1) = crate::policy::cells::window_span(
1610                 (*w).virtual_x, (*w).virtual_y, ow, oh, sp.cell_w, sp.cell_h, sp.gap_width,
1611             );
1612             occupied.push(crate::policy::spawn::CellBlock::new(oc0, or0, oc1, or1));
1613         }
1614         if occupied.is_empty() {
1615             return;
1616         }
1617 
1618         // Bounded: a window that cannot find room nearby stays put rather than
1619         // being flung to an empty region of a desktop that has no edges.
1620         const SEARCH_SQUARES: i32 = 12;
1621         let free = crate::policy::spawn::nearest_free(want, &occupied, SEARCH_SQUARES);
1622         if free == want {
1623             return;
1624         }
1625         let (bx, by, _, _) = crate::policy::cells::block_rect(
1626             free.col0, free.row0, free.col1, free.row1,
1627             sp.cell_w, sp.cell_h, sp.gap_width, sp.cell_inset,
1628         );
1629         log::info!(
1630             "spawn overlap: {} would open on a tiled window at {} -> moved to {}",
1631             self.get_app_id_string().unwrap_or_default(),
1632             crate::policy::cells::span_label(want.col0, want.row0, want.col1, want.row1),
1633             crate::policy::cells::span_label(free.col0, free.row0, free.col1, free.row1),
1634         );
1635         self.virtual_x = bx;
1636         self.virtual_y = by;
1637     }
1638 
1639     /// Place this window on the grid square the user invoked it from, keeping
1640     /// its remembered SIZE and growing away from the windows already there
1641     /// (`policy::spawn::place_at_cell`).
1642     ///
1643     /// The size comes from the remembered geometry `try_restore` just applied,
1644     /// measured in whole squares: a window last seen filling four squares
1645     /// opens filling four squares, at the corner of the invocation square that
1646     /// leaves it clear of its neighbours.
1647     unsafe fn place_on_invocation_cell(&mut self, app_id: &str, hx: f64, hy: f64) {
1648         let wm = &(*self.server).wm;
1649         let sp = wm.layout.snap_params();
1650         if sp.cell_w <= 0.5 || sp.cell_h <= 0.5 {
1651             return;
1652         }
1653         let (phys_x, phys_y, vp_w, vp_h) = self.first_enabled_output_box();
1654         let zoom = wm.desk_zoom.max(0.01);
1655         // The hint is a layout point; the grid is in virtual coordinates.
1656         let inv_vx = wm.desk_pan_x + (hx - phys_x) / zoom;
1657         let inv_vy = wm.desk_pan_y + (hy - phys_y) / zoom;
1658         let col = crate::policy::cells::cell_index(inv_vx, sp.cell_w, sp.gap_width);
1659         let row = crate::policy::cells::cell_index(inv_vy, sp.cell_h, sp.gap_width);
1660 
1661         // Size in squares, from the geometry `try_restore` left in place.
1662         let (vw, vh) = self.mapped_size_hint();
1663         let (c0, r0, c1, r1) = crate::policy::cells::window_span(
1664             0.0, 0.0, vw, vh, sp.cell_w, sp.cell_h, sp.gap_width,
1665         );
1666         let (cols, rows) = (c1 - c0 + 1, r1 - r0 + 1);
1667 
1668         // Everything else already on the desktop, in squares. Chrome and the
1669         // canvas itself are not obstacles.
1670         let mut occupied = Vec::new();
1671         for &w in wm.windows.iter() {
1672             if w.is_null() || w == (self as *mut Window) || (*w).closed || (*w).minimized {
1673                 continue;
1674             }
1675             if !matches!((*w).state, WindowState::Mapped) {
1676                 continue;
1677             }
1678             if (*w).is_status_bar() || (*w).is_wallpaper() || (*w).is_grid() {
1679                 continue;
1680             }
1681             let (ow, oh) = ((*w).box_geom.width as f64, (*w).box_geom.height as f64);
1682             if ow <= 0.0 || oh <= 0.0 {
1683                 continue;
1684             }
1685             let (oc0, or0, oc1, or1) = crate::policy::cells::window_span(
1686                 (*w).virtual_x, (*w).virtual_y, ow, oh, sp.cell_w, sp.cell_h, sp.gap_width,
1687             );
1688             occupied.push(crate::policy::spawn::CellBlock::new(oc0, or0, oc1, or1));
1689         }
1690 
1691         // Visible squares, so a tie between two clear corners goes to the one
1692         // on screen.
1693         let view = {
1694             let (vx0, vy0) = (wm.desk_pan_x, wm.desk_pan_y);
1695             let (vx1, vy1) = (vx0 + vp_w / zoom, vy0 + vp_h / zoom);
1696             let c0 = crate::policy::cells::cell_index(vx0, sp.cell_w, sp.gap_width);
1697             let r0 = crate::policy::cells::cell_index(vy0, sp.cell_h, sp.gap_width);
1698             let c1 = crate::policy::cells::cell_index(vx1, sp.cell_w, sp.gap_width);
1699             let r1 = crate::policy::cells::cell_index(vy1, sp.cell_h, sp.gap_width);
1700             crate::policy::spawn::CellBlock::new(c0, r0, c1, r1)
1701         };
1702 
1703         let block = crate::policy::spawn::place_at_cell(col, row, cols, rows, &occupied, Some(view));
1704         let (bx, by, bw, bh) = crate::policy::cells::block_rect(
1705             block.col0, block.row0, block.col1, block.row1,
1706             sp.cell_w, sp.cell_h, sp.gap_width, sp.cell_inset,
1707         );
1708         self.virtual_x = bx;
1709         self.virtual_y = by;
1710         // A window that was filling whole squares keeps doing so — it is the
1711         // same window, in the same shape, somewhere else. One that was not
1712         // keeps its own size and simply starts at the square's corner.
1713         if self.tiling_mode == crate::tiling::TilingMode::Tiled {
1714             self.box_geom.width = bw.round() as i32;
1715             self.box_geom.height = bh.round() as i32;
1716             self.wm_requested.dimensions = Some(crate::window::Dimensions {
1717                 width: bw.round() as u32,
1718                 height: bh.round() as u32,
1719             });
1720         }
1721         self.hint_placed = true;
1722         log::info!(
1723             "place-next-cell: {} -> {} ({}x{} squares) at virtual ({:.0}, {:.0})",
1724             app_id,
1725             crate::policy::cells::span_label(block.col0, block.row0, block.col1, block.row1),
1726             cols, rows, bx, by
1727         );
1728     }
1729 
1730     /// Open a session modal in the middle of what the user is looking at,
1731     /// ignoring wherever it last sat.
1732     ///
1733     /// On a panning desktop a remembered position is actively wrong for these
1734     /// windows: the camera has almost always moved since the last time, so
1735     /// the window maps somewhere off-view and the prompt reads as never
1736     /// having appeared — which for the polkit agent means the privileged
1737     /// action silently times out.
1738     ///
1739     /// Runs after `try_restore`, so the remembered SIZE is still available
1740     /// and only the position is overridden — the same split
1741     /// `try_hint_placement` uses — and marks `hint_placed` so the spawn
1742     /// viewport pan is skipped: the window is already centered in view, and
1743     /// panning the camera to it would move the desktop out from under the
1744     /// user for a dialog that is about to close again.
1745     /// Windows that open centered on the current view rather than wherever
1746     /// they last were: DE session modals whose whole job is to interrupt, and
1747     /// which the user must be able to answer immediately.
1748     ///
1749     /// Hardcoded by app_id like the compositor's other DE-internal window
1750     /// classes (`cce-status*`/`cce-wallpaper`/`cce-grid` in `try_restore`,
1751     /// `cce-notifier`/`cce-cloud` in `get_mode_for_window`). The config's
1752     /// per-app window rules assign a tiling MODE, not a placement, so there
1753     /// is nothing there to hang this off yet.
1754     fn is_view_centered_modal(app_id: &str) -> bool {
1755         // The polkit prompt, and the file chooser cce-files runs in --select/
1756         // --save mode: both are spawned BY an action in the current view and
1757         // must be answered immediately — a remembered position is actively
1758         // wrong for them (the chooser used to map wherever the file manager
1759         // was last used, squares away from the app that opened it).
1760         app_id == "cce-authenticator" || app_id == "cce-filesystem-chooser"
1761     }
1762 
1763     unsafe fn try_center_on_view(&mut self) {
1764         let app_id = self.get_app_id_string().unwrap_or_default();
1765         if !Self::is_view_centered_modal(&app_id) {
1766             return;
1767         }
1768 
1769         // Whatever history says, a modal has to be visible and free-floating:
1770         // a restored Tiled mode would re-snap it onto a grid cell (undoing
1771         // the centering) and a restored `minimized` would hide the prompt
1772         // outright. `mode_locked` is the "explicit beats heuristic" latch, so
1773         // the arrange pass cannot geometrically re-promote it either.
1774         //
1775         // Utility is exempt from the mode forcing ONLY — like
1776         // `try_hint_placement`, this owns the window's POSITION, never its
1777         // size. A Utility window already satisfies everything the forcing is
1778         // for: it always floats, never tiles, and both the grid snap and the
1779         // overview displacement skip it. Overwriting the field would silently
1780         // strip the mode — `set_utility` arrives before map, and every Utility
1781         // gate reads `tiling_mode` RAW — leaving the modal resizable, its
1782         // geometry saved, and a stale size restored over it next time.
1783         if self.tiling_mode != crate::tiling::TilingMode::Utility {
1784             self.tiling_mode = crate::tiling::TilingMode::Floating;
1785         }
1786         self.mode_locked = true;
1787         self.minimized = false;
1788 
1789         // A self-sizing modal has not committed its geometry yet, so
1790         // `mapped_size_hint` here is still the 400x400 floor — centering
1791         // against that misses by half the difference from the real size (a
1792         // 640x360 prompt landed 120px right and 20px high). Center anyway so
1793         // the first frame is not wildly off, and latch a redo for the commit
1794         // that brings the truth.
1795         //
1796         // Any mode with unknown geometry latches the redo — not Utility only.
1797         // The file chooser disproved the old Utility-only reasoning: a
1798         // FLOATING self-sizer on its first ever run has no restored geometry
1799         // and no arrange-given size either, so it was centered against the
1800         // 400x400 floor and stuck there, ~250px off for a 900x500 dialog.
1801         // A Floating modal with restored geometry still skips the latch
1802         // (box_geom is already filled by the time we run).
1803         self.pending_view_center = self.box_geom.width <= 0 || self.box_geom.height <= 0;
1804         self.apply_view_centering();
1805     }
1806 
1807     /// The centering itself, split out so the self-sizing commit path can redo
1808     /// it once the client's real size lands.
1809     unsafe fn apply_view_centering(&mut self) {
1810         let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
1811         let wm = &(*self.server).wm;
1812         let zoom = wm.desk_zoom.max(0.01);
1813         let (w, h) = self.mapped_size_hint();
1814 
1815         // Policy owns the camera math; the output's origin cancels out of the
1816         // centering, so only the extent is needed per axis.
1817         self.virtual_x = crate::policy::camera::centered_window_origin(wm.desk_pan_x, vp_w, zoom, w);
1818         self.virtual_y = crate::policy::camera::centered_window_origin(wm.desk_pan_y, vp_h, zoom, h);
1819         self.hint_placed = true;
1820         log::info!(
1821             "view-centered modal: app_id={} size=({:.0}x{:.0}) zoom={:.2} virtual=({:.1},{:.1})",
1822             self.get_app_id_string().unwrap_or_default(),
1823             w, h, zoom, self.virtual_x, self.virtual_y
1824         );
1825     }
1826 
1827     /// Redo a latched view-centering now that a self-sizing modal's real
1828     /// geometry has arrived. One-shot: a later commit (or a user dragging the
1829     /// window) must not snap it back to the middle.
1830     pub unsafe fn take_pending_view_center(&mut self) {
1831         if !self.pending_view_center || self.box_geom.width <= 0 || self.box_geom.height <= 0 {
1832             return;
1833         }
1834         self.pending_view_center = false;
1835         self.apply_view_centering();
1836     }
1837 
1838     pub unsafe fn map(&mut self) -> Result<(), &'static str> {
1839         log::debug!("window '{:?}' mapped", self.get_title());
1840         if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
1841             log::debug!("[LinkDbg] map app={:?} was_state={:?} linked={}",
1842                 self.get_app_id_string(), self.state, self.is_linked());
1843         }
1844         assert!(!matches!(self.impl_type, WindowImpl::Destroying));
1845         assert_eq!(self.state, WindowState::Initialized);
1846         self.state = WindowState::Mapped;
1847 
1848         self.try_restore();
1849         self.try_hint_placement();
1850         // Last: a session modal's placement is not negotiable, so it wins
1851         // over both the remembered geometry and any stale place-next hint.
1852         self.try_center_on_view();
1853         // After every placement decision, including the invocation-square one:
1854         // whichever chose this spot, a tiled window must not open stacked on
1855         // another. The anchor rule already avoids that when any corner is
1856         // clear, so this only acts when none was.
1857         self.avoid_tiled_overlap();
1858 
1859         let surface = self.root_surface();
1860         if !surface.is_null() {
1861             let commit_listener = &mut self.commit as *mut ffi::wl_listener as *mut WlListener;
1862             (*commit_listener).notify = Some(handle_window_commit);
1863             wl_signal_add(ffi::river_wlr_surface_get_commit_signal(surface), &mut self.commit);
1864         }
1865 
1866         let app_id_ptr = self.get_app_id();
1867         let (is_status_bar, is_wallpaper) = if !app_id_ptr.is_null() {
1868             let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
1869             (app_id.starts_with("cce-status"), app_id.as_ref() == "cce-wallpaper")
1870         } else {
1871             (false, false)
1872         };
1873 
1874         if is_status_bar || is_wallpaper {
1875             self.tiling_mode = crate::tiling::TilingMode::Status;
1876             if is_status_bar && self.status_edge == StatusEdge::Unspecified {
1877                 let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
1878                 let name = if let Some(stripped) = app_id.strip_prefix("cce-status-interface-left-").or_else(|| app_id.strip_prefix("cce-status-left-")) {
1879                     stripped
1880                 } else if let Some(stripped) = app_id.strip_prefix("cce-status-interface-right-").or_else(|| app_id.strip_prefix("cce-status-right-")) {
1881                     stripped
1882                 } else {
1883                     &app_id
1884                 };
1885                 let mut loaded_edge = None;
1886                 if name == "light_source" {
1887                     let mut light_pos = 2.356194490192345_f32; // Default 135 deg in rad
1888                     if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
1889                         let val = cce_ui::config::parse_kdl_to_json(&content);
1890                         if let Some(wm_obj) = val.get("window_manager") {
1891                             if let Some(pos_val) = wm_obj.get("light_source_position") {
1892                                 if let Some(f) = pos_val.as_f64() {
1893                                     light_pos = f as f32;
1894                                 } else if let Some(i) = pos_val.as_i64() {
1895                                     let deg = i as f32;
1896                                     if deg > 2.0 * std::f32::consts::PI {
1897                                         light_pos = deg.to_radians();
1898                                     } else {
1899                                         light_pos = deg;
1900                                     }
1901                                 }
1902                             }
1903                         }
1904                     }
1905                     
1906                     let two_pi = 2.0 * std::f32::consts::PI;
1907                     let mut angle = light_pos % two_pi;
1908                     if angle < 0.0 {
1909                         angle += two_pi;
1910                     }
1911                     
1912                     let pi = std::f32::consts::PI;
1913                     let edge = if angle < pi / 8.0 || angle >= 15.0 * pi / 8.0 {
1914                         StatusEdge::Right
1915                     } else if angle < 3.0 * pi / 8.0 {
1916                         StatusEdge::TopRight
1917                     } else if angle < 5.0 * pi / 8.0 {
1918                         StatusEdge::TopCenter
1919                     } else if angle < 7.0 * pi / 8.0 {
1920                         StatusEdge::TopLeft
1921                     } else if angle < 9.0 * pi / 8.0 {
1922                         StatusEdge::Left
1923                     } else if angle < 11.0 * pi / 8.0 {
1924                         StatusEdge::BottomLeft
1925                     } else if angle < 13.0 * pi / 8.0 {
1926                         StatusEdge::BottomCenter
1927                     } else {
1928                         StatusEdge::BottomRight
1929                     };
1930                     loaded_edge = Some(edge);
1931                 } else if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
1932                     let val = cce_ui::config::parse_kdl_to_json(&content);
1933                     if let Some(layout_obj) = val.get("layout") {
1934                         if let Some(status_bar_obj) = layout_obj.get("status_bar") {
1935                             if let Some(edge_val) = status_bar_obj.get(name) {
1936                                 if let Some(edge_str) = edge_val.as_str() {
1937                                     loaded_edge = match edge_str.to_lowercase().as_str() {
1938                                         "left" => Some(StatusEdge::Left),
1939                                         "right" => Some(StatusEdge::Right),
1940                                         "top-left" => Some(StatusEdge::TopLeft),
1941                                         "top-center" => Some(StatusEdge::TopCenter),
1942                                         "top-right" => Some(StatusEdge::TopRight),
1943                                         "bottom-left" => Some(StatusEdge::BottomLeft),
1944                                         "bottom-center" => Some(StatusEdge::BottomCenter),
1945                                         "bottom-right" => Some(StatusEdge::BottomRight),
1946                                         _ => None,
1947                                     };
1948                                 }
1949                             }
1950                         }
1951                     }
1952                 }
1953                 self.status_edge = if let Some(edge) = loaded_edge {
1954                     edge
1955                 } else {
1956                     if app_id.contains("viewport") {
1957                         StatusEdge::TopLeft
1958                     } else if app_id.contains("window") {
1959                         StatusEdge::TopCenter
1960                     } else {
1961                         StatusEdge::TopRight
1962                     }
1963                 };
1964             }
1965         } else {
1966             let mut should_focus = true;
1967             if self.session_restored && self.restored_focused {
1968                 (*self.server).wm.restored_focused_window_mapped = true;
1969                 if (*self.server).wm.startup_input_seen {
1970                     // The user already typed/clicked somewhere (e.g. into
1971                     // the keepassxc unlock dialog) while this window was
1972                     // still loading — mapping now must not yank focus out
1973                     // from under them.
1974                     log::info!("[FocusRestore] Restored focused window {:?} mapped after user input; leaving focus alone", self.get_title());
1975                     should_focus = false;
1976                 } else {
1977                     log::info!("[FocusRestore] Restored focused window mapped: {:?}", self.get_title());
1978                 }
1979             } else if (*self.server).wm.has_restored_focused_window
1980                 && !(*self.server).wm.restored_focused_window_mapped
1981                 && !(*self.server).wm.startup_input_seen
1982             {
1983                 // Strict settle phase: until the session's focused window
1984                 // maps (or the user intervenes), NOTHING else auto-focuses —
1985                 // neither restored siblings mapping first nor autostarts.
1986                 // This also keeps the focus-follow pan parked at the saved
1987                 // camera instead of wandering to whichever window loads
1988                 // fastest.
1989                 log::info!("[FocusRestore] Holding focus for the session's focused window; {:?} maps unfocused", self.get_title());
1990                 should_focus = false;
1991             } else if (*self.server).wm.has_restored_focused_window
1992                 && (*self.server).wm.restored_focused_window_mapped
1993             {
1994                 if self.session_restored {
1995                     // A restored sibling mapping after the session's focused
1996                     // window: never steal back. Only true session restores —
1997                     // a mid-session spawn that borrowed geometry from
1998                     // last_window_states is a fresh launch and must focus
1999                     // (and spawn-pan) normally, else it maps invisible at
2000                     // its remembered off-viewport spot for the whole session.
2001                     log::info!("[FocusRestore] Blocking focus to non-focused restored window {:?} because restored focused window is already mapped", self.get_title());
2002                     should_focus = false;
2003                 } else if !(*self.server).wm.startup_input_seen {
2004                     // A window mapping unbidden while the session is still
2005                     // settling (no key/button pressed yet) — an autostart
2006                     // like keepassxc popping up after the restored windows.
2007                     // It must not steal focus (or drag the focus-follow pan
2008                     // over to itself) from the session's focused window.
2009                     log::info!("[FocusRestore] Blocking focus steal by unrestored window {:?} mapping before first input", self.get_title());
2010                     should_focus = false;
2011                 }
2012             }
2013 
2014             // A client whose connection broke rebuilds its surface from
2015             // scratch (cce-ui window_runner::run) and maps again seconds
2016             // later. The user never asked for that window, so it must not
2017             // take focus from whatever they moved on to.
2018             //
2019             // Keyed on the previous window vanishing WITHOUT a requested
2020             // close — not on matching saved state, which a mid-session spawn
2021             // does too and which must still focus and spawn-pan normally.
2022             if should_focus {
2023                 if let Some(app_id) = self.get_app_id_string() {
2024                     if (*self.server).wm.take_recent_vanish(&app_id) {
2025                         log::info!("[FocusRestore] Blocking focus steal by reconnecting client {:?} ({})", self.get_title(), app_id);
2026                         should_focus = false;
2027                     }
2028                 }
2029             }
2030 
2031             // A WORLD window spawning during overview pulls the session
2032             // out of it, landing at zoom 1 on the new window — the user
2033             // asked for it (launcher pick, spawn keybind). Chrome
2034             // (Popup/Overlay), status, wallpaper and the grid spawn without
2035             // disturbing the overview. Before the focus loop, so the
2036             // focus-follow pan sees the settled zoom-1 camera and no-ops.
2037             if should_focus
2038                 && (*self.server).wm.mode == crate::window_manager::WindowManagerMode::Overview
2039                 && !self.is_grid()
2040                 && !self.is_status_bar()
2041                 && !self.is_wallpaper()
2042             {
2043                 let resolved = (*self.server).wm.get_mode_for_window(self as *mut Window);
2044                 if !matches!(
2045                     resolved,
2046                     crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
2047                 ) {
2048                     (*self.server).wm.exit_overview_to_window(self as *mut Window);
2049                 }
2050             }
2051 
2052             // The grid layer never takes focus — it is desktop furniture,
2053             // not a window (it is also input-transparent, so focus here
2054             // would be unreachable-by-click and unswitchable-away for
2055             // keyboard input).
2056             if should_focus && !self.is_grid() {
2057                 let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2058                 let mut curr = (*seats).next;
2059                 while curr != seats {
2060                     let next = (*curr).next;
2061                     let seat = crate::container_of!(curr, crate::seat::Seat, link);
2062                     (*seat).focus(crate::seat::Focus::Window(self as *mut Window));
2063                     curr = next;
2064                 }
2065             }
2066         }
2067 
2068         // The open dissolve. Last in `map`, so the window is fully placed and
2069         // its scene tree built before the ramp touches it — and so a window
2070         // that failed to map never starts one. `start_map_fade` snaps rather
2071         // than ramps when fading is off or this surface opts out (status
2072         // segments, wallpaper), so there is no second branch here.
2073         let fade_ms = (*self.server).wm.layout.fade_in_ms;
2074         if self.wants_map_fade() && fade_ms > 0 {
2075             self.map_fade = 0.0;
2076         }
2077         self.start_map_fade(1.0, fade_ms);
2078 
2079         (*self.server).wm.dirty_windowing();
2080         Ok(())
2081     }
2082 
2083     pub unsafe fn set_closing(&mut self) {
2084         if self.state != WindowState::Closing {
2085             if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2086                 log::debug!("[LinkDbg] set_closing app={:?} was_state={:?} was_linked={}",
2087                     self.get_app_id_string(), self.state, self.is_linked());
2088             }
2089             self.state = WindowState::Closing;
2090             if self.is_linked() {
2091                 wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2092             }
2093         }
2094     }
2095 
2096     pub unsafe fn unmap(&mut self) {
2097         log::debug!("window '{:?}' unmapped", self.get_title());
2098         if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2099             log::debug!("[LinkDbg] unmap app={:?} state={:?} linked={}",
2100                 self.get_app_id_string(), self.state, self.is_linked());
2101         }
2102         if self.state != WindowState::Mapped {
2103             return;
2104         }
2105         // Nobody asked this window to go: either its program exited on its
2106         // own or — the case this feeds — its Wayland connection broke and
2107         // cce-ui is about to rebuild the surface on a fresh one. Chrome is
2108         // the exception: a Popup (the cce-cloud launcher) or an Overlay dock
2109         // closes itself as part of being used — Escape, a pick, a click-away,
2110         // a keyboard leave — and the next super+d inside the grace is a
2111         // deliberate relaunch that must focus, not a crashed client
2112         // reconnecting. Counting it left the reopened launcher unfocused.
2113         if !self.close_requested
2114             && !matches!(
2115                 self.tiling_mode,
2116                 crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
2117             )
2118         {
2119             if let Some(app_id) = self.get_app_id_string() {
2120                 (*self.server).wm.note_vanished(app_id);
2121             }
2122         }
2123         wl_listener_remove_safe(&mut self.commit);
2124         self.surfaces.save();
2125         assert!(!matches!(self.impl_type, WindowImpl::Destroying));
2126         self.set_closing();
2127         (*self.server).wm.dirty_windowing();
2128 
2129         if !self.foreign_toplevel_handle.is_null() {
2130             ffi::wlr_ext_foreign_toplevel_handle_v1_destroy(self.foreign_toplevel_handle);
2131             self.foreign_toplevel_handle = std::ptr::null_mut();
2132         }
2133         if !self.wlr_toplevel_handle.is_null() {
2134             ffi::wlr_foreign_toplevel_handle_v1_destroy(self.wlr_toplevel_handle);
2135             self.wlr_toplevel_handle = std::ptr::null_mut();
2136         }
2137 
2138 
2139     }
2140 
2141     pub unsafe fn close(&mut self) {
2142         self.close_requested = true;
2143         match self.impl_type {
2144             WindowImpl::Toplevel(toplevel) => {
2145                 if !toplevel.is_null() {
2146                     ffi::wlr_xdg_toplevel_send_close((*toplevel).wlr_toplevel);
2147                 }
2148             }
2149             WindowImpl::Xwayland(xwindow) => {
2150                 if !xwindow.is_null() {
2151                     ffi::wlr_xwayland_surface_close((*xwindow).xsurface);
2152                 }
2153             }
2154             WindowImpl::Destroying => {}
2155         }
2156     }
2157 
2158     pub unsafe fn destroy(window: *mut Window) {
2159         assert!(matches!((*window).impl_type, WindowImpl::Destroying));
2160         match (*window).state {
2161             WindowState::Init => {}
2162             WindowState::Closing => {
2163                 (*(*window).server).wm.dirty_windowing();
2164                 return;
2165             }
2166             _ => unreachable!(),
2167         }
2168         assert!((*window).object.is_null());
2169 
2170         let seats = &mut (*(*window).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2171         let mut curr = (*seats).next;
2172         while curr != seats {
2173             let next = (*curr).next;
2174             let seat = crate::container_of!(curr, crate::seat::Seat, link);
2175             if let crate::seat::Focus::Window(w) = (*seat).focused {
2176                 if w == window {
2177                     (*seat).focus(crate::seat::Focus::None);
2178                     (*(*window).server).wm.focus_next_visible_window(seat);
2179                 }
2180             }
2181             if let Some(ref op) = (*seat).op {
2182                 if op.window_ptr == window {
2183                     (*seat).op = None;
2184                 }
2185             }
2186             curr = next;
2187         }
2188 
2189 
2190 
2191         // Destroy decorations
2192         for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
2193             let list_head = decorations as *mut WlList;
2194             let mut curr = (*list_head).next;
2195             while curr != list_head {
2196                 let next = (*curr).next;
2197                 let dec = crate::container_of!(curr, Decoration, link);
2198                 (*dec).destroy();
2199                 curr = next;
2200             }
2201         }
2202 
2203         wl_listener_remove_safe(&mut (*window).commit);
2204         ffi::wlr_scene_node_destroy((*window).tree as *mut ffi::wlr_scene_node);
2205         ffi::wlr_scene_node_destroy((*window).popup_tree as *mut ffi::wlr_scene_node);
2206         // The border segments hang off the global overlay layer, not off
2207         // `tree`, so destroying the window tree does not take them with it.
2208         // Left behind they would both leak and keep a SceneNodeData pointing
2209         // at this freed window for the next hit test to find.
2210         ffi::wlr_scene_node_destroy((*window).border.tree as *mut ffi::wlr_scene_node);
2211         ffi::wlr_scene_node_destroy(&mut (*(*window).capture_scene).tree as *mut ffi::wlr_scene_tree as *mut ffi::wlr_scene_node);
2212 
2213         (*window).node.deinit();
2214 
2215         (*(*window).server).wm.remove_from_history(window);
2216         // A seat cursor may still name this window as its adjust target.
2217         // The next hover evaluation would replace it, but a window allocated
2218         // at the same address in the meantime must not inherit the ring.
2219         {
2220             let seats = &mut (*(*window).server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2221             let mut curr = (*seats).next;
2222             while curr != seats {
2223                 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2224                 if (*seat).cursor.adjust_hover == window {
2225                     (*seat).cursor.adjust_hover = std::ptr::null_mut();
2226                 }
2227                 curr = (*curr).next;
2228             }
2229         }
2230         (*(*window).server).wm.windows.remove((*window).ref_key);
2231         (*(*window).server).wm.check_clean_exit_progress();
2232 
2233         let _ = Box::from_raw(window);
2234     }
2235 
2236     pub unsafe fn set_dimensions_hint(&mut self, hint: DimensionsHint) {
2237         self.wm_scheduled.dimensions_hint = hint;
2238         if self.wm_sent.dimensions_hint != hint {
2239             // Overlay included: a self-sizing overlay (cce-cloud) changes its hint
2240             // on every resize, and skipping it meant no arrange pass was scheduled.
2241             // Utility for the same reason: it is self-sizing by definition.
2242             if matches!(self.tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Utility) {
2243                 (*self.server).wm.dirty_windowing();
2244             }
2245             self.wm_sent.dimensions_hint = hint;
2246         }
2247     }
2248 
2249     pub unsafe fn set_dimensions(&mut self, width: u32, height: u32) {
2250         self.rendering_scheduled.width = width;
2251         self.rendering_scheduled.height = height;
2252 
2253         if self.rendering_scheduled.resend_dimensions ||
2254            self.rendering_scheduled.width != self.rendering_sent.width ||
2255            self.rendering_scheduled.height != self.rendering_sent.height {
2256             (*self.server).wm.dirty_rendering();
2257         }
2258     }
2259 
2260     /// Is a pointer resize op on this window still in progress on any seat?
2261     pub unsafe fn resize_op_active(&self) -> bool {
2262         let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2263         let mut curr_seat = (*seats_list).next;
2264         while curr_seat != seats_list {
2265             let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
2266             if let Some(ref op) = (*seat).op {
2267                 if op.window_ptr == self as *const Window as *mut Window {
2268                     if let crate::seat::PointerOpType::Resize { .. } = op.op_type {
2269                         return true;
2270                     }
2271                 }
2272             }
2273             curr_seat = (*curr_seat).next;
2274         }
2275         false
2276     }
2277 
2278     /// Interactive-resize anchoring for the commit path, shared by every
2279     /// surface kind. While a LEFT/TOP edge is being dragged, the client's
2280     /// committed size decides where the window's origin goes: the opposite
2281     /// edge stays where the drag found it, so the dragged edge is the one
2282     /// that appears to move. Without this the origin holds still and the
2283     /// window grows away from the grabbed edge — which is what Xwayland
2284     /// windows did until they were routed through here: only the
2285     /// xdg-toplevel commit handler had the math.
2286     ///
2287     /// `committed_w`/`committed_h` are the size the client just committed,
2288     /// in `box_geom` units (content size; for wine X11 windows the caller has
2289     /// already taken the 32px frame off, as the render pass does). Updates
2290     /// the virtual position and the requested/box screen origin and returns
2291     /// that origin, or `None` when no resize is armed. The anchoring outlives
2292     /// the seat op by one commit — the last configure is usually still in
2293     /// flight at release — so the first commit after the op disarms it.
2294     pub unsafe fn anchor_resize_commit(&mut self, committed_w: i32, committed_h: i32) -> Option<(i32, i32)> {
2295         let edges = self.resize_edges?;
2296         let resize_active = self.resize_op_active();
2297 
2298         if edges.left {
2299             self.virtual_x = self.resize_start_vx + (self.resize_start_w as f64 - committed_w as f64);
2300         }
2301         if edges.top {
2302             self.virtual_y = self.resize_start_vy + (self.resize_start_h as f64 - committed_h as f64);
2303         }
2304 
2305         let (final_x, final_y) = self.virtual_to_screen(self.virtual_x, self.virtual_y);
2306         self.rendering_requested.x = final_x;
2307         self.rendering_requested.y = final_y;
2308         self.box_geom.x = final_x;
2309         self.box_geom.y = final_y;
2310 
2311         if !resize_active {
2312             self.resize_edges = None;
2313         }
2314         Some((final_x, final_y))
2315     }
2316 
2317     pub unsafe fn set_decoration_hint(&mut self, hint: ffi::zcce_window_v1_decoration_hint) {
2318         self.wm_scheduled.decoration_hint = hint;
2319         if hint != self.wm_sent.decoration_hint {
2320             (*self.server).wm.dirty_windowing();
2321             self.wm_sent.decoration_hint = hint;
2322         }
2323     }
2324 
2325     pub unsafe fn root_surface(&self) -> *mut ffi::wlr_surface {
2326         match self.impl_type {
2327             WindowImpl::Toplevel(toplevel) => {
2328                 if toplevel.is_null() {
2329                     std::ptr::null_mut()
2330                 } else {
2331                     let base = ffi::river_wlr_xdg_toplevel_get_base((*toplevel).wlr_toplevel);
2332                     ffi::river_wlr_xdg_surface_get_surface(base)
2333                 }
2334             }
2335             WindowImpl::Xwayland(xwindow) => {
2336                 if xwindow.is_null() || (*xwindow).xsurface.is_null() {
2337                     std::ptr::null_mut()
2338                 } else {
2339                     (*(*xwindow).xsurface).surface
2340                 }
2341             }
2342             _ => std::ptr::null_mut(),
2343         }
2344     }
2345 
2346     pub unsafe fn get_decorations_size(&self) -> (i32, i32) {
2347         if self.wm_requested.ssd {
2348             return (0, 0);
2349         }
2350         self.measure_decorations()
2351     }
2352 
2353     /// Raw client-side decoration size (surface minus geometry), regardless
2354     /// of the current SSD setting. Callers that honor SSD gate on it
2355     /// themselves.
2356     pub unsafe fn measure_decorations(&self) -> (i32, i32) {
2357         let surface = self.root_surface();
2358         if surface.is_null() {
2359             return (0, 0);
2360         }
2361         let surf_w = ffi::river_wlr_surface_get_width(surface);
2362         let surf_h = ffi::river_wlr_surface_get_height(surface);
2363         
2364         let (geom_w, geom_h) = match self.impl_type {
2365             WindowImpl::Toplevel(toplevel) => {
2366                 if toplevel.is_null() {
2367                     (surf_w, surf_h)
2368                 } else {
2369                     ((*toplevel).geometry.width, (*toplevel).geometry.height)
2370                 }
2371             }
2372             _ => (surf_w, surf_h),
2373         };
2374         
2375         let dec_w = (surf_w - geom_w).max(0);
2376         let dec_h = (surf_h - geom_h).max(0);
2377         (dec_w, dec_h)
2378     }
2379 
2380     pub unsafe fn send_frame_done(&self) {
2381         assert_eq!(self.state, WindowState::Mapped);
2382         if !matches!(self.impl_type, WindowImpl::Destroying) {
2383             let mut now = std::mem::zeroed();
2384             clock_gettime(libc::CLOCK_MONOTONIC, &mut now);
2385             let now_ffi = ffi::timespec {
2386                 tv_sec: now.tv_sec as _,
2387                 tv_nsec: now.tv_nsec as _,
2388             };
2389             ffi::wlr_surface_send_frame_done(self.root_surface(), &now_ffi);
2390         }
2391     }
2392 
2393     pub unsafe fn manage_start(&mut self) {
2394         match self.state {
2395             WindowState::Init => {}
2396             WindowState::Closing => {
2397                 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2398                     log::debug!("[LinkDbg] manage_start closing->init app={:?} was_linked={}",
2399                         self.get_app_id_string(), self.is_linked());
2400                 }
2401                 self.state = WindowState::Init;
2402                 self.wm_sent = WmSentState {
2403                     dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
2404                     decoration_hint: ffi::zcce_window_v1_decoration_hint_ZCCE_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
2405                     parent: None,
2406                 };
2407                 self.wm_requested = WmRequestedState {
2408                     dimensions: None,
2409                     bounds: Dimensions { width: 0, height: 0 },
2410                     ssd: false,
2411                     tiled: 0,
2412                     capabilities: 1 | 2 | 4 | 8,
2413                     resizing: false,
2414                     maximized: false,
2415                     fullscreen: std::ptr::null_mut(),
2416                     inform_fullscreen: false,
2417                     close: false,
2418                 };
2419                 self.rendering_sent = WindowRenderingSent {
2420                     width: 0,
2421                     height: 0,
2422                     presentation_hint: ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC,
2423                 };
2424                 self.rendering_requested = WindowRenderingRequested {
2425                     x: 0,
2426                     y: 0,
2427                     hidden: false,
2428                     border: Border::none(),
2429                     clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
2430                     content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
2431                     opacity: 1.0f32,
2432                     circular: false,
2433                     blur: false,
2434                 };
2435 
2436                 if self.is_linked() {
2437                     wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2438                 }
2439 
2440                 self.make_inert();
2441             }
2442             WindowState::Ready | WindowState::Initialized | WindowState::Mapped => {
2443                 let wm_v1 = (*self.server).wm.object;
2444                 if wm_v1.is_null() {
2445                     let is_linked = self.is_linked();
2446                     if !is_linked {
2447                         if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2448                             log::debug!("[LinkDbg] manage_start LINK app={:?} state={:?}",
2449                                 self.get_app_id_string(), self.state);
2450                         }
2451                         if !self.node.link.prev.is_null() && !self.node.link.next.is_null() {
2452                             wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2453                         }
2454                         let rendering_list = &mut (*self.server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
2455                         // The tail is the top of the stack. A shy helper
2456                         // window (`is_shy`) links at the head instead — beneath
2457                         // the app's own windows, where its app keeps it.
2458                         let anchor = if self.is_shy() { rendering_list } else { (*rendering_list).prev };
2459                         wl_list_insert(anchor, &mut self.node.link as *mut ffi::wl_list as *mut WlList);
2460 
2461                         if self.foreign_toplevel_handle.is_null() {
2462                             let list = (*self.server).foreign_toplevel_list;
2463                             let title = self.get_title();
2464                             let app_id = self.get_app_id();
2465                             let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2466                                 title,
2467                                 app_id,
2468                             };
2469                             let handle = ffi::wlr_ext_foreign_toplevel_handle_v1_create(list, &state);
2470                             if !handle.is_null() {
2471                                 self.foreign_toplevel_handle = handle;
2472                                 (*handle).data = self as *mut Window as *mut _;
2473                             }
2474                         }
2475 
2476                         if self.wlr_toplevel_handle.is_null() {
2477                             let manager = (*self.server).wlr_foreign_toplevel_manager;
2478                             let handle = ffi::wlr_foreign_toplevel_handle_v1_create(manager);
2479                             if !handle.is_null() {
2480                                 self.wlr_toplevel_handle = handle;
2481                                 let title = self.get_title();
2482                                 if !title.is_null() {
2483                                     ffi::wlr_foreign_toplevel_handle_v1_set_title(handle, title);
2484                                 }
2485                                 let app_id = self.get_app_id();
2486                                 if !app_id.is_null() {
2487                                     ffi::wlr_foreign_toplevel_handle_v1_set_app_id(handle, app_id);
2488                                 }
2489                             }
2490                         }
2491                         self.rendering_scheduled.resend_dimensions = true;
2492                     }
2493                     return;
2494                 }
2495                 let new_resource = self.object.is_null();
2496                 let window_v1 = if new_resource {
2497                     let client = ffi::wl_resource_get_client(wm_v1);
2498                     let res = ffi::wl_resource_create(client, &ffi::zcce_window_v1_interface, ffi::wl_resource_get_version(wm_v1), 0);
2499                     if res.is_null() {
2500                         log::error!("out of memory");
2501                         return;
2502                     }
2503                     self.object = res;
2504                     self.rendering_scheduled.resend_dimensions = true;
2505                     ffi::wl_resource_set_implementation(
2506                         res,
2507                         &WINDOW_INTERFACE as *const _ as *const _,
2508                         self as *mut Window as *mut _,
2509                         Some(handle_destroy_resource),
2510                     );
2511                     
2512                     // Send window to manager
2513                     ffi::wl_resource_post_event(wm_v1, ffi::ZCCE_WINDOW_MANAGER_V1_WINDOW, res); // zcce_window_manager_v1.window
2514                     res
2515                 } else {
2516                     self.object
2517                 };
2518 
2519                 let is_linked = self.is_linked();
2520                 if !is_linked {
2521                     if !self.node.link.prev.is_null() && !self.node.link.next.is_null() {
2522                         wl_list_remove_and_reinit(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
2523                     }
2524                     let rendering_list = &mut (*self.server).wm.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
2525                     wl_list_insert((*rendering_list).prev, &mut self.node.link as *mut ffi::wl_list as *mut WlList);
2526 
2527                     if self.foreign_toplevel_handle.is_null() {
2528                         let list = (*self.server).foreign_toplevel_list;
2529                         let title = self.get_title();
2530                         let app_id = self.get_app_id();
2531                         let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2532                             title,
2533                             app_id,
2534                         };
2535                         let handle = ffi::wlr_ext_foreign_toplevel_handle_v1_create(list, &state);
2536                         if !handle.is_null() {
2537                             self.foreign_toplevel_handle = handle;
2538                             (*handle).data = self as *mut Window as *mut _;
2539                         }
2540                     }
2541 
2542                     if self.wlr_toplevel_handle.is_null() {
2543                         let manager = (*self.server).wlr_foreign_toplevel_manager;
2544                         let handle = ffi::wlr_foreign_toplevel_handle_v1_create(manager);
2545                         if !handle.is_null() {
2546                             self.wlr_toplevel_handle = handle;
2547                             let title = self.get_title();
2548                             if !title.is_null() {
2549                                 ffi::wlr_foreign_toplevel_handle_v1_set_title(handle, title);
2550                             }
2551                             let app_id = self.get_app_id();
2552                             if !app_id.is_null() {
2553                                 ffi::wlr_foreign_toplevel_handle_v1_set_app_id(handle, app_id);
2554                             }
2555                         }
2556                     }
2557                 };
2558 
2559                 if new_resource {
2560                     let version = ffi::wl_resource_get_version(window_v1);
2561                     if version >= 2 {
2562                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_UNRELIABLE_PID, self.unreliable_pid()); // sendUnreliablePid
2563                     }
2564                     if version >= 4 {
2565                         if !self.foreign_toplevel_handle.is_null() {
2566                             let identifier = (*self.foreign_toplevel_handle).identifier;
2567                             ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_IDENTIFIER, identifier);
2568                         }
2569                     }
2570                 }
2571 
2572                 if new_resource || self.wm_scheduled.dimensions_hint != self.wm_sent.dimensions_hint {
2573                     ffi::wl_resource_post_event(
2574                         window_v1,
2575                         ffi::ZCCE_WINDOW_V1_DIMENSIONS_HINT, // sendDimensionsHint
2576                         self.wm_scheduled.dimensions_hint.min_width as i32,
2577                         self.wm_scheduled.dimensions_hint.min_height as i32,
2578                         self.wm_scheduled.dimensions_hint.max_width as i32,
2579                         self.wm_scheduled.dimensions_hint.max_height as i32,
2580                     );
2581                     self.wm_sent.dimensions_hint = self.wm_scheduled.dimensions_hint;
2582                 }
2583 
2584                 if new_resource || self.wm_scheduled.decoration_hint != self.wm_sent.decoration_hint {
2585                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_DECORATION_HINT, self.wm_scheduled.decoration_hint); // sendDecorationHint
2586                     self.wm_sent.decoration_hint = self.wm_scheduled.decoration_hint;
2587                 }
2588 
2589                 if let Some(ref offset) = self.wm_scheduled.show_window_menu_requested {
2590                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_SHOW_WINDOW_MENU_REQUESTED, offset.x, offset.y); // sendShowWindowMenuRequested
2591                     self.wm_scheduled.show_window_menu_requested = None;
2592                 }
2593 
2594                 match self.wm_scheduled.fullscreen_requested {
2595                     FullscreenRequest::NoRequest => {}
2596                     FullscreenRequest::Fullscreen(output) => {
2597                         let mut out_resource = if output.is_null() { std::ptr::null_mut() } else { (*output).object };
2598                         if !window_v1.is_null() && !out_resource.is_null() {
2599                             let client_win = ffi::wl_resource_get_client(window_v1);
2600                             let client_out = ffi::wl_resource_get_client(out_resource);
2601                             if client_win != client_out {
2602                                 log::error!(
2603                                     "Fullscreen output client mismatch: win_client={:?}, out_client={:?}. Fallback to null_mut",
2604                                     client_win,
2605                                     client_out
2606                                 );
2607                                 out_resource = std::ptr::null_mut();
2608                             }
2609                         }
2610                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_FULLSCREEN_REQUESTED, out_resource); // sendFullscreenRequested
2611                     }
2612                     FullscreenRequest::Exit => {
2613                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_EXIT_FULLSCREEN_REQUESTED); // sendExitFullscreenRequested
2614                     }
2615                 }
2616                 self.wm_scheduled.fullscreen_requested = FullscreenRequest::NoRequest;
2617 
2618                 match self.wm_scheduled.maximize_requested {
2619                     MaximizeRequest::NoRequest => {}
2620                     MaximizeRequest::Maximize => {
2621                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_MAXIMIZE_REQUESTED); // sendMaximizeRequested
2622                     }
2623                     MaximizeRequest::Unmaximize => {
2624                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_UNMAXIMIZE_REQUESTED); // sendUnmaximizeRequested
2625                     }
2626                 }
2627                 self.wm_scheduled.maximize_requested = MaximizeRequest::NoRequest;
2628 
2629                 if self.wm_scheduled.minimize_requested {
2630                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_MINIMIZE_REQUESTED); // sendMinimizeRequested
2631                 }
2632                 self.wm_scheduled.minimize_requested = false;
2633 
2634                 let parent = self.get_parent();
2635                 if !parent.is_null() {
2636                     let parent_ref = Some((*parent).ref_key);
2637                     if self.wm_sent.parent.is_none() || self.wm_sent.parent != parent_ref {
2638                         let parent_obj = (*parent).object;
2639                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_PARENT, parent_obj); // sendParent
2640                         self.wm_sent.parent = parent_ref;
2641                     }
2642                 } else if self.wm_sent.parent.is_some() {
2643                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_PARENT, std::ptr::null_mut::<ffi::wl_resource>()); // sendParent
2644                     self.wm_sent.parent = None;
2645                 }
2646 
2647                 if new_resource || self.wm_scheduled.dirty_app_id {
2648                     let app_id = self.get_app_id();
2649                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_APP_ID, app_id); // sendAppId
2650                     self.wm_scheduled.dirty_app_id = false;
2651                 }
2652 
2653                 if new_resource || self.wm_scheduled.dirty_title {
2654                     let title = self.get_title();
2655                     ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_TITLE, title); // sendTitle
2656                     self.wm_scheduled.dirty_title = false;
2657                 }
2658 
2659                 if let Some(seat) = self.wm_scheduled.pointer_move_requested.as_mut() {
2660                     if !seat.object.is_null() {
2661                         ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_POINTER_MOVE_REQUESTED, seat.object); // sendPointerMoveRequested
2662                     }
2663                 }
2664                 self.wm_scheduled.pointer_move_requested = std::ptr::null_mut();
2665 
2666                 if let Some(ref data) = self.wm_scheduled.pointer_resize_requested {
2667                     if let Some(seat) = unsafe { data.seat.as_ref() } {
2668                         if !seat.object.is_null() {
2669                             ffi::wl_resource_post_event(window_v1, ffi::ZCCE_WINDOW_V1_POINTER_RESIZE_REQUESTED, seat.object, data.edges); // sendPointerResizeRequested
2670                         }
2671                     }
2672                 }
2673                 self.wm_scheduled.pointer_resize_requested = None;
2674             }
2675         }
2676     }
2677 
2678     pub unsafe fn make_inert(&mut self) {
2679         if !self.object.is_null() {
2680             ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_CLOSED); // sendClosed // sendClosed
2681             ffi::wl_resource_set_implementation(
2682                 self.object,
2683                 &INERT_WINDOW_INTERFACE as *const _ as *const _,
2684                 std::ptr::null_mut(),
2685                 None,
2686             );
2687             self.object = std::ptr::null_mut();
2688             (*self.server).wm.dirty_windowing();
2689             self.node.make_inert();
2690 
2691             for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
2692                 let list_head = decorations as *mut WlList;
2693                 let mut curr = (*list_head).next;
2694                 while curr != list_head {
2695                     let next = (*curr).next;
2696                     let dec = crate::container_of!(curr, Decoration, link);
2697                     (*dec).make_inert();
2698                     curr = next;
2699                 }
2700             }
2701 
2702             let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
2703             let mut curr = (*seats).next;
2704             while curr != seats {
2705                 let next = (*curr).next;
2706                 let seat = crate::container_of!(curr, crate::seat::Seat, link);
2707                 if let crate::seat::Focus::Window(w) = (*seat).focused {
2708                     if w == self as *mut Window {
2709                         (*seat).focus(crate::seat::Focus::None);
2710                     }
2711                 }
2712                 curr = next;
2713             }
2714         }
2715     }
2716 
2717     pub unsafe fn manage_finish(&mut self) -> bool {
2718         if matches!(self.impl_type, WindowImpl::Destroying) {
2719             assert_eq!(self.state, WindowState::Closing);
2720             return false;
2721         }
2722 
2723         match self.state {
2724             WindowState::Init => unreachable!(),
2725             WindowState::Ready => {
2726                 if self.wm_requested.dimensions.is_none() && self.wm_requested.fullscreen.is_null() {
2727                     return false;
2728                 }
2729                 if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
2730                     log::debug!("[LinkDbg] manage_finish ready->initialized app={:?} linked={}",
2731                         self.get_app_id_string(), self.is_linked());
2732                 }
2733                 self.state = WindowState::Initialized;
2734             }
2735             WindowState::Initialized | WindowState::Mapped => {}
2736             WindowState::Closing => return false,
2737         }
2738 
2739         if self.wm_requested.close {
2740             self.close();
2741             self.wm_requested.close = false;
2742         }
2743 
2744         let mut activated = false;
2745         let seats = &mut (*self.server).wm.sent.seats as *mut ffi::wl_list as *mut WlList;
2746         let mut curr = (*seats).next;
2747         while curr != seats {
2748             let next = (*curr).next;
2749             let seat = crate::container_of!(curr, crate::seat::Seat, link_sent);
2750             if let crate::seat::Focus::Window(w) = (*seat).focused {
2751                 if w == self as *mut Window {
2752                     activated = true;
2753                     break;
2754                 }
2755             }
2756             curr = next;
2757         }
2758 
2759         if !self.wlr_toplevel_handle.is_null() {
2760             ffi::wlr_foreign_toplevel_handle_v1_set_activated(self.wlr_toplevel_handle, activated);
2761         }
2762 
2763         let output = if !self.wm_requested.fullscreen.is_null() {
2764             self.wm_requested.fullscreen
2765         } else if self.is_fullscreen() {
2766             let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
2767             let mut curr = (*outputs_list).next;
2768             let mut found_output = std::ptr::null_mut();
2769             while curr != outputs_list {
2770                 let out = crate::container_of!(curr, crate::output::Output, link);
2771                 if (*out).sent.state == crate::output::OutputStateValue::Enabled {
2772                     found_output = out;
2773                     break;
2774                 }
2775                 curr = (*curr).next;
2776             }
2777             found_output
2778         } else {
2779             std::ptr::null_mut()
2780         };
2781 
2782         let new_fullscreen = !output.is_null();
2783         if new_fullscreen && !self.was_fullscreen {
2784             if self.box_geom.width > 0 && self.box_geom.height > 0 {
2785                 self.start_fs_anim();
2786                 self.saved_width = self.box_geom.width;
2787                 self.saved_height = self.box_geom.height;
2788                 self.saved_virtual_x = self.virtual_x;
2789                 self.saved_virtual_y = self.virtual_y;
2790                 self.was_fullscreen = true;
2791                 log::info!("[Fullscreen] Saved window {:?} geometry: {}x{} at ({}, {})", self.get_title_string().as_deref().unwrap_or(""), self.saved_width, self.saved_height, self.saved_virtual_x, self.saved_virtual_y);
2792             }
2793         } else if !new_fullscreen && self.was_fullscreen {
2794             if self.saved_width > 0 && self.saved_height > 0 {
2795                 // Captures the on-screen fullscreen rect before the restore
2796                 // below rewrites box_geom.
2797                 self.start_fs_anim();
2798                 self.box_geom.width = self.saved_width;
2799                 self.box_geom.height = self.saved_height;
2800                 self.virtual_x = self.saved_virtual_x;
2801                 self.virtual_y = self.saved_virtual_y;
2802                 self.was_fullscreen = false;
2803 
2804                 self.wm_requested.dimensions = Some(crate::window::Dimensions {
2805                     width: self.saved_width as u32,
2806                     height: self.saved_height as u32,
2807                 });
2808                 self.wm_requested.bounds = crate::window::Dimensions {
2809                     width: self.saved_width as u32,
2810                     height: self.saved_height as u32,
2811                 };
2812 
2813                 (*self.server).wm.dirty_windowing();
2814                 log::info!("[Fullscreen] Restored window {:?} geometry: {}x{} at ({}, {})", self.get_title_string().as_deref().unwrap_or(""), self.saved_width, self.saved_height, self.saved_virtual_x, self.saved_virtual_y);
2815             }
2816         }
2817 
2818         let (width, height) = if !output.is_null() {
2819             let (w, h) = (*output).sent.dimensions();
2820             if self.configure_sent.width != Some(w as u32) || self.configure_sent.height != Some(h as u32) {
2821                 self.configure_scheduled.width = Some(w as u32);
2822                 self.configure_scheduled.height = Some(h as u32);
2823                 self.rendering_scheduled.resend_dimensions = true;
2824                 (Some(w as u32), Some(h as u32))
2825             } else {
2826                 (None, None)
2827             }
2828         } else if let Some(dimensions) = self.wm_requested.dimensions {
2829             self.rendering_scheduled.resend_dimensions = true;
2830             (Some(dimensions.width), Some(dimensions.height))
2831         } else {
2832             (None, None)
2833         };
2834         self.wm_requested.dimensions = None;
2835 
2836         // A tiled window thinks it is maximized: the xdg maximized state
2837         // follows the mode.
2838         let is_maximized_layout = self.tiling_mode == crate::tiling::TilingMode::Tiled;
2839         self.configure_scheduled = Configure {
2840             width,
2841             height,
2842             bounds: self.wm_requested.bounds,
2843             activated,
2844             ssd: self.wm_requested.ssd,
2845             tiled: self.wm_requested.tiled,
2846             capabilities: self.wm_requested.capabilities,
2847             maximized: self.wm_requested.maximized || is_maximized_layout,
2848             inform_fullscreen: self.wm_requested.inform_fullscreen || self.is_fullscreen(),
2849             resizing: self.wm_requested.resizing,
2850         };
2851 
2852         let track_configure = match self.impl_type {
2853             WindowImpl::Toplevel(toplevel) => {
2854                 if toplevel.is_null() {
2855                     false
2856                 } else {
2857                     (*toplevel).configure()
2858                 }
2859             }
2860             WindowImpl::Xwayland(xwindow) => {
2861                 if xwindow.is_null() {
2862                     false
2863                 } else {
2864                     (*xwindow).configure()
2865                 }
2866             }
2867             WindowImpl::Destroying => unreachable!(),
2868         };
2869 
2870         if track_configure && matches!(self.state, WindowState::Mapped) {
2871             self.surfaces.save();
2872             self.send_frame_done();
2873         }
2874 
2875         track_configure
2876     }
2877 
2878     pub unsafe fn render_start(&mut self) {
2879         match self.impl_type {
2880             WindowImpl::Toplevel(toplevel) => {
2881                 if !toplevel.is_null() {
2882                     match (*toplevel).configure_state {
2883                         ConfigureState::Inflight(serial) => {
2884                             (*toplevel).configure_state = ConfigureState::TimedOut(serial);
2885                         }
2886                         ConfigureState::Acked => {
2887                             (*toplevel).configure_state = ConfigureState::TimedOutAcked;
2888                         }
2889                         ConfigureState::Committed => {
2890                             (*toplevel).configure_state = ConfigureState::Idle;
2891                         }
2892                         _ => {}
2893                     }
2894                     // The client's committed geometry is the authority on
2895                     // this window's size — but only once the client has
2896                     // ANSWERED a configure. Before its first ack, `geometry`
2897                     // holds the size the client asked for on its own:
2898                     // Chromium restores its remembered bounds with
2899                     // `set_window_geometry` before it ever acks, and those
2900                     // bounds are its window PLUS its CSD shadow insets, so
2901                     // they always overhang the cell block the restore just
2902                     // gave it. Adopting that wish made it `box_geom` (see
2903                     // `render_finish`), the next Tiled arrange covered every
2904                     // cell the overhang touched (`snap::tiled_span` floors the
2905                     // low edge and CEILS the high one), the grown size was
2906                     // saved, and Chrome came back a whole cell wider and
2907                     // taller on every login — a one-way ratchet, since each
2908                     // session's insets sit on top of the last session's block.
2909                     // A window with no restored size still seeds its block
2910                     // from the client's first wish, which is where a freshly
2911                     // launched app's size comes from.
2912                     if !self.restored || (*toplevel).acked_once {
2913                         self.rendering_scheduled.width = (*toplevel).geometry.width as u32;
2914                         self.rendering_scheduled.height = (*toplevel).geometry.height as u32;
2915                     }
2916                 }
2917             }
2918             WindowImpl::Xwayland(xwindow) => {
2919                 if !xwindow.is_null() {
2920                     let s = crate::xwayland_window::x11_scale_for(self.server, (*xwindow).xsurface);
2921                     let mut w = crate::xwayland_window::from_x11((*(*xwindow).xsurface).width as i32, s) as u32;
2922                     let mut h = crate::xwayland_window::from_x11((*(*xwindow).xsurface).height as i32, s) as u32;
2923                     let has_parent = !(*(*xwindow).xsurface).parent.is_null();
2924                     if self.is_wine() && !has_parent && !self.is_fullscreen() {
2925                         w = w.saturating_sub((crate::xwayland_window::WINE_MARGIN * 2) as u32);
2926                         h = h.saturating_sub((crate::xwayland_window::WINE_MARGIN * 2) as u32);
2927                     }
2928                     self.rendering_scheduled.width = w;
2929                     self.rendering_scheduled.height = h;
2930                 }
2931             }
2932             WindowImpl::Destroying => {}
2933         }
2934 
2935         let presentation_hint = self.presentation_hint();
2936         let sent = &mut self.rendering_sent;
2937         let scheduled = &mut self.rendering_scheduled;
2938 
2939         if matches!(self.state, WindowState::Mapped) &&
2940            (scheduled.resend_dimensions ||
2941             scheduled.width != sent.width || scheduled.height != sent.height) {
2942             if !self.object.is_null() {
2943                 ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_DIMENSIONS, scheduled.width as i32, scheduled.height as i32); // sendDimensions
2944                 scheduled.resend_dimensions = false;
2945             }
2946         }
2947         sent.width = scheduled.width;
2948         sent.height = scheduled.height;
2949         if sent.presentation_hint != presentation_hint {
2950             if !self.object.is_null() {
2951                 let version = ffi::wl_resource_get_version(self.object);
2952                 if version >= 4 {
2953                     ffi::wl_resource_post_event(self.object, ffi::ZCCE_WINDOW_V1_PRESENTATION_HINT, presentation_hint); // sendPresentationHint
2954                 }
2955             }
2956             sent.presentation_hint = presentation_hint;
2957         }
2958     }
2959 
2960     pub unsafe fn presentation_hint(&self) -> ffi::zcce_output_v1_presentation_mode {
2961         let root = self.root_surface();
2962         if root.is_null() {
2963             return ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC;
2964         }
2965         
2966         // tearing control check stub:
2967         // switch (server.tearing_control_manager.hintFromSurface(root)) {
2968         //     .async => .async,
2969         //     .vsync => .vsync,
2970         // }
2971         // For now, return VSYNC by default.
2972         ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC
2973     }
2974 
2975     pub unsafe fn notify_title(&mut self) {
2976         self.wm_scheduled.dirty_title = true;
2977         self.try_restore();
2978         // A title is arrangement input only through a mode rule that matches
2979         // on it (`title=` in a rule); the built-in policy is what runs — no
2980         // external manager is ever bound to `wm.object` (see the bind
2981         // handler) — so nothing else in the manage sequence reads it. A
2982         // terminal running a busy program retitles several times a second,
2983         // and each retitle used to cost a full manage/arrange/render pass.
2984         // Without a title rule the title's other consumers are the status
2985         // bar's `title` topic and the saved-state file, so feed those directly.
2986         let wm = &mut (*self.server).wm;
2987         if wm.mode_rules.iter().any(|r| r.title_pattern.is_some()) {
2988             wm.dirty_windowing();
2989         } else {
2990             wm.update_status();
2991             wm.schedule_save_state();
2992         }
2993 
2994         if !self.foreign_toplevel_handle.is_null() {
2995             let title = self.get_title();
2996             let app_id = self.get_app_id();
2997             let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
2998                 title,
2999                 app_id,
3000             };
3001             ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
3002         }
3003 
3004         if !self.wlr_toplevel_handle.is_null() {
3005             let title = self.get_title();
3006             if !title.is_null() {
3007                 ffi::wlr_foreign_toplevel_handle_v1_set_title(self.wlr_toplevel_handle, title);
3008             }
3009         }
3010     }
3011 
3012     pub unsafe fn notify_app_id(&mut self) {
3013         self.wm_scheduled.dirty_app_id = true;
3014         let app_id_str = self.get_app_id_string();
3015         if app_id_str.as_deref().map_or(false, |id| id.starts_with("cce-status") || id == "cce-wallpaper") {
3016             self.tiling_mode = crate::tiling::TilingMode::Status;
3017         }
3018         self.try_restore();
3019         (*self.server).wm.dirty_windowing();
3020 
3021         if !self.foreign_toplevel_handle.is_null() {
3022             let title = self.get_title();
3023             let app_id = self.get_app_id();
3024             let state = ffi::wlr_ext_foreign_toplevel_handle_v1_state {
3025                 title,
3026                 app_id,
3027             };
3028             ffi::wlr_ext_foreign_toplevel_handle_v1_update_state(self.foreign_toplevel_handle, &state);
3029         }
3030 
3031         if !self.wlr_toplevel_handle.is_null() {
3032             let app_id = self.get_app_id();
3033             if !app_id.is_null() {
3034                 ffi::wlr_foreign_toplevel_handle_v1_set_app_id(self.wlr_toplevel_handle, app_id);
3035             }
3036         }
3037     }
3038 
3039     pub unsafe fn render_finish(&mut self) {
3040         let requested = &self.rendering_requested;
3041         let enabled = !requested.hidden && (matches!(self.state, WindowState::Mapped) || matches!(self.state, WindowState::Closing));
3042 
3043         let title_ptr = match self.impl_type {
3044             WindowImpl::Xwayland(xwindow) => {
3045                 if xwindow.is_null() { std::ptr::null() } else { (*(*xwindow).xsurface).title }
3046             }
3047             _ => std::ptr::null(),
3048         };
3049         let title = if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") };
3050         if title.contains("Ubisoft") {
3051             log::info!("render_finish for '{}' (addr={:p}): enabled={} hidden={} state={:?}", title, self as *const Window, enabled, requested.hidden, self.state);
3052         }
3053 
3054         ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, enabled);
3055         ffi::wlr_scene_node_set_enabled(self.popup_tree as *mut ffi::wlr_scene_node, enabled);
3056         if !enabled {
3057             // The segment tree is not a child of `tree`, so disabling the
3058             // window does not hide a revealed border with it.
3059             self.border_reveal = [0.0; 8];
3060             ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3061         }
3062 
3063         if enabled {
3064             let app_id = self.get_app_id_string().unwrap_or_default();
3065             let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
3066                             app_id.starts_with("cce-status");
3067             let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3068             let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
3069             let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
3070             if is_status {
3071                 ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
3072             }
3073             // Hoisted above the blur setup: the blur node needs this radius, and whether
3074             // the window wants rounded corners at all decides the optimized-blur question
3075             // below.
3076             let radius = if self.is_fullscreen() {
3077                 0
3078             } else if requested.circular {
3079                 let w = self.rendering_sent.width as i32;
3080                 let h = self.rendering_sent.height as i32;
3081                 w.min(h) / 2
3082             } else if is_status {
3083                 // Status segments draw their own module-box corners. The
3084                 // root plate clip is invisible on a bar-thin segment (the
3085                 // half-extent cap keeps it inside the transparent band) but
3086                 // carves visible sweeps into an EXPANDED segment's in-surface
3087                 // menu box once the cap stops binding.
3088                 0
3089             } else if self.wm_requested.ssd || is_decorated {
3090                 (*self.server).wm.layout.root_plate_corner_radius
3091             } else {
3092                 0
3093             };
3094             // Rounded corners do NOT require live blur: the corner shape is applied by the
3095             // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
3096             // the optimized node only re-bakes the shared offscreen cache
3097             // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
3098             // screen. The old `radius > 0` opt-out silently disabled the optimization for
3099             // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
3100             // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
3101             let use_optimized = if is_status {
3102                 false
3103             } else {
3104                 (*self.server).wm.layout.scenefx_optimized_blur
3105             };
3106             let toplevel_w = match self.impl_type {
3107                 WindowImpl::Toplevel(toplevel) => {
3108                     if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
3109                 }
3110                 _ => 0,
3111             };
3112             let toplevel_h = match self.impl_type {
3113                 WindowImpl::Toplevel(toplevel) => {
3114                     if toplevel.is_null() { 0 } else { (*toplevel).geometry.height }
3115                 }
3116                 _ => 0,
3117             };
3118             // Status segments are self-sizing: their committed geometry is
3119             // fresher than the render-start snapshot (`rendering_sent`),
3120             // which lags an expand/contract commit by a render pass — same
3121             // rule as the commit-path blur sizing in xdg_toplevel.rs.
3122             let (actual_w, actual_h) = if is_status && toplevel_w > 0 && toplevel_h > 0 {
3123                 (toplevel_w as u32, toplevel_h as u32)
3124             } else {
3125                 (
3126                     if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 },
3127                     if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 },
3128                 )
3129             };
3130             // Widen squircle corners to the span the clients draw (see
3131             // widen_corner_radius); circles already sit at the half-extent cap.
3132             let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
3133             // Mid fullscreen-toggle the window draws at the animated rect:
3134             // buffers stretch per-axis toward it (aspect changes in flight,
3135             // so the axes diverge) and the effect extents follow.
3136             let (scale_x, scale_y) = match self.fs_anim {
3137                 Some(anim) if actual_w > 0 && actual_h > 0 => {
3138                     (anim.w / actual_w as f64, anim.h / actual_h as f64)
3139                 }
3140                 _ => (self.scale, self.scale),
3141             };
3142             let width = (actual_w as f64 * scale_x).round() as i32;
3143             let height = (actual_h as f64 * scale_y).round() as i32;
3144             ffi::river_scene_node_enable_blur(
3145                 self.tree as *mut ffi::wlr_scene_node,
3146                 blur_enabled,
3147                 use_optimized,
3148                 ignore_transparent,
3149                 0,
3150                 0,
3151                 width,
3152                 height,
3153                 // width/height above are scaled to device pixels, so the radius must be too
3154                 // (cf. the window_background rect, which scales it the same way).
3155                 (radius as f64 * self.scale) as i32,
3156             );
3157             // The decorated-window predicate feeds two things: the shadow, and
3158             // the bevel's focus glint. Only the shadow honours the tiled switch.
3159             let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
3160             let want_shadow = want_decor && self.wants_tiled_shadow();
3161                 // The bevel keys on its OWN app list, not on is_decorated:
3162                 // every cce-ui app draws its own bevel, so a compositor one
3163                 // would sit on top of it.
3164                 let want_bevel = !is_status
3165                     && !self.is_fullscreen()
3166                     && (*self.server).wm.is_beveled_app(&app_id);
3167             self.update_shadow(width, height, radius, want_shadow);
3168                 self.update_bevel(width, height, radius, want_bevel, want_decor);
3169                 self.update_droplet(width, height);
3170             ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
3171 
3172             // Device px, like the blur radius above: the surface content is
3173             // scaled to its dest size, so an unscaled clip radius would keep
3174             // cutting zoom-1-sized corners into a zoomed-down window (the
3175             // clients' own drawn corners shrink with the buffer).
3176             ffi::river_scene_node_set_corner_radius(
3177                 self.surfaces.tree as *mut ffi::wlr_scene_node,
3178                 (radius as f64 * self.scale) as i32,
3179             );
3180             ffi::river_scene_rect_set_corner_radius(
3181                 self.window_background,
3182                 (radius as f64 * self.scale) as i32,
3183             );
3184 
3185             struct ScaleData {
3186                 scale_x: f64,
3187                 scale_y: f64,
3188                 ancestor: *mut ffi::wlr_scene_node,
3189             }
3190 
3191             unsafe extern "C" fn set_overview_scale_iterator(
3192                 buffer: *mut ffi::wlr_scene_buffer,
3193                 sx: i32,
3194                 sy: i32,
3195                 user_data: *mut std::ffi::c_void,
3196             ) {
3197                 let data = &*(user_data as *const ScaleData);
3198                 let node = buffer as *mut ffi::wlr_scene_node;
3199 
3200                 let surface = ffi::river_scene_node_get_surface(node);
3201                 if !surface.is_null() {
3202                     let (w, h, ox, oy) = surface_buffer_extent(buffer, surface);
3203                     if data.scale_x == 1.0 && data.scale_y == 1.0 {
3204                         ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
3205                         ffi::river_scene_node_set_position_if_changed(node, ox, oy);
3206                     } else {
3207                         let dest_w = (w as f64 * data.scale_x).round() as i32;
3208                         let dest_h = (h as f64 * data.scale_y).round() as i32;
3209                         ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
3210 
3211                         // The parent offset scales like the content; the
3212                         // clip origin rides on top of it, scaled the same.
3213                         let (px, py) = get_parent_position_relative_to(node, data.ancestor);
3214                         let dest_x = (px as f64 * (data.scale_x - 1.0) + ox as f64 * data.scale_x).round() as i32;
3215                         let dest_y = (py as f64 * (data.scale_y - 1.0) + oy as f64 * data.scale_y).round() as i32;
3216                         ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
3217                     }
3218                     // Keep the opaque region in step with the dest scale —
3219                     // unscaled it covers the shrunken node's translucent CSD
3220                     // margins and occlusion culling stops repainting behind
3221                     // the client shadow (stale pixels show through it). The
3222                     // region must never overclaim, so a briefly non-uniform
3223                     // stretch takes the smaller axis.
3224                     ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale_x.min(data.scale_y));
3225                 }
3226                 // Non-surface buffers are frozen SAVED copies (see
3227                 // save_surface_tree_iter): their natural buffer size is
3228                 // meaningless for geometry — HiDPI clients commit scale-N
3229                 // buffers and Chromium pads buffers beyond the surface,
3230                 // cropping via viewport src — so rescaling from it ballooned
3231                 // ghosts around the window at any zoom change. A frozen copy
3232                 // keeps its save-time dest/position; a zoom mid-transaction
3233                 // leaves it briefly at the old zoom, which restore corrects.
3234             }
3235 
3236             let scale_data_surfaces = ScaleData { scale_x, scale_y, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
3237             ffi::wlr_scene_node_for_each_buffer(
3238                 self.surfaces.tree as *mut ffi::wlr_scene_node,
3239                 Some(set_overview_scale_iterator),
3240                 &scale_data_surfaces as *const ScaleData as *mut std::ffi::c_void,
3241             );
3242 
3243             if self.surfaces.saved {
3244                 let scale_data_saved = ScaleData { scale_x, scale_y, ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
3245                 ffi::wlr_scene_node_for_each_buffer(
3246                     self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
3247                     Some(set_overview_scale_iterator),
3248                     &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
3249                 );
3250             }
3251             
3252             let scale_data_popup = ScaleData { scale_x, scale_y, ancestor: self.popup_tree as *mut ffi::wlr_scene_node };
3253             ffi::wlr_scene_node_for_each_buffer(
3254                 self.popup_tree as *mut ffi::wlr_scene_node,
3255                 Some(set_overview_scale_iterator),
3256                 &scale_data_popup as *const ScaleData as *mut std::ffi::c_void,
3257             );
3258             self.last_applied_scale = self.scale;
3259         }
3260 
3261         // During an interactive resize, size the box from the client's
3262         // CURRENT committed geometry instead of the render-start snapshot
3263         // (rendering_sent): commits land between render_start and
3264         // render_finish, and the anchored position (rendering_requested.x,
3265         // updated by the commit handler) always tracks the newest commit.
3266         // Pairing it with the older snapshot size clips the surface short
3267         // and makes the anchored edge bounce every cycle.
3268         // self_resized: same reasoning, for a client that resized itself without a
3269         // configure — its newest buffer is already on screen, so rendering_sent is
3270         // behind and would drag the border back to the previous size.
3271         let mut resize_synced = false;
3272         if self.resize_edges.is_some() || self.self_resized {
3273             if let WindowImpl::Toplevel(toplevel) = self.impl_type {
3274                 if !toplevel.is_null() {
3275                     self.box_geom.width = (*toplevel).geometry.width;
3276                     self.box_geom.height = (*toplevel).geometry.height;
3277                     resize_synced = true;
3278                 }
3279             }
3280         }
3281         if !resize_synced {
3282             if self.rendering_sent.width > 0 {
3283                 self.box_geom.width = self.rendering_sent.width as i32;
3284             }
3285             if self.rendering_sent.height > 0 {
3286                 self.box_geom.height = self.rendering_sent.height as i32;
3287             }
3288         }
3289         self.self_resized = false;
3290 
3291         let mut clip = requested.clip;
3292         let mut content_clip = requested.content_clip;
3293 
3294         let output = if !self.wm_requested.fullscreen.is_null() {
3295             self.wm_requested.fullscreen
3296         } else if self.is_fullscreen() {
3297             let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
3298             let mut curr = (*outputs_list).next;
3299             let mut found_output = std::ptr::null_mut();
3300             while curr != outputs_list {
3301                 let out = crate::container_of!(curr, crate::output::Output, link);
3302                 if (*out).sent.state == crate::output::OutputStateValue::Enabled {
3303                     found_output = out;
3304                     break;
3305                 }
3306                 curr = (*curr).next;
3307             }
3308             found_output
3309         } else {
3310             std::ptr::null_mut()
3311         };
3312 
3313         if !output.is_null() {
3314             self.box_geom.x = (*output).sent.x;
3315             self.box_geom.y = (*output).sent.y;
3316 
3317             let app_id_ptr = self.get_app_id();
3318             let (is_status_bar, is_wallpaper) = if !app_id_ptr.is_null() {
3319                 let app_id = std::ffi::CStr::from_ptr(app_id_ptr).to_string_lossy();
3320                 (app_id.starts_with("cce-status"), app_id.as_ref() == "cce-wallpaper")
3321             } else {
3322                 (false, false)
3323             };
3324 
3325             ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, !is_status_bar && !is_wallpaper);
3326             let (width, height) = (*output).sent.dimensions();
3327             ffi::wlr_scene_rect_set_size(self.fullscreen_background, width as i32, height as i32);
3328             clip = ffi::wlr_box { x: 0, y: 0, width: width as i32, height: height as i32 };
3329             content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
3330 
3331             ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
3332             ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
3333             ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
3334             ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
3335             ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, false);
3336             // Fullscreen skips draw_borders entirely, and the segment tree
3337             // lives outside this window's tree, so it has to be taken down
3338             // explicitly or a revealed edge would hang over the fullscreen
3339             // surface.
3340             self.border_reveal = [0.0; 8];
3341             ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3342         } else {
3343             self.box_geom.x = requested.x;
3344             self.box_geom.y = requested.y;
3345             ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, false);
3346             if self.fs_anim.is_none() {
3347                 self.draw_borders();
3348             }
3349         }
3350 
3351         ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3352         ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3353 
3354         // Mid fullscreen-toggle: draw at the animated rect regardless of which
3355         // branch above ran. The tree overrides its settled position, the black
3356         // backdrop rides the rect (it is what grows/shrinks visually on both
3357         // directions), the clip follows, and the borders stay down until the
3358         // animation settles — the final settling frame re-runs the branch
3359         // above with fs_anim cleared and puts everything back.
3360         if let Some(anim) = self.fs_anim {
3361             let ax = anim.x.round() as i32;
3362             let ay = anim.y.round() as i32;
3363             let aw = (anim.w.round() as i32).max(1);
3364             let ah = (anim.h.round() as i32).max(1);
3365             ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, ax, ay);
3366             ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, ax, ay);
3367             ffi::wlr_scene_node_set_enabled(self.fullscreen_background as *mut ffi::wlr_scene_node, true);
3368             ffi::wlr_scene_rect_set_size(self.fullscreen_background, aw, ah);
3369             clip = ffi::wlr_box { x: 0, y: 0, width: aw, height: ah };
3370             content_clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
3371             ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
3372             ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
3373             ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
3374             ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
3375             ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, false);
3376             self.border_reveal = [0.0; 8];
3377             ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3378         }
3379 
3380         // No geometry compensation here: wlr_scene_xdg_surface_create already
3381         // anchors its subtree at the top-left of the xdg window geometry (it
3382         // re-offsets by -geometry on every commit), so subtracting geometry.x/y
3383         // again shifted CSD windows with shadow margins (Electron/Chromium
3384         // floating) up-left by their shadow size, off the desktop grid.
3385         ffi::river_scene_node_set_position_if_changed(self.surfaces.tree as *mut ffi::wlr_scene_node, 0, 0);
3386 
3387         self.apply_surface_clip(&clip, &content_clip);
3388 
3389         for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
3390             let list_head = decorations as *mut WlList;
3391             let mut curr = (*list_head).next;
3392             while curr != list_head {
3393                 let next = (*curr).next;
3394                 let dec = crate::container_of!(curr, Decoration, link);
3395                 (*dec).render_finish(&clip);
3396                 curr = next;
3397             }
3398         }
3399 
3400         match self.impl_type {
3401             WindowImpl::Xwayland(xwindow) => {
3402                 if !xwindow.is_null() {
3403                     if !(*xwindow).surface_tree.is_null() {
3404                         let has_parent = !(*(*xwindow).xsurface).parent.is_null();
3405                         if self.is_wine() && !has_parent && !self.is_fullscreen() {
3406                             ffi::wlr_scene_node_set_position((*xwindow).surface_tree as *mut ffi::wlr_scene_node, -16, -16);
3407                         } else {
3408                             ffi::wlr_scene_node_set_position((*xwindow).surface_tree as *mut ffi::wlr_scene_node, 0, 0);
3409                         }
3410                     }
3411                     (*xwindow).configure();
3412                 }
3413             }
3414             _ => {}
3415         }
3416     }
3417 
3418     pub unsafe fn scale_only_render_finish(&mut self) {
3419         // Mid fullscreen-toggle the animation tick owns the buffer dest
3420         // sizes; a uniform-scale pass here would stomp the stretch.
3421         if self.fs_anim.is_some() {
3422             return;
3423         }
3424         // The zoom the overview asks for, times 1/output-scale for an X11
3425         // surface whose buffer is physical pixels (`x11_buffer_scale`).
3426         let eff_scale = self.scale * self.x11_buffer_scale();
3427         if eff_scale == 1.0 {
3428             self.last_applied_scale = 1.0;
3429             return;
3430         }
3431 
3432         // No last_applied_scale short-circuit here: wlroots' scene-surface
3433         // commit listener resets a committed buffer's dest size and opaque
3434         // region to the surface's natural extent, so any client repainting
3435         // while scaled (browser animations, caret blink) pops back to full
3436         // size even though the cached scale says nothing changed. This runs
3437         // per rendered frame (output.rs render_and_commit), after commits and
3438         // before build_state, and every setter below is change-checked — an
3439         // already-correct tree produces no damage.
3440         self.last_applied_scale = self.scale;
3441 
3442         struct ScaleData {
3443             scale: f64,
3444             ancestor: *mut ffi::wlr_scene_node,
3445         }
3446 
3447         unsafe extern "C" fn set_overview_scale_iterator(
3448             buffer: *mut ffi::wlr_scene_buffer,
3449             sx: i32,
3450             sy: i32,
3451             user_data: *mut std::ffi::c_void,
3452         ) {
3453             let data = &*(user_data as *const ScaleData);
3454             let node = buffer as *mut ffi::wlr_scene_node;
3455 
3456             let surface = ffi::river_scene_node_get_surface(node);
3457             if !surface.is_null() {
3458                 let (w, h, ox, oy) = surface_buffer_extent(buffer, surface);
3459                 if data.scale == 1.0 {
3460                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
3461                     ffi::river_scene_node_set_position_if_changed(node, ox, oy);
3462                 } else {
3463                     let dest_w = (w as f64 * data.scale).round() as i32;
3464                     let dest_h = (h as f64 * data.scale).round() as i32;
3465                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
3466 
3467                     let (px, py) = get_parent_position_relative_to(node, data.ancestor);
3468                     let dest_x = (px as f64 * (data.scale - 1.0) + ox as f64 * data.scale).round() as i32;
3469                     let dest_y = (py as f64 * (data.scale - 1.0) + oy as f64 * data.scale).round() as i32;
3470                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
3471                 }
3472                 // Keep the opaque region in step with the dest scale —
3473                 // unscaled it covers the shrunken node's translucent CSD
3474                 // margins and occlusion culling stops repainting behind
3475                 // the client shadow (stale pixels show through it).
3476                 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
3477             }
3478             // Non-surface buffers are frozen SAVED copies (see
3479             // save_surface_tree_iter): their natural buffer size is
3480             // meaningless for geometry — HiDPI clients commit scale-N
3481             // buffers and Chromium pads buffers beyond the surface,
3482             // cropping via viewport src — so rescaling from it ballooned
3483             // ghosts around the window at any zoom change. A frozen copy
3484             // keeps its save-time dest/position; a zoom mid-transaction
3485             // leaves it briefly at the old zoom, which restore corrects.
3486         }
3487 
3488         let scale_data_surfaces = ScaleData { scale: eff_scale, ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
3489         ffi::wlr_scene_node_for_each_buffer(
3490             self.surfaces.tree as *mut ffi::wlr_scene_node,
3491             Some(set_overview_scale_iterator),
3492             &scale_data_surfaces as *const ScaleData as *mut std::ffi::c_void,
3493         );
3494 
3495         if self.surfaces.saved {
3496             let scale_data_saved = ScaleData { scale: eff_scale, ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
3497             ffi::wlr_scene_node_for_each_buffer(
3498                 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
3499                 Some(set_overview_scale_iterator),
3500                 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
3501             );
3502         }
3503 
3504         let scale_data_popup = ScaleData { scale: eff_scale, ancestor: self.popup_tree as *mut ffi::wlr_scene_node };
3505         ffi::wlr_scene_node_for_each_buffer(
3506             self.popup_tree as *mut ffi::wlr_scene_node,
3507             Some(set_overview_scale_iterator),
3508             &scale_data_popup as *const ScaleData as *mut std::ffi::c_void,
3509         );
3510 
3511         for decorations in [&mut self.decorations_above as *mut ffi::wl_list, &mut self.decorations_below as *mut ffi::wl_list] {
3512             let list_head = decorations as *mut WlList;
3513             let mut curr = (*list_head).next;
3514             while curr != list_head {
3515                 let next = (*curr).next;
3516                 let dec = crate::container_of!(curr, Decoration, link);
3517                 (*dec).scale_only_render_finish();
3518                 curr = next;
3519             }
3520         }
3521     }
3522 
3523     pub unsafe fn render_viewport_update(&mut self) {
3524         let requested = &self.rendering_requested;
3525         let enabled = !requested.hidden && (matches!(self.state, WindowState::Mapped) || matches!(self.state, WindowState::Closing));
3526 
3527         ffi::wlr_scene_node_set_enabled(self.tree as *mut ffi::wlr_scene_node, enabled);
3528         ffi::wlr_scene_node_set_enabled(self.popup_tree as *mut ffi::wlr_scene_node, enabled);
3529         if !enabled {
3530             self.border_reveal = [0.0; 8];
3531             ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, false);
3532         }
3533 
3534         if enabled {
3535             self.box_geom.x = requested.x;
3536             self.box_geom.y = requested.y;
3537             ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3538             ffi::river_scene_node_set_position_if_changed(self.popup_tree as *mut ffi::wlr_scene_node, self.box_geom.x, self.box_geom.y);
3539 
3540             // Blur stays on through a pan for every window. Non-cce windows
3541             // used to have their blur nodes DESTROYED on the first motion
3542             // frame and rebuilt 120ms after the gesture — a visible pop at
3543             // the end of every pan — on the theory that per-frame blur was
3544             // too expensive to keep during motion. Since the scene freezes
3545             // its optimized-blur caches for the duration of the motion
3546             // (river_scene_set_blur_frozen), a blurred window costs one
3547             // cached-texture sample per frame while moving, so the same
3548             // treatment cce apps always had now applies to all.
3549             // The geometry below is computed for EVERY window regardless: the drop
3550             // shadow has to track the zoom even where live blur does not (see the
3551             // update_shadow call at the end of the block).
3552             let app_id = self.get_app_id_string().unwrap_or_default();
3553             {
3554                 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
3555                                 app_id.starts_with("cce-status");
3556                 let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3557                 let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
3558                 let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
3559                 if is_status {
3560                     ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
3561                 }
3562                 // Same radius/optimized reasoning as set_rendering_state. Before, this path
3563                 // set no radius at all, so a blur node recreated during a pan came back
3564                 // square and stayed that way.
3565                 let radius = if self.is_fullscreen() {
3566                     0
3567                 } else if requested.circular {
3568                     let w = self.rendering_sent.width as i32;
3569                     let h = self.rendering_sent.height as i32;
3570                     w.min(h) / 2
3571                 } else if is_status {
3572                     // Same status exemption as set_rendering_state — the two
3573                     // paths drive the same nodes and must agree.
3574                     0
3575                 } else if self.wm_requested.ssd || is_decorated {
3576                     (*self.server).wm.layout.root_plate_corner_radius
3577                 } else {
3578                     0
3579                 };
3580                 // Rounded corners do NOT require live blur: the corner shape is applied by the
3581                 // standard blur node's sampler (wlr_scene_blur_set_corner_radius) in both modes;
3582                 // the optimized node only re-bakes the shared offscreen cache
3583                 // (fx_render_pass_add_optimized_blur -> read_to_buffer) and never paints on
3584                 // screen. The old `radius > 0` opt-out silently disabled the optimization for
3585                 // every (rounded) window, forcing full-backdrop dual-kawase blur per frame per
3586                 // translucent window — the DE-wide hover-lag / constant-GPU-load root cause.
3587                 let use_optimized = if is_status {
3588                     false
3589                 } else {
3590                     (*self.server).wm.layout.scenefx_optimized_blur
3591                 };
3592                 let toplevel_w = match self.impl_type {
3593                     WindowImpl::Toplevel(toplevel) => {
3594                         if toplevel.is_null() { 0 } else { (*toplevel).geometry.width }
3595                     }
3596                     _ => 0,
3597                 };
3598                 let toplevel_h = match self.impl_type {
3599                     WindowImpl::Toplevel(toplevel) => {
3600                         if toplevel.is_null() { 0 } else { (*toplevel).geometry.height }
3601                     }
3602                     _ => 0,
3603                 };
3604                 // Same self-sizing rule as set_rendering_state above.
3605                 let (actual_w, actual_h) = if is_status && toplevel_w > 0 && toplevel_h > 0 {
3606                     (toplevel_w as u32, toplevel_h as u32)
3607                 } else {
3608                     (
3609                         if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 },
3610                         if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 },
3611                     )
3612                 };
3613                 // Same span widening as set_rendering_state — the two paths
3614                 // drive the same blur node and must agree.
3615                 let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
3616                 let width = (actual_w as f64 * self.scale) as i32;
3617                 let height = (actual_h as f64 * self.scale) as i32;
3618                 ffi::river_scene_node_enable_blur(
3619                     self.tree as *mut ffi::wlr_scene_node,
3620                     blur_enabled,
3621                     use_optimized,
3622                     ignore_transparent,
3623                     0,
3624                     0,
3625                     width,
3626                     height,
3627                     (radius as f64 * self.scale) as i32,
3628                 );
3629                 // Every window, blurred or not: the shadow's size, blur sigma,
3630                 // offset and — critically — the clipped region that punches the
3631                 // window out of it are all scale-dependent, and nothing else on
3632                 // the motion path touches them. Left stale they keep the scale
3633                 // from before the gesture, so the punch-out overruns the shrunken
3634                 // window and swallows the shadow whole.
3635                 // The decorated-window predicate feeds two things: the shadow, and
3636                 // the bevel's focus glint. Only the shadow honours the tiled switch.
3637                 let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
3638                 let want_shadow = want_decor && self.wants_tiled_shadow();
3639                 // The bevel keys on its OWN app list, not on is_decorated:
3640                 // every cce-ui app draws its own bevel, so a compositor one
3641                 // would sit on top of it.
3642                 let want_bevel = !is_status
3643                     && !self.is_fullscreen()
3644                     && (*self.server).wm.is_beveled_app(&app_id);
3645                 self.update_shadow(width, height, radius, want_shadow);
3646                 self.update_bevel(width, height, radius, want_bevel, want_decor);
3647                 self.update_droplet(width, height);
3648             }
3649 
3650             self.scale_only_render_finish();
3651             self.draw_borders();
3652         }
3653     }
3654 
3655     /// The root plate / content-clip corner radius in logical px, before span
3656     /// widening. Single source for every writer of that radius: the two render
3657     /// paths clip the surface with it, and `draw_borders` shapes the root plate
3658     /// rect with it. Those disagreed — draw_borders applied the BORDER ring's
3659     /// radius to the root plate node and, running last, silently overrode the
3660     /// value set_rendering_state had just written, making
3661     /// `root_plate_corner_radius` dead config.
3662     pub unsafe fn root_plate_radius_base(&self) -> i32 {
3663         if self.is_fullscreen() {
3664             return 0;
3665         }
3666         if self.rendering_requested.circular {
3667             let w = self.rendering_sent.width as i32;
3668             let h = self.rendering_sent.height as i32;
3669             return w.min(h) / 2;
3670         }
3671         let app_id = self.get_app_id_string().unwrap_or_default();
3672         let is_status = self.tiling_mode == crate::tiling::TilingMode::Status
3673             || app_id.starts_with("cce-status");
3674         if is_status {
3675             return 0;
3676         }
3677         let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
3678         if self.wm_requested.ssd || is_decorated {
3679             (*self.server).wm.layout.root_plate_corner_radius
3680         } else {
3681             0
3682         }
3683     }
3684 
3685     /// Sync the drop shadow with the current geometry. `width`/`height` are the
3686     /// content size in device px, `radius` the corner radius in logical px (as
3687     /// computed for the blur/rounding paths). scenefx's box-shadow shader draws
3688     /// the shadow of a box inset by sigma on all sides of the node box, so the
3689     /// node is padded by sigma and offset so the casting box lands exactly on
3690     /// the window, displaced by the configured offset — which should point away
3691     /// from the light (down-right for the DE's default top-left light). The
3692     /// window's own box is punched out via the clipped region so the shadow
3693     /// darkens only the desktop around the window, never the (translucent)
3694     /// window itself.
3695     pub unsafe fn update_shadow(&self, width: i32, height: i32, radius: i32, want: bool) {
3696         if self.shadow.is_null() {
3697             return;
3698         }
3699         let node = &mut (*self.shadow).node as *mut ffi::wlr_scene_node;
3700         let layout = &(*self.server).wm.layout;
3701         let enabled = want && layout.shadow_enabled && width > 0 && height > 0;
3702         ffi::wlr_scene_node_set_enabled(node, enabled);
3703         if !enabled {
3704             return;
3705         }
3706         let sigma = (layout.shadow_sigma as f64 * self.scale) as f32;
3707         let pad = sigma.ceil() as i32;
3708         let ox = (layout.shadow_offset_x as f64 * self.scale) as i32;
3709         let oy = (layout.shadow_offset_y as f64 * self.scale) as i32;
3710         let radius_dev = (radius as f64 * self.scale) as i32;
3711         ffi::wlr_scene_shadow_set_color(self.shadow, layout.shadow_color.as_ptr());
3712         ffi::wlr_scene_shadow_set_blur_sigma(self.shadow, sigma);
3713         ffi::wlr_scene_shadow_set_corner_radius(self.shadow, radius_dev);
3714         ffi::wlr_scene_shadow_set_size(self.shadow, width + 2 * pad, height + 2 * pad);
3715         ffi::river_scene_node_set_position_if_changed(node, -pad + ox, -pad + oy);
3716         let r = radius_dev.clamp(0, u16::MAX as i32) as u16;
3717         ffi::wlr_scene_shadow_set_clipped_region(self.shadow, ffi::clipped_region {
3718             area: ffi::wlr_box { x: pad - ox, y: pad - oy, width, height },
3719             corners: ffi::fx_corner_radii {
3720                 top_left: r, top_right: r, bottom_right: r, bottom_left: r,
3721             },
3722         });
3723     }
3724 
3725     /// Sync the edge bevel with the current geometry. `width`/`height` are the
3726     /// content size in device px and `radius` the corner radius in logical px,
3727     /// exactly as `update_shadow` takes them. The rim is drawn INSIDE that box
3728     /// (see the shader), so it overlays the client's outermost pixels and needs
3729     /// no room of its own.
3730     ///
3731     /// The light direction is the DE's convention — the same top-left source
3732     /// the drop shadow is offset away from — so a window reads as a slab lit
3733     /// from the same place as everything else on the desktop.
3734     /// Is this window any seat's keyboard focus? The window's `activated`
3735     /// field is a configure-time snapshot, not live state, so live answers
3736     /// come from the seats.
3737     pub unsafe fn is_seat_focused(&self) -> bool {
3738         let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
3739         let mut curr = (*seats).next;
3740         while curr != seats {
3741             let seat = crate::container_of!(curr, crate::seat::Seat, link);
3742             if let crate::seat::Focus::Window(w) = (*seat).focused {
3743                 if w == self as *const Window as *mut Window {
3744                     return true;
3745                 }
3746             }
3747             curr = (*curr).next;
3748         }
3749         false
3750     }
3751 
3752     /// Whether this is the window the adjust-mode handles belong to: the
3753     /// toplevel under some seat's pointer (`Cursor::adjust_hover`), focused
3754     /// or not, in overview and with Super held alike — the handles are
3755     /// shown on what the pointer is over and on nothing else, so a pointer
3756     /// on the background shows none. (Overview's hover-to-focus still moves
3757     /// focus with the pointer, but focus is not what the ring keys on: a
3758     /// pointer resting on the background would otherwise keep the last
3759     /// window's ring up.) The reveal (`step_border_fade`), the drawn handles
3760     /// and catchers (`draw_borders`) and the hit test
3761     /// (`cursor::get_border_zone`) all ask this one predicate, so the ring
3762     /// cannot be drawn on one window and grabbed on another.
3763     pub unsafe fn is_adjust_target(&self) -> bool {
3764         let me = self as *const Window as *mut Window;
3765         let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
3766         let mut curr = (*seats).next;
3767         while curr != seats {
3768             let seat = crate::container_of!(curr, crate::seat::Seat, link);
3769             if (*seat).cursor.adjust_hover == me {
3770                 return true;
3771             }
3772             curr = (*curr).next;
3773         }
3774         false
3775     }
3776 
3777     pub unsafe fn update_bevel(&self, width: i32, height: i32, radius: i32, want: bool, want_focus: bool) {
3778         if self.bevel.is_null() {
3779             return;
3780         }
3781         let node = &mut (*self.bevel).node as *mut ffi::wlr_scene_node;
3782         let layout = &(*self.server).wm.layout;
3783         // Focused-window treatment: the rim highlight wraps all four sides
3784         // in the accent (the DE focus glint). Focus is read off the seats —
3785         // the window's `activated` field is a configure-time snapshot, not
3786         // live state — and this runs on both render paths, so a focus switch
3787         // restyles on the next frame. A focused window that is NOT in the
3788         // bevel app list still enables the node: the shader's focus branch
3789         // draws ONLY the glint, so it lays cleanly over a cce-ui app's own
3790         // client-side bevel instead of doubling its shading.
3791         let focused = self.is_seat_focused();
3792         let enabled = (want || (focused && want_focus))
3793             && layout.bevel_enabled
3794             && layout.bevel_thickness > 0.0
3795             && width > 0
3796             && height > 0;
3797         ffi::wlr_scene_node_set_enabled(node, enabled);
3798         if !enabled {
3799             return;
3800         }
3801 
3802         // Device px, like the blur radius and shadow sigma: the content is
3803         // scaled to its dest size, so an unscaled rim would keep its zoom-1
3804         // width while the window shrinks.
3805         let thickness = (layout.bevel_thickness as f64 * self.scale) as f32;
3806         let radius_dev = (radius as f64 * self.scale) as i32;
3807 
3808         // Light from the top-left, matching shadow_offset_x/y pointing away
3809         // from it. Normalized here so the shader can take it as-is.
3810         let (lx, ly) = (layout.bevel_light_x, layout.bevel_light_y);
3811         let len = (lx * lx + ly * ly).sqrt();
3812         let (lx, ly) = if len > 1e-6 { (lx / len, ly / len) } else { (-0.7071, -0.7071) };
3813 
3814         ffi::wlr_scene_bevel_set_size(self.bevel, width, height);
3815         ffi::wlr_scene_bevel_set_corner_radius(self.bevel, radius_dev);
3816         ffi::wlr_scene_bevel_set_thickness(self.bevel, thickness.max(1.0));
3817         ffi::wlr_scene_bevel_set_light(
3818             self.bevel,
3819             lx,
3820             ly,
3821             layout.bevel_light_intensity,
3822             layout.bevel_shade_intensity,
3823         );
3824         ffi::wlr_scene_bevel_set_shoulder(self.bevel, layout.bevel_shoulder);
3825         ffi::wlr_scene_bevel_set_color(self.bevel, layout.bevel_color.as_ptr());
3826         ffi::wlr_scene_bevel_set_focus(
3827             self.bevel,
3828             if focused { 1.0 } else { 0.0 },
3829             layout.bevel_focus_sharpness,
3830             layout.bevel_focus_color.as_ptr(),
3831         );
3832         ffi::river_scene_node_set_position_if_changed(node, 0, 0);
3833     }
3834     /// Sync the droplet backdrop-refraction node for a droplet-styled status
3835     /// segment. Called from BOTH render paths, like update_bevel — one-path
3836     /// effects freeze at the pre-gesture zoom (the shadow's old trap).
3837     pub unsafe fn update_droplet(&self, width: i32, height: i32) {
3838         if self.droplet.is_null() {
3839             return;
3840         }
3841         let node = &mut (*self.droplet).node as *mut ffi::wlr_scene_node;
3842         let layout = &(*self.server).wm.layout;
3843         let is_status = self.tiling_mode == crate::tiling::TilingMode::Status;
3844         // Only bar-strip segments: an expanded (menu) segment is taller than
3845         // the bar and draws its own grown drop client-side — refracting the
3846         // collapsed silhouette beneath it would be wrong.
3847         let enabled = is_status
3848             && layout.status_droplet.is_some()
3849             && width > 0
3850             && height > 0
3851             && height <= layout.bar_height as i32;
3852         if !enabled {
3853             ffi::wlr_scene_node_set_enabled(node, false);
3854             return;
3855         }
3856         let spec = cce_ui::scene::paint::DropletSpec::parse(
3857             layout.status_droplet.as_deref().unwrap_or(""),
3858         );
3859         if spec.refr <= 0.0 && spec.ghost <= 0.0 {
3860             ffi::wlr_scene_node_set_enabled(node, false);
3861             return;
3862         }
3863         ffi::wlr_scene_node_set_enabled(node, true);
3864 
3865         // Match the client's drop box: inset 1px from the surface bottom.
3866         // Camera-zoom scaling like the bevel; output scale is applied by the
3867         // render pass itself.
3868         let w = width as f32;
3869         let h = (height as f32 - 1.0).max(1.0);
3870         let (sr, ar, bow) = spec.resolve_silhouette(w, h);
3871         let k = (spec.blend.max(0.0) * h).max(1.0);
3872         let band = (spec.band.max(0.05) * h).max(1.0);
3873         let zs = self.scale as f32;
3874         ffi::wlr_scene_droplet_set_size(self.droplet, width, height);
3875         ffi::wlr_scene_droplet_set_silhouette(
3876             self.droplet,
3877             ar * zs,
3878             sr * zs,
3879             bow * zs,
3880             k * zs,
3881             spec.curve.clamp(2.0, 6.0),
3882         );
3883         ffi::wlr_scene_droplet_set_lens(self.droplet, band * zs, spec.refr * zs, spec.ghost.clamp(0.0, 1.0));
3884         ffi::river_scene_node_set_position_if_changed(node, 0, 0);
3885     }
3886     /// True when this status segment's droplet backdrop node is live. The
3887     /// per-window blur must yield to it: the blur pass would composite the
3888     /// UNREFRACTED cached backdrop over the lens output.
3889     pub unsafe fn droplet_backdrop_on(&self) -> bool {
3890         if self.droplet.is_null() || self.tiling_mode != crate::tiling::TilingMode::Status {
3891             return false;
3892         }
3893         match (*self.server).wm.layout.status_droplet.as_deref() {
3894             Some(raw) => {
3895                 let spec = cce_ui::scene::paint::DropletSpec::parse(raw);
3896                 spec.refr > 0.0 || spec.ghost > 0.0
3897             }
3898             None => false,
3899         }
3900     }
3901 
3902 
3903 
3904     /// The opacity the scene tree gets: the requested one, scaled down by the
3905     /// adjust-mode overlap dim (`adjust_dim`, 0..1) toward
3906     /// `border.overlap_opacity`, and again by the map/close fade
3907     /// (`map_fade`), which rests at 1.0 whenever no fade is in flight.
3908     pub unsafe fn effective_opacity(&self) -> f32 {
3909         let floor = (*self.server).wm.layout.border_overlap_opacity;
3910         self.rendering_requested.opacity
3911             * (1.0 - self.adjust_dim.clamp(0.0, 1.0) * (1.0 - floor))
3912             * self.map_fade.clamp(0.0, 1.0)
3913     }
3914 
3915     /// Whether this window takes the map/close fade at all. Surfaces that are
3916     /// part of the desktop itself rather than something the user opened — the
3917     /// status segments, the wallpaper, the grid layer — are left alone: they
3918     /// map once at login and a dissolve there reads as the desktop failing to
3919     /// draw. Same exclusion list `adjust_dim_wanted` uses, for the same
3920     /// reason: these are not windows the user thinks of as opening.
3921     pub unsafe fn wants_map_fade(&self) -> bool {
3922         !self.is_status_bar() && !self.is_wallpaper() && !self.is_grid()
3923     }
3924 
3925     /// Begin a fade toward `target` (0.0 out, 1.0 in) over `ms`, and arm the
3926     /// timer that steps it. A `ms` of 0 (or fading disabled) snaps instead,
3927     /// so every caller can treat this as "put the window at `target`".
3928     pub unsafe fn start_map_fade(&mut self, target: f32, ms: u32) {
3929         self.map_fade_target = target.clamp(0.0, 1.0);
3930         if ms == 0 || !self.wants_map_fade() {
3931             self.map_fade = self.map_fade_target;
3932             ffi::river_scene_node_set_opacity(
3933                 self.tree as *mut ffi::wlr_scene_node,
3934                 self.effective_opacity(),
3935             );
3936             return;
3937         }
3938         // Ticks at 16 ms; at least one step, so a sub-frame duration still
3939         // lands on the target rather than dividing by zero.
3940         let ticks = ((ms as f32) / 16.0).max(1.0);
3941         self.map_fade_step = ((self.map_fade_target - self.map_fade).abs() / ticks).max(1.0e-4);
3942         ffi::river_scene_node_set_opacity(
3943             self.tree as *mut ffi::wlr_scene_node,
3944             self.effective_opacity(),
3945         );
3946         (*self.server).wm.arm_border_fade();
3947     }
3948 
3949     /// Advance the map/close fade one tick toward `map_fade_target`, applying
3950     /// the opacity as it goes. Returns true while still in motion, like
3951     /// `step_adjust_dim`.
3952     pub unsafe fn step_map_fade(&mut self) -> bool {
3953         let delta = self.map_fade_target - self.map_fade;
3954         if delta.abs() <= self.map_fade_step {
3955             if self.map_fade == self.map_fade_target {
3956                 return false;
3957             }
3958             self.map_fade = self.map_fade_target;
3959         } else {
3960             self.map_fade += self.map_fade_step * delta.signum();
3961         }
3962         ffi::river_scene_node_set_opacity(
3963             self.tree as *mut ffi::wlr_scene_node,
3964             self.effective_opacity(),
3965         );
3966         true
3967     }
3968 
3969     /// Whether this window should be dimmed right now: adjust mode is on,
3970     /// this is a Floating window, and it lies ABOVE the adjust target in the
3971     /// render stack while overlapping it on screen — where it would cover
3972     /// the target's handles. Windows under the target are left alone; they
3973     /// hide nothing.
3974     pub unsafe fn adjust_dim_wanted(&self) -> bool {
3975         let wm = &(*self.server).wm;
3976         if !wm.window_adjust_active()
3977             || self.closed
3978             || self.tiling_mode != crate::tiling::TilingMode::Floating
3979             || self.is_status_bar()
3980             || self.is_wallpaper()
3981             || self.is_grid()
3982         {
3983             return false;
3984         }
3985         let me = self as *const Window as *mut Window;
3986         let on_screen = |w: *mut Window| -> (f64, f64, f64, f64) {
3987             let sc = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
3988             let g = (*w).box_geom;
3989             (g.x as f64, g.y as f64, g.width as f64 * sc, g.height as f64 * sc)
3990         };
3991         let (mx, my, mw, mh) = on_screen(me);
3992         // The render list runs bottom to top (raise_window moves to the
3993         // tail), so a target met before this window sits beneath it.
3994         let list = &wm.rendering_requested.list as *const ffi::wl_list as *mut WlList;
3995         let mut curr = (*list).next;
3996         let mut covered = false;
3997         while curr != list {
3998             let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
3999             if let crate::wm_node::WmNodeType::Window(w) = (*node).get() {
4000                 if w == me {
4001                     return covered;
4002                 }
4003                 if !w.is_null()
4004                     && !(*w).closed
4005                     && window_takes_handles(w)
4006                     && (*w).is_adjust_target()
4007                 {
4008                     let (tx, ty, tw, th) = on_screen(w);
4009                     if mx < tx + tw && tx < mx + mw && my < ty + th && ty < my + mh {
4010                         covered = true;
4011                     }
4012                 }
4013             }
4014             curr = (*curr).next;
4015         }
4016         false
4017     }
4018 
4019     /// Advance the overlap dim one tick toward where `adjust_dim_wanted`
4020     /// says it should rest, applying the opacity as it goes. Returns true
4021     /// while still in motion, like `step_border_fade`.
4022     pub unsafe fn step_adjust_dim(&mut self) -> bool {
4023         let target = if self.adjust_dim_wanted() { 1.0 } else { 0.0 };
4024         let delta = target - self.adjust_dim;
4025         let moving;
4026         if delta.abs() <= BORDER_FADE_EPSILON {
4027             if self.adjust_dim == target {
4028                 return false;
4029             }
4030             self.adjust_dim = target;
4031             moving = false;
4032         } else {
4033             self.adjust_dim += delta * BORDER_FADE_STEP;
4034             moving = true;
4035         }
4036         ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, self.effective_opacity());
4037         moving
4038     }
4039 
4040     /// Advance the hover fade one tick. Every zone eases toward 1.0 if it is
4041     /// the one under the pointer and 0.0 otherwise. Returns true while any
4042     /// zone is still in motion, so the caller knows to schedule another tick.
4043     pub unsafe fn step_border_fade(&mut self) -> bool {
4044         let mut moving = false;
4045         let mut changed = false;
4046         // The adjust TARGET — the window under the pointer — shows its whole
4047         // ring for as long as the mode is on; other windows show nothing.
4048         // The ring follows the pointer from window to window, each swap
4049         // easing through this same fade. Hover still reads through on the revealed ring, as
4050         // `color_for` paints the hovered zone in hover_color over the full
4051         // reveal.
4052         let all_on = (*self.server).wm.window_adjust_active()
4053             && window_takes_handles(self as *mut Window)
4054             && self.is_adjust_target();
4055         for elem in BorderElement::ALL {
4056             let i = elem.index();
4057             let target = if all_on || self.hovered_border_element == Some(elem) { 1.0 } else { 0.0 };
4058             let delta = target - self.border_reveal[i];
4059             if delta.abs() <= BORDER_FADE_EPSILON {
4060                 if self.border_reveal[i] != target {
4061                     self.border_reveal[i] = target;
4062                     changed = true;
4063                 }
4064                 continue;
4065             }
4066             self.border_reveal[i] += delta * BORDER_FADE_STEP;
4067             moving = true;
4068             changed = true;
4069         }
4070         // A hover swap on a fully revealed ring moves nothing above, but the
4071         // shader still has to be told which zone to paint.
4072         if self.hovered_border_element != self.border_hover_drawn {
4073             changed = true;
4074         }
4075         if changed {
4076             self.draw_borders();
4077         }
4078         moving
4079     }
4080 
4081     /// The output a fullscreen window fills: the one the WM pinned it to, or
4082     /// the first enabled output (the same fallback manage/render use).
4083     pub unsafe fn fullscreen_output(&self) -> *mut crate::output::Output {
4084         if !self.wm_requested.fullscreen.is_null() {
4085             return self.wm_requested.fullscreen;
4086         }
4087         let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
4088         let mut curr = (*outputs_list).next;
4089         while curr != outputs_list {
4090             let out = crate::container_of!(curr, crate::output::Output, link);
4091             if (*out).sent.state == crate::output::OutputStateValue::Enabled {
4092                 return out;
4093             }
4094             curr = (*curr).next;
4095         }
4096         std::ptr::null_mut()
4097     }
4098 
4099     /// Arms the fullscreen-toggle animation at the window's current on-screen
4100     /// rect. Called from manage_finish on the enter/exit transition, before
4101     /// the settled geometry is rewritten; a re-toggle mid-flight continues
4102     /// from wherever the previous animation had reached. Sized with
4103     /// last_applied_scale (the scale actually drawn) because self.scale has
4104     /// already been rewritten to the destination state's scale by the arrange
4105     /// pass in this same cycle.
4106     unsafe fn start_fs_anim(&mut self) {
4107         if !matches!(self.impl_type, WindowImpl::Toplevel(_))
4108             || !matches!(self.state, WindowState::Mapped)
4109             || self.box_geom.width <= 0
4110             || self.box_geom.height <= 0
4111         {
4112             return;
4113         }
4114         let (x, y, w, h) = if let Some(a) = self.fs_anim {
4115             (a.x, a.y, a.w, a.h)
4116         } else {
4117             let s = if self.last_applied_scale > 0.0 { self.last_applied_scale } else { 1.0 };
4118             (
4119                 self.box_geom.x as f64,
4120                 self.box_geom.y as f64,
4121                 self.box_geom.width as f64 * s,
4122                 self.box_geom.height as f64 * s,
4123             )
4124         };
4125         self.fs_anim = Some(FsAnim { x, y, w, h, moved: false, ticks: 0 });
4126         (*self.server).wm.arm_border_fade();
4127     }
4128 
4129     /// One tick of the fullscreen-toggle animation. Returns true while the
4130     /// caller should re-render (including the final settling frame). The
4131     /// target rect is recomputed live every tick — the output box while
4132     /// fullscreen, else the arranged position at the last configured size —
4133     /// so it tracks the client's asynchronous resize instead of freezing a
4134     /// stale goal on the first frame.
4135     pub unsafe fn step_fs_anim(&mut self) -> bool {
4136         let Some(mut anim) = self.fs_anim else {
4137             return false;
4138         };
4139 
4140         let (tx, ty, tw, th) = if self.is_fullscreen() {
4141             let output = self.fullscreen_output();
4142             if output.is_null() {
4143                 self.fs_anim = None;
4144                 return true;
4145             }
4146             let (w, h) = (*output).sent.dimensions();
4147             ((*output).sent.x as f64, (*output).sent.y as f64, w as f64, h as f64)
4148         } else {
4149             let w = self.configure_sent.width.map(|w| w as i32).unwrap_or(self.box_geom.width);
4150             let h = self.configure_sent.height.map(|h| h as i32).unwrap_or(self.box_geom.height);
4151             (
4152                 self.rendering_requested.x as f64,
4153                 self.rendering_requested.y as f64,
4154                 w as f64 * self.scale,
4155                 h as f64 * self.scale,
4156             )
4157         };
4158 
4159         anim.ticks += 1;
4160         let dx = tx - anim.x;
4161         let dy = ty - anim.y;
4162         let dw = tw - anim.w;
4163         let dh = th - anim.h;
4164         let settled = dx.abs() < FS_ANIM_EPSILON
4165             && dy.abs() < FS_ANIM_EPSILON
4166             && dw.abs() < FS_ANIM_EPSILON
4167             && dh.abs() < FS_ANIM_EPSILON;
4168         if !settled {
4169             anim.moved = true;
4170         }
4171         if (settled && anim.moved) || anim.ticks > FS_ANIM_MAX_TICKS {
4172             self.fs_anim = None;
4173             return true;
4174         }
4175         anim.x += dx * FS_ANIM_STEP;
4176         anim.y += dy * FS_ANIM_STEP;
4177         anim.w += dw * FS_ANIM_STEP;
4178         anim.h += dh * FS_ANIM_STEP;
4179         self.fs_anim = Some(anim);
4180         true
4181     }
4182 
4183     /// Outward extent (unscaled px) the interactive border may reach on each
4184     /// side — `[left, right, top, bottom]` — after the foam rule against the
4185     /// other windows: where two windows' bands would overlap across a gap,
4186     /// each band stops at the gap's midline (the ramp key-ring behavior,
4187     /// rectangular — the wall is equidistant from the two content edges).
4188     /// Stacked windows (content rects overlapping) do not clip each other,
4189     /// mirroring the rings' degenerate-distance guard. Per-side, not
4190     /// per-span: one near neighbor claims the whole facing side.
4191     pub unsafe fn border_side_extents(&self, band_unscaled: f64) -> [f64; 4] {
4192         let scale = if self.scale > 0.0 { self.scale } else { 1.0 };
4193         let band = band_unscaled * scale;
4194         let ax0 = self.box_geom.x as f64;
4195         let ay0 = self.box_geom.y as f64;
4196         let ax1 = ax0 + self.box_geom.width as f64 * scale;
4197         let ay1 = ay0 + self.box_geom.height as f64 * scale;
4198         let mut ext = [band; 4]; // left, right, top, bottom (layout px)
4199 
4200         let self_ptr = self as *const Window as *mut Window;
4201         for &other in (*self.server).wm.windows.iter() {
4202             if other.is_null() || other == self_ptr {
4203                 continue;
4204             }
4205             let o = &*other;
4206             if o.closed
4207                 || o.minimized
4208                 || o.rendering_requested.hidden
4209                 || o.rendering_requested.circular
4210                 || matches!(
4211                     o.tiling_mode,
4212                     crate::tiling::TilingMode::Popup
4213                         | crate::tiling::TilingMode::Fullscreen
4214                         | crate::tiling::TilingMode::Status
4215                 )
4216                 || o.is_status_bar()
4217                 || o.is_wallpaper()
4218             {
4219                 continue;
4220             }
4221             let os = if o.scale > 0.0 { o.scale } else { 1.0 };
4222             let bx0 = o.box_geom.x as f64;
4223             let by0 = o.box_geom.y as f64;
4224             let bx1 = bx0 + o.box_geom.width as f64 * os;
4225             let by1 = by0 + o.box_geom.height as f64 * os;
4226             // Stacked: keep the full band.
4227             if bx0 < ax1 && bx1 > ax0 && by0 < ay1 && by1 > ay0 {
4228                 continue;
4229             }
4230             let ob = border_band_width(o.rendering_requested.border.width) * os;
4231             // Spans (including bands) must overlap for a wall to exist.
4232             let v_overlap = by0 - ob < ay1 + band && by1 + ob > ay0 - band;
4233             let h_overlap = bx0 - ob < ax1 + band && bx1 + ob > ax0 - band;
4234             if v_overlap {
4235                 if bx0 >= ax1 {
4236                     let gap = bx0 - ax1;
4237                     if gap < band + ob {
4238                         ext[1] = ext[1].min((gap / 2.0).max(0.0));
4239                     }
4240                 } else if bx1 <= ax0 {
4241                     let gap = ax0 - bx1;
4242                     if gap < band + ob {
4243                         ext[0] = ext[0].min((gap / 2.0).max(0.0));
4244                     }
4245                 }
4246             }
4247             if h_overlap {
4248                 if by0 >= ay1 {
4249                     let gap = by0 - ay1;
4250                     if gap < band + ob {
4251                         ext[3] = ext[3].min((gap / 2.0).max(0.0));
4252                     }
4253                 } else if by1 <= ay0 {
4254                     let gap = ay0 - by1;
4255                     if gap < band + ob {
4256                         ext[2] = ext[2].min((gap / 2.0).max(0.0));
4257                     }
4258                 }
4259             }
4260         }
4261         [ext[0] / scale, ext[1] / scale, ext[2] / scale, ext[3] / scale]
4262     }
4263 
4264     }
4265 
4266 /// Does this window get resize handles at all?
4267 ///
4268 /// The single answer for both halves — `cursor::get_border_zone`'s hit test
4269 /// and `draw_borders`' visuals — so a window can never show a handle it
4270 /// would not honour, or honour one it does not show. Excluded: the internal
4271 /// roles that are not user-geometry (Popup, Fullscreen, Status), Utility
4272 /// (self-sizing by definition — the client owns its size), circular windows
4273 /// (no rectangular ring to hug), and hidden ones.
4274 pub unsafe fn window_takes_handles(window: *mut Window) -> bool {
4275     !matches!(
4276         (*window).tiling_mode,
4277         crate::tiling::TilingMode::Popup
4278             | crate::tiling::TilingMode::Fullscreen
4279             | crate::tiling::TilingMode::Status
4280             | crate::tiling::TilingMode::Utility
4281     ) && !(*window).rendering_requested.circular
4282         && !(*window).rendering_requested.hidden
4283 }
4284 
4285 impl Window {
4286     pub unsafe fn draw_borders(&mut self) {
4287         // Taken before `requested` borrows self: `window_takes_handles` is
4288         // the shared predicate with cursor::get_border_zone and must not be
4289         // duplicated here just to satisfy borrowck.
4290         let self_ptr = self as *mut Window;
4291         let requested = &self.rendering_requested;
4292 
4293         let border = &requested.border;
4294         let border_color = border.color;
4295         ffi::river_scene_node_set_position_if_changed(self.window_background as *mut ffi::wlr_scene_node, 0, 0);
4296         let bg_width = (self.box_geom.width as f64 * self.scale) as i32;
4297         let bg_height = (self.box_geom.height as f64 * self.scale) as i32;
4298         ffi::river_scene_rect_set_size_if_changed(self.window_background, bg_width, bg_height);
4299         ffi::wlr_scene_rect_set_color(self.window_background, border_color.as_ptr());
4300         // The background plate sits directly under the client's plate, so it
4301         // takes the ROOT_PLATE radius and the same span widening as the
4302         // blur/clip radius — not the border ring's radius, which is a
4303         // separate key describing a different edge.
4304         let bg_radius = widen_corner_radius(
4305             self.root_plate_radius_base(),
4306             self.box_geom.width,
4307             self.box_geom.height,
4308         );
4309         ffi::river_scene_rect_set_corner_radius(self.window_background, (bg_radius as f64 * self.scale) as i32);
4310         ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, !requested.hidden && self.wm_requested.ssd);
4311 
4312         // The handles draw as eight discs in one frame node; the hovered
4313         // disc draws in hover_color. Under each disc a transparent square
4314         // rect is a scene hit-test catcher (width 0 keeps the legacy
4315         // invisible 8px virtual resize zones).
4316         //
4317         // They live in `border.tree`, parented to the global border overlay
4318         // layer rather than to this window's tree, so it has to be
4319         // positioned and enabled in step with the window by hand.
4320         let is_virtual_border = border.width == 0;
4321         // Deliberately NOT gated on `wm_requested.ssd`: that flag defaults to
4322         // false and is only set by a client calling use_ssd, and the segments
4323         // have never depended on it — only `window_background` does.
4324         let borders_visible = !requested.hidden
4325             && !requested.circular
4326             && !is_virtual_border
4327             && self.border_reveal.iter().any(|&a| a > 0.0);
4328         ffi::wlr_scene_node_set_enabled(self.border.tree as *mut ffi::wlr_scene_node, borders_visible);
4329         if borders_visible {
4330             ffi::river_scene_node_set_position_if_changed(
4331                 self.border.tree as *mut ffi::wlr_scene_node,
4332                 self.box_geom.x,
4333                 self.box_geom.y,
4334             );
4335         }
4336         if requested.circular {
4337             ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
4338             ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
4339             ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
4340             ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
4341             for &seg in self.border.segments.iter() {
4342                 ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
4343             }
4344             return;
4345         }
4346         let content = ffi::wlr_box {
4347             x: 0,
4348             y: 0,
4349             width: self.box_geom.width,
4350             height: self.box_geom.height,
4351         };
4352 
4353         let mut intersect = std::mem::zeroed();
4354         let clip_empty = requested.content_clip.width == 0 && requested.content_clip.height == 0;
4355         if clip_empty || ffi::wlr_box_intersection(&mut intersect, &content, &requested.content_clip) {
4356             let border = &requested.border;
4357             // The interactive band (doubled configured width, floored). The
4358             // per-side foam clipping this used to carry is gone with the
4359             // outside band: it split a gap SHARED with a neighbouring window,
4360             // and the inside ring shares nothing.
4361             let band_f = border_band_width(border.width);
4362             let band = band_f as i32;
4363             let transparent = [0.0f32; 4];
4364 
4365             // The rounded-frame path used to leave radius/clip state on the
4366             // top band rect; keep it reset.
4367             ffi::river_scene_rect_set_corner_radius(self.border.top, 0);
4368             ffi::wlr_scene_rect_set_clipped_region(self.border.top, ffi::clipped_region_get_default());
4369 
4370             let apply = |rect: *mut ffi::wlr_scene_rect, bx: ffi::wlr_box, color: &[f32; 4], enabled: bool| {
4371                 let mut bx = bx;
4372                 if enabled && (requested.clip.width != 0 || requested.clip.height != 0) {
4373                     let mut clip_intersect = std::mem::zeroed();
4374                     ffi::wlr_box_intersection(&mut clip_intersect, &bx, &requested.clip);
4375                     bx = clip_intersect;
4376                 }
4377                 let enabled = enabled && bx.width > 0 && bx.height > 0;
4378                 ffi::wlr_scene_node_set_enabled(rect as *mut ffi::wlr_scene_node, enabled);
4379                 if !enabled {
4380                     return;
4381                 }
4382                 ffi::river_scene_node_set_position_if_changed(
4383                     rect as *mut ffi::wlr_scene_node,
4384                     (bx.x as f64 * self.scale) as i32,
4385                     (bx.y as f64 * self.scale) as i32,
4386                 );
4387                 ffi::river_scene_rect_set_size_if_changed(
4388                     rect,
4389                     (bx.width as f64 * self.scale) as i32,
4390                     (bx.height as f64 * self.scale) as i32,
4391                 );
4392                 ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
4393             };
4394 
4395             // Handles live INSIDE the content rect, and only in overview
4396             // mode — see `cursor::get_border_zone`, which hit-tests the same
4397             // ring from the same band width and corner length. In normal
4398             // mode there is nothing to grab, so the catchers and the visible
4399             // segments are both disabled outright. The window's own border
4400             // (`window_background`, above) is untouched in either mode: this
4401             // moved the HANDLES inward, not the border.
4402             // Overview, or Super held: the same adjust mode at any zoom.
4403             let in_overview = (*self.server).wm.window_adjust_active();
4404             let bw = band;
4405             let layout_handle_w = (*self.server).wm.layout.border_handle_width;
4406             let sc = if self.scale > 0.0 { self.scale } else { 1.0 };
4407             let (cw, ch) = (content.width, content.height);
4408             // A window thinner than two bands has no interior left for a
4409             // ring; drawing one would be a solid block over the whole window.
4410             // Live handles: the mode is on and this is the adjust target
4411             // (the window under the pointer). Target-only,
4412             // like the reveal in step_border_fade: without this the
4413             // invisible catcher rects would keep intercepting scene hits on
4414             // windows whose ring is not even drawn.
4415             let handles_live = in_overview && self.is_adjust_target();
4416             // Drawn handles: live, OR still fading out — releasing Super (or
4417             // leaving overview, or losing focus) eases the ring away instead
4418             // of cutting it, so the ring stays drawn while any reveal is
4419             // above zero. The catchers below are gated on `handles_live`
4420             // alone: a fading ring is decoration, never a grab.
4421             let fading_out = !handles_live && self.border_reveal.iter().any(|&a| a > 0.0);
4422             let handles_on = (handles_live || fading_out)
4423                 && window_takes_handles(self_ptr)
4424                 && !is_virtual_border
4425                 && bw > 0
4426                 && (cw as f64 * sc) >= 12.0
4427                 && (ch as f64 * sc) >= 12.0;
4428             if !handles_on {
4429                 // Nothing is drawn, so nothing is stale: without this the
4430                 // fade step would see a mismatch and repaint every tick.
4431                 self.border_hover_drawn = self.hovered_border_element;
4432                 for r in [self.border.left, self.border.right, self.border.top, self.border.bottom] {
4433                     ffi::wlr_scene_node_set_enabled(r as *mut ffi::wlr_scene_node, false);
4434                 }
4435                 for &seg in self.border.segments.iter() {
4436                     ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
4437                 }
4438                 ffi::wlr_scene_node_set_enabled(
4439                     &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4440                     false,
4441                 );
4442                 return;
4443             }
4444 
4445             // The band is a SCREEN width, not a world one. Handles exist only
4446             // in overview, which is zoomed OUT, so a band that scaled with the
4447             // window would be at its thinnest exactly where it is the only way
4448             // to resize — 16px becomes 7 at a typical overview zoom, and the
4449             // thin corners 2.5. `apply` scales the boxes it is given, so the
4450             // catchers are sized in unscaled units that come back to
4451             // `band_screen` on screen. cursor::get_border_zone measures the
4452             // same width in layout px; the two must agree.
4453             // Screen thickness, but never more than a fifth of the smaller
4454             // on-screen side: a zoomed-out window would otherwise be mostly
4455             // ring. Shrinking beats the old hard cutoff, which dropped the
4456             // handles altogether below a threshold — a window you cannot
4457             // resize at all is worse than one with a slimmer grip.
4458             let short_side = (cw.min(ch) as f64 * sc).max(1.0);
4459             let band_screen = (layout_handle_w as f64)
4460                 .max(crate::window::HOVER_BAND_MIN)
4461                 .min(short_side * 0.2);
4462             let px = |v: i32| (v as f64 * sc) as i32;
4463 
4464             // The band catchers are retired: between two discs the pointer
4465             // must reach the app, not a catcher.
4466             for r in [self.border.left, self.border.right, self.border.top, self.border.bottom] {
4467                 ffi::wlr_scene_node_set_enabled(r as *mut ffi::wlr_scene_node, false);
4468             }
4469 
4470             let layout = &(*self.server).wm.layout;
4471             // The discs sit inside the window's own silhouette, so the
4472             // corner discs place against the content radius (the widened
4473             // root plate radius the corner clip uses).
4474             let r_in = bg_radius;
4475 
4476             // Hit catchers: one transparent square under each disc, laid out
4477             // by the same function the hit test uses. `apply` takes unscaled
4478             // boxes, so the screen-px layout is divided back out (a px of
4479             // rounding on an invisible catcher is nothing).
4480             let (centres, disc_r) = handle_disc_layout(
4481                 cw as f64 * sc,
4482                 ch as f64 * sc,
4483                 px(r_in) as f64,
4484                 band_screen,
4485             );
4486             for (i, &(cx, cy)) in centres.iter().enumerate() {
4487                 let b = ffi::wlr_box {
4488                     x: ((cx - disc_r) / sc).floor() as i32,
4489                     y: ((cy - disc_r) / sc).floor() as i32,
4490                     width: (2.0 * disc_r / sc).ceil() as i32,
4491                     height: (2.0 * disc_r / sc).ceil() as i32,
4492                 };
4493                 apply(self.border.segments[i], b, &transparent, handles_live);
4494             }
4495             // corner_len and gap are retired by the wave profile (the
4496             // valleys place the seams now, a quarter along each side) and
4497             // ignored by the shader; still passed so the node API holds.
4498             let cl = border_corner_len(bw as f64, layout.border_corner_length, r_in as f64) as i32;
4499             let g = layout.border_segment_gap;
4500 
4501             // Handles rest invisible and fade in with the mode; one alpha for
4502             // the whole ring now that every zone reveals together in overview
4503             // (`step_border_fade`'s all_on branch). The Top slot carries it —
4504             // they are all equal while the ring is up, and taking one keeps
4505             // the fade a single number.
4506             let a = self.border_reveal[BorderElement::Top.index()].clamp(0.0, 1.0);
4507             let premul = |c: &[f32; 4]| [c[0] * a, c[1] * a, c[2] * a, c[3] * a];
4508 
4509             ffi::wlr_scene_frame_set_size(self.border.frame, px(cw), px(ch));
4510             ffi::wlr_scene_frame_set_corner_radius(self.border.frame, px(r_in));
4511             // band is the disc diameter; the rest is retired by the discs and
4512             // ignored by the shader, still passed so the node API holds.
4513             ffi::wlr_scene_frame_set_shape(
4514                 self.border.frame,
4515                 band_screen as f32,
4516                 (band_screen as f32 * layout.border_taper.clamp(0.0, 1.0)).max(2.0),
4517                 (px(cl) as f64).max(band_screen) as f32,
4518                 px(g) as f32,
4519                 layout.border_swell_curve,
4520                 (layout.border_corner_bulge as f64).min(short_side * 0.3) as f32,
4521             );
4522             // The popover hint arrives in surface-local LOGICAL px; the node
4523             // space is zoom-scaled device px like everything else here, so it
4524             // takes the same px() mapping. Zeroed when clear.
4525             let ex = match self.popover_region {
4526                 Some(r) => [
4527                     px(r.x) as f32,
4528                     px(r.y) as f32,
4529                     px(r.width) as f32,
4530                     px(r.height) as f32,
4531                 ],
4532                 None => [0.0; 4],
4533             };
4534             ffi::wlr_scene_frame_set_exclusion(self.border.frame, ex.as_ptr());
4535             // The ring exists only for the SEAT-focused window — `handles_on`
4536             // above says so, and so does step_border_fade's reveal — so it
4537             // paints in the focused color, taken from the layout rather than
4538             // from `requested.border.color`.
4539             //
4540             // That field is a PLAN value, written by the arrange pass from the
4541             // focus it saw when it ran, and a focus change only schedules an
4542             // arrange when the newly focused window happens to be Floating
4543             // (Seat::focus). Every other focus change armed the border fade
4544             // and nothing else, so the ring eased in wearing the UNFOCUSED
4545             // color: hovering a tiled window in overview drew its resize ring
4546             // in the plain border gray instead of the focus color, and it
4547             // stayed gray until some unrelated transaction refreshed the plan.
4548             // Whether the ring is drawn and what color it is are the same
4549             // fact — focused — so both now read it live, the way update_bevel
4550             // already reads focus off the seats for the rim highlight.
4551             //
4552             // `window_background` above keeps the plan color: that one is the
4553             // window's own plate, not this compositor-drawn handle.
4554             ffi::wlr_scene_frame_set_color(
4555                 self.border.frame,
4556                 premul(&layout.border_color_focused).as_ptr(),
4557             );
4558             let hovered = self
4559                 .hovered_border_element
4560                 .map(|e| e.index() as f32)
4561                 .unwrap_or(-1.0);
4562             self.border_hover_drawn = self.hovered_border_element;
4563             ffi::wlr_scene_frame_set_hover(
4564                 self.border.frame,
4565                 hovered,
4566                 premul(&border.hover_color).as_ptr(),
4567             );
4568             ffi::river_scene_node_set_position_if_changed(
4569                 &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4570                 0,
4571                 0,
4572             );
4573             ffi::wlr_scene_node_set_enabled(
4574                 &mut (*self.border.frame).node as *mut ffi::wlr_scene_node,
4575                 a > 0.0,
4576             );
4577         }
4578     }
4579 
4580     #[allow(unused_assignments)]
4581     pub unsafe fn apply_surface_clip(&mut self, a: *const ffi::wlr_box, b: *const ffi::wlr_box) {
4582         let mut surface_clip = std::mem::zeroed::<ffi::wlr_box>();
4583         let a_empty = (*a).width == 0 && (*a).height == 0;
4584         let b_empty = (*b).width == 0 && (*b).height == 0;
4585 
4586         let layout_box = ffi::wlr_box {
4587             x: 0,
4588             y: 0,
4589             width: self.box_geom.width,
4590             height: self.box_geom.height,
4591         };
4592 
4593         if !a_empty && !b_empty {
4594             let mut temp_clip = std::mem::zeroed::<ffi::wlr_box>();
4595             if !ffi::wlr_box_intersection(&mut temp_clip, a, b) {
4596                 self.surfaces.set_enabled(false);
4597                 return;
4598             }
4599             if !ffi::wlr_box_intersection(&mut surface_clip, &temp_clip, &layout_box) {
4600                 self.surfaces.set_enabled(false);
4601                 return;
4602             }
4603         } else if !a_empty {
4604             if !ffi::wlr_box_intersection(&mut surface_clip, a, &layout_box) {
4605                 self.surfaces.set_enabled(false);
4606                 return;
4607             }
4608         } else if !b_empty {
4609             if !ffi::wlr_box_intersection(&mut surface_clip, b, &layout_box) {
4610                 self.surfaces.set_enabled(false);
4611                 return;
4612             }
4613         } else {
4614             surface_clip = layout_box;
4615         }
4616 
4617         self.surfaces.set_enabled(true);
4618         let margin = 0;
4619         surface_clip.x -= margin;
4620         surface_clip.y -= margin;
4621         surface_clip.width += 2 * margin;
4622         surface_clip.height += 2 * margin;
4623 
4624         match self.impl_type {
4625             WindowImpl::Toplevel(toplevel) => {
4626                 if !toplevel.is_null() {
4627                     let x = if self.wm_requested.ssd { 0 } else { (*toplevel).geometry.x };
4628                     let y = if self.wm_requested.ssd { 0 } else { (*toplevel).geometry.y };
4629                     surface_clip.x += x;
4630                     surface_clip.y += y;
4631                 }
4632             }
4633             WindowImpl::Xwayland(xwindow) => {
4634                 if !xwindow.is_null() {
4635                     let title_ptr = (*(*xwindow).xsurface).title;
4636                     let title = if title_ptr.is_null() { "" } else { std::ffi::CStr::from_ptr(title_ptr).to_str().unwrap_or("") };
4637                     if title.contains("Ubisoft") {
4638                         log::info!(
4639                             "XWayland window clip check: title='{}' box_geom=({}, {}, {}, {}) xsurface=({}, {}, {}, {})",
4640                             title,
4641                             self.box_geom.x,
4642                             self.box_geom.y,
4643                             self.box_geom.width,
4644                             self.box_geom.height,
4645                             (*(*xwindow).xsurface).x,
4646                             (*(*xwindow).xsurface).y,
4647                             (*(*xwindow).xsurface).width,
4648                             (*(*xwindow).xsurface).height,
4649                         );
4650                     }
4651                 }
4652             }
4653             _ => {}
4654         }
4655 
4656         // Crop a CSD toplevel to its xdg geometry. Chromium-family clients
4657         // paint a translucent shadow band outside the geometry whenever they
4658         // are not maximized; the compositor draws its own shadow, and it
4659         // rounds corners per buffer at the buffer's edge, so uncropped the
4660         // rounding fell in that band and the visible window read
4661         // square-cornered (an Electron window un-tiled by a fullscreen round
4662         // trip). A geometry clip was set once and nulled in 34b3ae64: the
4663         // scaling passes rewrote every buffer's dest size from the full
4664         // surface each commit and stretched the crop back out — they go
4665         // through surface_buffer_extent now. Skipped while a cce-ui client
4666         // has a popover overhanging its geometry (set_popover_region): that
4667         // rim is live menu content, not a shadow. And skipped mid
4668         // fullscreen-toggle, where the animation owns the buffers' stretch.
4669         let mut crop = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
4670         if let WindowImpl::Toplevel(toplevel) = self.impl_type {
4671             if !toplevel.is_null()
4672                 && !self.wm_requested.ssd
4673                 && self.popover_region.is_none()
4674                 && self.fs_anim.is_none()
4675             {
4676                 crop = (*toplevel).geometry;
4677             }
4678         }
4679         let clip: *const ffi::wlr_box = if crop.width > 0 && crop.height > 0 {
4680             &crop
4681         } else {
4682             std::ptr::null()
4683         };
4684         let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
4685         if (*children_head).next != children_head {
4686             ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, clip);
4687         }
4688     }
4689 }
4690 
4691 unsafe fn clock_gettime(clk_id: libc::clockid_t, tp: &mut libc::timespec) -> libc::c_int {
4692     libc::clock_gettime(clk_id, tp)
4693 }
4694 
4695 unsafe extern "C" fn window_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4696     ffi::wl_resource_destroy(resource);
4697 }
4698 
4699 unsafe extern "C" fn window_close(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4700     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4701     if window.is_null() {
4702         return;
4703     }
4704     let server = (*window).server;
4705     if !(*server).wm.ensure_windowing() {
4706         return;
4707     }
4708     (*window).wm_requested.close = true;
4709 }
4710 
4711 unsafe extern "C" fn window_get_node(
4712     client: *mut ffi::wl_client,
4713     resource: *mut ffi::wl_resource,
4714     id: u32,
4715 ) {
4716     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4717     if window.is_null() {
4718         return;
4719     }
4720     if !(*window).node.object.is_null() {
4721         ffi::wl_resource_post_error(
4722             resource,
4723             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_NODE_EXISTS,
4724             b"window already has a node object\0".as_ptr() as *const _,
4725         );
4726         return;
4727     }
4728     (*window).node.create_object(client, ffi::wl_resource_get_version(resource) as u32, id);
4729 }
4730 
4731 unsafe extern "C" fn window_propose_dimensions(
4732     _client: *mut ffi::wl_client,
4733     resource: *mut ffi::wl_resource,
4734     width: i32,
4735     height: i32,
4736 ) {
4737     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4738     if window.is_null() {
4739         return;
4740     }
4741     let server = (*window).server;
4742     if !(*server).wm.ensure_windowing() {
4743         return;
4744     }
4745     if width < 0 || height < 0 {
4746         ffi::wl_resource_post_error(
4747             resource,
4748             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
4749             b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
4750         );
4751         return;
4752     }
4753     if (*window).get_parent().is_null() {
4754         (*window).wm_requested.dimensions = Some(Dimensions {
4755             width: width as u32,
4756             height: height as u32,
4757         });
4758     }
4759 }
4760 
4761 unsafe extern "C" fn window_hide(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4762     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4763     if window.is_null() {
4764         return;
4765     }
4766     let server = (*window).server;
4767     if !(*server).wm.ensure_rendering() {
4768         return;
4769     }
4770     (*window).rendering_requested.hidden = true;
4771 }
4772 
4773 unsafe extern "C" fn window_show(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4774     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4775     if window.is_null() {
4776         return;
4777     }
4778     let server = (*window).server;
4779     if !(*server).wm.ensure_rendering() {
4780         return;
4781     }
4782     (*window).rendering_requested.hidden = false;
4783 }
4784 
4785 unsafe extern "C" fn window_use_csd(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4786     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4787     if window.is_null() {
4788         return;
4789     }
4790     let server = (*window).server;
4791     if !(*server).wm.ensure_windowing() {
4792         return;
4793     }
4794     (*window).wm_requested.ssd = false;
4795     (*server).wm.dirty_windowing();
4796 }
4797 
4798 unsafe extern "C" fn window_use_ssd(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
4799     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4800     if window.is_null() {
4801         return;
4802     }
4803     let server = (*window).server;
4804     if !(*server).wm.ensure_windowing() {
4805         return;
4806     }
4807     (*window).wm_requested.ssd = true;
4808     (*server).wm.dirty_windowing();
4809 }
4810 
4811 unsafe extern "C" fn window_set_borders(
4812     _client: *mut ffi::wl_client,
4813     resource: *mut ffi::wl_resource,
4814     edges: u32,
4815     width: i32,
4816     r: u32,
4817     g: u32,
4818     b: u32,
4819     a: u32,
4820 ) {
4821     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4822     if window.is_null() {
4823         return;
4824     }
4825     let server = (*window).server;
4826     if !(*server).wm.ensure_rendering() {
4827         return;
4828     }
4829     if width < 0 {
4830         ffi::wl_resource_post_error(
4831             resource,
4832             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_BORDER,
4833             b"border width must be greater than or equal to 0\0".as_ptr() as *const _,
4834         );
4835         return;
4836     }
4837     let alpha = (a as f64 / u32::MAX as f64) as f32;
4838     // Protocol channels are straight alpha; scene colors are premultiplied.
4839     let color = [
4840         (r as f64 / u32::MAX as f64) as f32 * alpha,
4841         (g as f64 / u32::MAX as f64) as f32 * alpha,
4842         (b as f64 / u32::MAX as f64) as f32 * alpha,
4843         alpha,
4844     ];
4845     (*window).rendering_requested.border = Border {
4846         edges: Edges::from_u32(edges),
4847         width: width as u32,
4848         color,
4849         // Protocol-set borders don't participate in hover highlighting.
4850         hover_color: color,
4851         corner_radius: 0,
4852     };
4853 }
4854 
4855 unsafe extern "C" fn window_set_tiled(
4856     _client: *mut ffi::wl_client,
4857     resource: *mut ffi::wl_resource,
4858     edges: u32,
4859 ) {
4860     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4861     if window.is_null() {
4862         return;
4863     }
4864     let server = (*window).server;
4865     if !(*server).wm.ensure_windowing() {
4866         return;
4867     }
4868     (*window).wm_requested.tiled = edges;
4869 }
4870 
4871 unsafe extern "C" fn window_get_decoration_above(
4872     client: *mut ffi::wl_client,
4873     resource: *mut ffi::wl_resource,
4874     id: u32,
4875     wl_surface: *mut ffi::wl_resource,
4876 ) {
4877     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4878     if window.is_null() {
4879         return;
4880     }
4881     let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
4882     let decoration = match Decoration::create(
4883         client,
4884         ffi::wl_resource_get_version(resource) as u32,
4885         id,
4886         wlr_surface,
4887         (*window).decorations_above_tree,
4888         window,
4889     ) {
4890         Ok(d) => d,
4891         Err(e) => {
4892             log::error!("Failed to create decoration: {}", e);
4893             ffi::wl_client_post_no_memory(client);
4894             return;
4895         }
4896     };
4897     let list_head = &mut (*window).decorations_above as *mut ffi::wl_list as *mut WlList;
4898     wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
4899 }
4900 
4901 unsafe extern "C" fn window_get_decoration_below(
4902     client: *mut ffi::wl_client,
4903     resource: *mut ffi::wl_resource,
4904     id: u32,
4905     wl_surface: *mut ffi::wl_resource,
4906 ) {
4907     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4908     if window.is_null() {
4909         return;
4910     }
4911     let wlr_surface = ffi::wlr_surface_from_resource(wl_surface);
4912     let decoration = match Decoration::create(
4913         client,
4914         ffi::wl_resource_get_version(resource) as u32,
4915         id,
4916         wlr_surface,
4917         (*window).decorations_below_tree,
4918         window,
4919     ) {
4920         Ok(d) => d,
4921         Err(e) => {
4922             log::error!("Failed to create decoration: {}", e);
4923             ffi::wl_client_post_no_memory(client);
4924             return;
4925         }
4926     };
4927     let list_head = &mut (*window).decorations_below as *mut ffi::wl_list as *mut WlList;
4928     wl_list_insert((*list_head).prev, &mut (*decoration).link as *mut ffi::wl_list as *mut WlList);
4929 }
4930 
4931 unsafe extern "C" fn window_inform_resize_start(
4932     _client: *mut ffi::wl_client,
4933     resource: *mut ffi::wl_resource,
4934 ) {
4935     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4936     if window.is_null() {
4937         return;
4938     }
4939     let server = (*window).server;
4940     if !(*server).wm.ensure_windowing() {
4941         return;
4942     }
4943     (*window).wm_requested.resizing = true;
4944 }
4945 
4946 unsafe extern "C" fn window_inform_resize_end(
4947     _client: *mut ffi::wl_client,
4948     resource: *mut ffi::wl_resource,
4949 ) {
4950     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4951     if window.is_null() {
4952         return;
4953     }
4954     let server = (*window).server;
4955     if !(*server).wm.ensure_windowing() {
4956         return;
4957     }
4958     (*window).wm_requested.resizing = false;
4959 }
4960 
4961 unsafe extern "C" fn window_set_capabilities(
4962     _client: *mut ffi::wl_client,
4963     resource: *mut ffi::wl_resource,
4964     caps: u32,
4965 ) {
4966     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4967     if window.is_null() {
4968         return;
4969     }
4970     let server = (*window).server;
4971     if !(*server).wm.ensure_windowing() {
4972         return;
4973     }
4974     (*window).wm_requested.capabilities = caps;
4975 }
4976 
4977 unsafe extern "C" fn window_inform_maximized(
4978     _client: *mut ffi::wl_client,
4979     resource: *mut ffi::wl_resource,
4980 ) {
4981     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4982     if window.is_null() {
4983         return;
4984     }
4985     let server = (*window).server;
4986     if !(*server).wm.ensure_windowing() {
4987         return;
4988     }
4989     (*window).wm_requested.maximized = true;
4990 }
4991 
4992 unsafe extern "C" fn window_inform_unmaximized(
4993     _client: *mut ffi::wl_client,
4994     resource: *mut ffi::wl_resource,
4995 ) {
4996     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
4997     if window.is_null() {
4998         return;
4999     }
5000     let server = (*window).server;
5001     if !(*server).wm.ensure_windowing() {
5002         return;
5003     }
5004     (*window).wm_requested.maximized = false;
5005 }
5006 
5007 unsafe extern "C" fn window_inform_fullscreen(
5008     _client: *mut ffi::wl_client,
5009     resource: *mut ffi::wl_resource,
5010 ) {
5011     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5012     if window.is_null() {
5013         return;
5014     }
5015     let server = (*window).server;
5016     if !(*server).wm.ensure_windowing() {
5017         return;
5018     }
5019     (*window).wm_requested.inform_fullscreen = true;
5020 }
5021 
5022 unsafe extern "C" fn window_inform_not_fullscreen(
5023     _client: *mut ffi::wl_client,
5024     resource: *mut ffi::wl_resource,
5025 ) {
5026     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5027     if window.is_null() {
5028         return;
5029     }
5030     let server = (*window).server;
5031     if !(*server).wm.ensure_windowing() {
5032         return;
5033     }
5034     (*window).wm_requested.inform_fullscreen = false;
5035 }
5036 
5037 unsafe extern "C" fn window_fullscreen(
5038     _client: *mut ffi::wl_client,
5039     resource: *mut ffi::wl_resource,
5040     output: *mut ffi::wl_resource,
5041 ) {
5042     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5043     if window.is_null() {
5044         return;
5045     }
5046     let server = (*window).server;
5047     if !(*server).wm.ensure_windowing() {
5048         return;
5049     }
5050     let out = if output.is_null() {
5051         std::ptr::null_mut()
5052     } else {
5053         let wlr_output = ffi::wlr_output_from_resource(output);
5054         if wlr_output.is_null() {
5055             std::ptr::null_mut()
5056         } else {
5057             ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output
5058         }
5059     };
5060     (*window).wm_requested.fullscreen = out;
5061 }
5062 
5063 unsafe extern "C" fn window_exit_fullscreen(
5064     _client: *mut ffi::wl_client,
5065     resource: *mut ffi::wl_resource,
5066 ) {
5067     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5068     if window.is_null() {
5069         return;
5070     }
5071     let server = (*window).server;
5072     if !(*server).wm.ensure_windowing() {
5073         return;
5074     }
5075     (*window).wm_requested.fullscreen = std::ptr::null_mut();
5076 }
5077 
5078 unsafe extern "C" fn window_set_clip_box(
5079     _client: *mut ffi::wl_client,
5080     resource: *mut ffi::wl_resource,
5081     x: i32,
5082     y: i32,
5083     width: i32,
5084     height: i32,
5085 ) {
5086     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5087     if window.is_null() {
5088         return;
5089     }
5090     let server = (*window).server;
5091     if !(*server).wm.ensure_rendering() {
5092         return;
5093     }
5094     if width < 0 || height < 0 {
5095         ffi::wl_resource_post_error(
5096             resource,
5097             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
5098             b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
5099         );
5100         return;
5101     }
5102     (*window).rendering_requested.clip = ffi::wlr_box {
5103         x,
5104         y,
5105         width,
5106         height,
5107     };
5108 }
5109 
5110 unsafe extern "C" fn window_set_content_clip_box(
5111     _client: *mut ffi::wl_client,
5112     resource: *mut ffi::wl_resource,
5113     x: i32,
5114     y: i32,
5115     width: i32,
5116     height: i32,
5117 ) {
5118     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5119     if window.is_null() {
5120         return;
5121     }
5122     let server = (*window).server;
5123     if !(*server).wm.ensure_rendering() {
5124         return;
5125     }
5126     if width < 0 || height < 0 {
5127         ffi::wl_resource_post_error(
5128             resource,
5129             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_CLIP_BOX,
5130             b"width/height must be greater than or equal to 0\0".as_ptr() as *const _,
5131         );
5132         return;
5133     }
5134     (*window).rendering_requested.content_clip = ffi::wlr_box {
5135         x,
5136         y,
5137         width,
5138         height,
5139     };
5140 }
5141 
5142 unsafe extern "C" fn window_set_dimension_bounds(
5143     _client: *mut ffi::wl_client,
5144     resource: *mut ffi::wl_resource,
5145     max_width: i32,
5146     max_height: i32,
5147 ) {
5148     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5149     if window.is_null() {
5150         return;
5151     }
5152     let server = (*window).server;
5153     if !(*server).wm.ensure_windowing() {
5154         return;
5155     }
5156     if max_width < 0 || max_height < 0 {
5157         ffi::wl_resource_post_error(
5158             resource,
5159             ffi::zcce_window_v1_error_ZCCE_WINDOW_V1_ERROR_INVALID_DIMENSIONS,
5160             b"dimensions must be greater than or equal to 0\0".as_ptr() as *const _,
5161         );
5162         return;
5163     }
5164     (*window).wm_requested.bounds = Dimensions {
5165         width: max_width as u32,
5166         height: max_height as u32,
5167     };
5168 }
5169 
5170 unsafe extern "C" fn window_set_opacity(
5171     _client: *mut ffi::wl_client,
5172     resource: *mut ffi::wl_resource,
5173     opacity: u32,
5174 ) {
5175     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5176     if window.is_null() {
5177         return;
5178     }
5179     let server = (*window).server;
5180     if !(*server).wm.ensure_rendering() {
5181         return;
5182     }
5183     let opacity_f32 = opacity as f32 / u32::MAX as f32;
5184     (*window).rendering_requested.opacity = opacity_f32;
5185 }
5186 
5187 unsafe extern "C" fn window_set_circular(
5188     _client: *mut ffi::wl_client,
5189     resource: *mut ffi::wl_resource,
5190     circular: u32,
5191 ) {
5192     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5193     if window.is_null() {
5194         return;
5195     }
5196     let server = (*window).server;
5197     if !(*server).wm.ensure_rendering() {
5198         return;
5199     }
5200     (*window).rendering_requested.circular = circular != 0;
5201 }
5202 
5203 unsafe extern "C" fn window_set_blur(
5204     _client: *mut ffi::wl_client,
5205     resource: *mut ffi::wl_resource,
5206     blur: u32,
5207 ) {
5208     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5209     if window.is_null() {
5210         return;
5211     }
5212     let server = (*window).server;
5213     if !(*server).wm.ensure_rendering() {
5214         return;
5215     }
5216     (*window).rendering_requested.blur = blur != 0;
5217 }
5218 
5219 // zcce_window_v1 implementation
5220 static WINDOW_INTERFACE: ffi::zcce_window_v1_interface = ffi::zcce_window_v1_interface {
5221     destroy: Some(window_destroy),
5222     close: Some(window_close),
5223     get_node: Some(window_get_node),
5224     propose_dimensions: Some(window_propose_dimensions),
5225     hide: Some(window_hide),
5226     show: Some(window_show),
5227     use_csd: Some(window_use_csd),
5228     use_ssd: Some(window_use_ssd),
5229     set_borders: Some(window_set_borders),
5230     set_tiled: Some(window_set_tiled),
5231     get_decoration_above: Some(window_get_decoration_above),
5232     get_decoration_below: Some(window_get_decoration_below),
5233     inform_resize_start: Some(window_inform_resize_start),
5234     inform_resize_end: Some(window_inform_resize_end),
5235     set_capabilities: Some(window_set_capabilities),
5236     inform_maximized: Some(window_inform_maximized),
5237     inform_unmaximized: Some(window_inform_unmaximized),
5238     inform_fullscreen: Some(window_inform_fullscreen),
5239     inform_not_fullscreen: Some(window_inform_not_fullscreen),
5240     fullscreen: Some(window_fullscreen),
5241     exit_fullscreen: Some(window_exit_fullscreen),
5242     set_clip_box: Some(window_set_clip_box),
5243     set_content_clip_box: Some(window_set_content_clip_box),
5244     set_dimension_bounds: Some(window_set_dimension_bounds),
5245     set_opacity: Some(window_set_opacity),
5246     set_circular: Some(window_set_circular),
5247     set_blur: Some(window_set_blur),
5248 };
5249 
5250 static INERT_WINDOW_INTERFACE: ffi::zcce_window_v1_interface = ffi::zcce_window_v1_interface {
5251     destroy: Some(window_destroy),
5252     close: None,
5253     get_node: None,
5254     propose_dimensions: None,
5255     hide: None,
5256     show: None,
5257     use_csd: None,
5258     use_ssd: None,
5259     set_borders: None,
5260     set_tiled: None,
5261     get_decoration_above: None,
5262     get_decoration_below: None,
5263     inform_resize_start: None,
5264     inform_resize_end: None,
5265     set_capabilities: None,
5266     inform_maximized: None,
5267     inform_unmaximized: None,
5268     inform_fullscreen: None,
5269     inform_not_fullscreen: None,
5270     fullscreen: None,
5271     exit_fullscreen: None,
5272     set_clip_box: None,
5273     set_content_clip_box: None,
5274     set_dimension_bounds: None,
5275     set_opacity: None,
5276     set_circular: None,
5277     set_blur: None,
5278 };
5279 
5280 unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
5281     let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
5282     if !window.is_null() {
5283         if (*window).object != resource {
5284             return;
5285         }
5286         (*window).object = std::ptr::null_mut();
5287         (*window).node.make_inert();
5288         
5289         for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
5290             let list_head = decorations as *mut WlList;
5291             let mut curr = (*list_head).next;
5292             while curr != list_head {
5293                 let next = (*curr).next;
5294                 let dec = crate::container_of!(curr, Decoration, link);
5295                 (*dec).make_inert();
5296                 curr = next;
5297             }
5298         }
5299     }
5300 }
5301 
5302 // zcce_decoration_v1 implementation
5303 pub struct DecorationRenderingRequested {
5304     pub offset_x: i32,
5305     pub offset_y: i32,
5306     pub sync_next_commit: bool,
5307     pub blur: bool,
5308 }
5309 
5310 pub struct Decoration {
5311     pub object: *mut ffi::wl_resource, // zcce_decoration_v1
5312     pub surface: *mut ffi::wlr_surface,
5313     pub tree: *mut ffi::wlr_scene_tree,
5314     pub surfaces: crate::scene::SaveableSurfaces,
5315     pub link: ffi::wl_list,
5316     pub window: *mut Window,
5317     pub rendering_requested: DecorationRenderingRequested,
5318 }
5319 
5320 impl Decoration {
5321     pub unsafe fn create(
5322         client: *mut ffi::wl_client,
5323         version: u32,
5324         id: u32,
5325         surface: *mut ffi::wlr_surface,
5326         parent: *mut ffi::wlr_scene_tree,
5327         window: *mut Window,
5328     ) -> Result<*mut Self, &'static str> {
5329         let decoration_v1 = ffi::wl_resource_create(client, &ffi::zcce_decoration_v1_interface, version as i32, id);
5330         if decoration_v1.is_null() {
5331             ffi::wl_client_post_no_memory(client);
5332             return Err("wl_resource_create failed");
5333         }
5334 
5335         if !ffi::wlr_surface_set_role(
5336             surface,
5337             &raw const DECORATION_ROLE,
5338             decoration_v1,
5339             ffi::zcce_window_manager_v1_error_ZCCE_WINDOW_MANAGER_V1_ERROR_ROLE,
5340         ) {
5341             return Err("wlr_surface_set_role failed");
5342         }
5343         ffi::river_wlr_surface_set_role_object(surface, decoration_v1);
5344 
5345         let tree = ffi::wlr_scene_tree_create(parent);
5346         if tree.is_null() {
5347             return Err("wlr_scene_tree_create failed");
5348         }
5349 
5350         let surfaces = crate::scene::SaveableSurfaces::init(tree)?;
5351         let subsurface_tree = ffi::wlr_scene_subsurface_tree_create(surfaces.tree, surface);
5352         if subsurface_tree.is_null() {
5353             ffi::wlr_scene_node_destroy(tree as *mut ffi::wlr_scene_node);
5354             return Err("wlr_scene_subsurface_tree_create failed");
5355         }
5356 
5357         let dec = Box::new(Decoration {
5358             object: decoration_v1,
5359             surface,
5360             tree,
5361             surfaces,
5362             link: std::mem::zeroed(),
5363             window,
5364             rendering_requested: DecorationRenderingRequested {
5365                 offset_x: 0,
5366                 offset_y: 0,
5367                 sync_next_commit: false,
5368                 blur: false,
5369             },
5370         });
5371         let raw = Box::into_raw(dec);
5372 
5373         ffi::wl_resource_set_implementation(
5374             decoration_v1,
5375             &DECORATION_INTERFACE as *const _ as *const _,
5376             raw as *mut _,
5377             Some(handle_dec_destroy_resource),
5378         );
5379 
5380         Ok(raw)
5381     }
5382 
5383     pub unsafe fn destroy(&mut self) {
5384         assert!(self.object.is_null());
5385         ffi::wlr_scene_node_destroy(self.tree as *mut ffi::wlr_scene_node);
5386         wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
5387         let _ = Box::from_raw(self);
5388     }
5389 
5390     pub unsafe fn make_inert(&mut self) {
5391         if !self.object.is_null() {
5392             ffi::wl_resource_set_implementation(
5393                 self.object,
5394                 &INERT_DECORATION_INTERFACE as *const _ as *const _,
5395                 std::ptr::null_mut(),
5396                 None,
5397             );
5398             self.object = std::ptr::null_mut();
5399         }
5400         if !self.surface.is_null() {
5401             ffi::river_wlr_surface_set_role_object(self.surface, std::ptr::null_mut());
5402         }
5403         self.surfaces.save();
5404     }
5405 
5406     pub unsafe fn render_finish(&mut self, _window_clip: *const ffi::wlr_box) {
5407         if self.rendering_requested.sync_next_commit {
5408             self.rendering_requested.sync_next_commit = false;
5409 
5410             if !self.surfaces.saved {
5411                 if !self.object.is_null() {
5412                     ffi::wl_resource_post_error(
5413                         self.object,
5414                         ffi::zcce_decoration_v1_error_ZCCE_DECORATION_V1_ERROR_NO_COMMIT,
5415                         b"no wl_surface.commit after sync_next_commit and before update_rendering_finish\0".as_ptr() as *const _,
5416                     );
5417                 }
5418             }
5419         }
5420 
5421         self.surfaces.drop_saved();
5422 
5423         let server = (*self.window).server;
5424         let app_id = (*self.window).get_app_id_string().unwrap_or_default();
5425         let mut ignore_transparent = (*server).wm.layout.window_backdrop_blur_ignore_transparent;
5426         let is_status = (*self.window).tiling_mode == crate::tiling::TilingMode::Status ||
5427                         app_id.starts_with("cce-status");
5428         if is_status {
5429             ignore_transparent = (*server).wm.layout.status_backdrop_blur_ignore_transparent;
5430         }
5431         let is_decorated = (*server).wm.is_decorated_app(&app_id);
5432         let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_decorated || is_status) && !(*self.window).droplet_backdrop_on();
5433         // Radius 0 preserves existing behaviour on the layer-surface path (see layer_shell.rs)
5434         // — it never had a blur radius applied, and this fix is scoped to toplevels.
5435         ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0, 0);
5436 
5437         let scale = (*self.window).scale;
5438         let scaled_x = (self.rendering_requested.offset_x as f64 * scale) as i32;
5439         let scaled_y = (self.rendering_requested.offset_y as f64 * scale) as i32;
5440         ffi::river_scene_node_set_position_if_changed(self.tree as *mut ffi::wlr_scene_node, scaled_x, scaled_y);
5441 
5442         struct ScaleData {
5443             scale: f64,
5444             ancestor: *mut ffi::wlr_scene_node,
5445         }
5446 
5447         unsafe extern "C" fn set_overview_scale_iterator(
5448             buffer: *mut ffi::wlr_scene_buffer,
5449             sx: i32,
5450             sy: i32,
5451             user_data: *mut std::ffi::c_void,
5452         ) {
5453             let data = &*(user_data as *const ScaleData);
5454             let node = buffer as *mut ffi::wlr_scene_node;
5455 
5456             let surface = ffi::river_scene_node_get_surface(node);
5457             if !surface.is_null() {
5458                 let w = ffi::river_wlr_surface_get_width(surface);
5459                 let h = ffi::river_wlr_surface_get_height(surface);
5460                 if data.scale == 1.0 {
5461                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
5462                     ffi::river_scene_node_set_position_if_changed(node, 0, 0);
5463                 } else {
5464                     let dest_w = (w as f64 * data.scale).round() as i32;
5465                     let dest_h = (h as f64 * data.scale).round() as i32;
5466                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
5467 
5468                     let (px, py) = get_parent_position_relative_to(node, data.ancestor);
5469                     let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
5470                     let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
5471                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
5472                 }
5473                 // Keep the opaque region in step with the dest scale —
5474                 // unscaled it covers the shrunken node's translucent CSD
5475                 // margins and occlusion culling stops repainting behind
5476                 // the client shadow (stale pixels show through it).
5477                 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
5478             }
5479             // Non-surface buffers are frozen SAVED copies (see
5480             // save_surface_tree_iter): their natural buffer size is
5481             // meaningless for geometry — HiDPI clients commit scale-N
5482             // buffers and Chromium pads buffers beyond the surface,
5483             // cropping via viewport src — so rescaling from it ballooned
5484             // ghosts around the window at any zoom change. A frozen copy
5485             // keeps its save-time dest/position; a zoom mid-transaction
5486             // leaves it briefly at the old zoom, which restore corrects.
5487         }
5488 
5489         let scale_data = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
5490         ffi::wlr_scene_node_for_each_buffer(
5491             self.surfaces.tree as *mut ffi::wlr_scene_node,
5492             Some(set_overview_scale_iterator),
5493             &scale_data as *const ScaleData as *mut std::ffi::c_void,
5494         );
5495 
5496         if self.surfaces.saved {
5497             let scale_data_saved = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
5498             ffi::wlr_scene_node_for_each_buffer(
5499                 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
5500                 Some(set_overview_scale_iterator),
5501                 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
5502             );
5503         }
5504 
5505         let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
5506         if (*children_head).next != children_head {
5507             ffi::wlr_scene_subsurface_tree_set_clip(self.surfaces.tree as *mut ffi::wlr_scene_node, std::ptr::null());
5508         }
5509     }
5510 
5511     pub unsafe fn scale_only_render_finish(&mut self) {
5512         let scale = (*self.window).scale;
5513         if scale * (*self.window).x11_buffer_scale() == 1.0 {
5514             return;
5515         }
5516 
5517         struct ScaleData {
5518             scale: f64,
5519             ancestor: *mut ffi::wlr_scene_node,
5520         }
5521 
5522         unsafe extern "C" fn set_overview_scale_iterator(
5523             buffer: *mut ffi::wlr_scene_buffer,
5524             sx: i32,
5525             sy: i32,
5526             user_data: *mut std::ffi::c_void,
5527         ) {
5528             let data = &*(user_data as *const ScaleData);
5529             let node = buffer as *mut ffi::wlr_scene_node;
5530 
5531             let surface = ffi::river_scene_node_get_surface(node);
5532             if !surface.is_null() {
5533                 let w = ffi::river_wlr_surface_get_width(surface);
5534                 let h = ffi::river_wlr_surface_get_height(surface);
5535                 if data.scale == 1.0 {
5536                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, w, h);
5537                     ffi::river_scene_node_set_position_if_changed(node, 0, 0);
5538                 } else {
5539                     let dest_w = (w as f64 * data.scale).round() as i32;
5540                     let dest_h = (h as f64 * data.scale).round() as i32;
5541                     ffi::river_scene_buffer_set_dest_size_if_changed(buffer, dest_w, dest_h);
5542 
5543                     let (px, py) = get_parent_position_relative_to(node, data.ancestor);
5544                     let dest_x = (px as f64 * (data.scale - 1.0)) as i32;
5545                     let dest_y = (py as f64 * (data.scale - 1.0)) as i32;
5546                     ffi::river_scene_node_set_position_if_changed(node, dest_x, dest_y);
5547                 }
5548                 // Keep the opaque region in step with the dest scale —
5549                 // unscaled it covers the shrunken node's translucent CSD
5550                 // margins and occlusion culling stops repainting behind
5551                 // the client shadow (stale pixels show through it).
5552                 ffi::river_scene_buffer_set_scaled_opaque_region(buffer, surface, data.scale);
5553             }
5554             // Non-surface buffers are frozen SAVED copies (see
5555             // save_surface_tree_iter): their natural buffer size is
5556             // meaningless for geometry — HiDPI clients commit scale-N
5557             // buffers and Chromium pads buffers beyond the surface,
5558             // cropping via viewport src — so rescaling from it ballooned
5559             // ghosts around the window at any zoom change. A frozen copy
5560             // keeps its save-time dest/position; a zoom mid-transaction
5561             // leaves it briefly at the old zoom, which restore corrects.
5562         }
5563 
5564         let scale_data = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.tree as *mut ffi::wlr_scene_node };
5565         ffi::wlr_scene_node_for_each_buffer(
5566             self.surfaces.tree as *mut ffi::wlr_scene_node,
5567             Some(set_overview_scale_iterator),
5568             &scale_data as *const ScaleData as *mut std::ffi::c_void,
5569         );
5570 
5571         if self.surfaces.saved {
5572             let scale_data_saved = ScaleData { scale: scale * (*self.window).x11_buffer_scale(), ancestor: self.surfaces.saved_tree as *mut ffi::wlr_scene_node };
5573             ffi::wlr_scene_node_for_each_buffer(
5574                 self.surfaces.saved_tree as *mut ffi::wlr_scene_node,
5575                 Some(set_overview_scale_iterator),
5576                 &scale_data_saved as *const ScaleData as *mut std::ffi::c_void,
5577             );
5578         }
5579     }
5580 }
5581 
5582 pub unsafe fn decoration_from_wlr_surface(surface: *mut ffi::wlr_surface) -> *mut Decoration {
5583     if surface.is_null() {
5584         return std::ptr::null_mut();
5585     }
5586     let role_ptr = ffi::river_wlr_surface_get_role(surface);
5587     if role_ptr != &raw const DECORATION_ROLE {
5588         return std::ptr::null_mut();
5589     }
5590     let resource = ffi::river_wlr_surface_get_role_resource(surface);
5591     if resource.is_null() {
5592         return std::ptr::null_mut();
5593     }
5594     ffi::wl_resource_get_user_data(resource) as *mut Decoration
5595 }
5596 
5597 unsafe extern "C" fn dec_client_commit(surface: *mut ffi::wlr_surface) {
5598     let dec = decoration_from_wlr_surface(surface);
5599     if dec.is_null() {
5600         return;
5601     }
5602     if (*dec).rendering_requested.sync_next_commit {
5603         (*dec).surfaces.save();
5604     }
5605 }
5606 
5607 unsafe extern "C" fn dec_commit(surface: *mut ffi::wlr_surface) {
5608     if ffi::wlr_surface_has_buffer(surface) {
5609         ffi::wlr_surface_map(surface);
5610     }
5611 }
5612 
5613 unsafe extern "C" fn dec_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
5614     ffi::wl_resource_destroy(resource);
5615 }
5616 
5617 unsafe extern "C" fn dec_set_offset(
5618     _client: *mut ffi::wl_client,
5619     resource: *mut ffi::wl_resource,
5620     x: i32,
5621     y: i32,
5622 ) {
5623     let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5624     if dec.is_null() {
5625         return;
5626     }
5627     let server = (*(*dec).window).server;
5628     if !(*server).wm.ensure_rendering() {
5629         return;
5630     }
5631     (*dec).rendering_requested.offset_x = x;
5632     (*dec).rendering_requested.offset_y = y;
5633 }
5634 
5635 unsafe extern "C" fn dec_sync_next_commit(
5636     _client: *mut ffi::wl_client,
5637     resource: *mut ffi::wl_resource,
5638 ) {
5639     let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5640     if dec.is_null() {
5641         return;
5642     }
5643     let server = (*(*dec).window).server;
5644     if !(*server).wm.ensure_rendering() {
5645         return;
5646     }
5647     (*dec).rendering_requested.sync_next_commit = true;
5648 }
5649 
5650 unsafe extern "C" fn dec_set_blur(
5651     _client: *mut ffi::wl_client,
5652     resource: *mut ffi::wl_resource,
5653     blur: u32,
5654 ) {
5655     let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5656     if dec.is_null() {
5657         return;
5658     }
5659     let server = (*(*dec).window).server;
5660     if !(*server).wm.ensure_rendering() {
5661         return;
5662     }
5663     (*dec).rendering_requested.blur = blur != 0;
5664 }
5665 
5666 static DECORATION_INTERFACE: ffi::zcce_decoration_v1_interface = ffi::zcce_decoration_v1_interface {
5667     destroy: Some(dec_destroy),
5668     set_offset: Some(dec_set_offset),
5669     sync_next_commit: Some(dec_sync_next_commit),
5670     set_blur: Some(dec_set_blur),
5671 };
5672 
5673 static INERT_DECORATION_INTERFACE: ffi::zcce_decoration_v1_interface = ffi::zcce_decoration_v1_interface {
5674     destroy: Some(dec_destroy),
5675     set_offset: None,
5676     sync_next_commit: None,
5677     set_blur: None,
5678 };
5679 
5680 unsafe extern "C" fn handle_dec_destroy_resource(resource: *mut ffi::wl_resource) {
5681     let dec = ffi::wl_resource_get_user_data(resource) as *mut Decoration;
5682     if !dec.is_null() {
5683         ffi::river_wlr_surface_set_role_object((*dec).surface, std::ptr::null_mut());
5684         (*dec).object = std::ptr::null_mut();
5685         (*dec).destroy();
5686     }
5687 }
5688 
5689 unsafe extern "C" fn dec_role_destroy(surface: *mut ffi::wlr_surface) {
5690     let dec = decoration_from_wlr_surface(surface);
5691     if dec.is_null() {
5692         return;
5693     }
5694     ffi::river_wlr_surface_set_role_object(surface, std::ptr::null_mut());
5695     if !(*dec).object.is_null() {
5696         ffi::wl_resource_set_user_data((*dec).object, std::ptr::null_mut());
5697         ffi::wl_resource_destroy((*dec).object);
5698         (*dec).object = std::ptr::null_mut();
5699     }
5700     (*dec).destroy();
5701 }
5702 
5703 #[no_mangle]
5704 pub static mut DECORATION_ROLE: ffi::wlr_surface_role = ffi::wlr_surface_role {
5705     name: b"zcce_decoration_v1\0".as_ptr() as *const _,
5706     no_object: false,
5707     client_commit: Some(dec_client_commit),
5708     commit: Some(dec_commit),
5709     map: None,
5710     unmap: None,
5711     destroy: Some(dec_role_destroy),
5712 };
5713 
5714 /// A surface buffer's visible extent for the scaling passes: `(width,
5715 /// height, x, y)` — the subsurface clip when one is set (the xdg geometry,
5716 /// see `apply_surface_clip`), placed where wlroots puts the cropped content
5717 /// in its parent, else the whole surface at the origin. wlroots re-derives
5718 /// dest size and position from the clip on every commit; a pass that
5719 /// overrides them from the full surface size stretches the crop back out.
5720 unsafe fn surface_buffer_extent(
5721     buffer: *mut ffi::wlr_scene_buffer,
5722     surface: *mut ffi::wlr_surface,
5723 ) -> (i32, i32, i32, i32) {
5724     let mut clip = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
5725     if ffi::river_scene_buffer_get_surface_clip(buffer, &mut clip) {
5726         (clip.width, clip.height, clip.x, clip.y)
5727     } else {
5728         (
5729             ffi::river_wlr_surface_get_width(surface),
5730             ffi::river_wlr_surface_get_height(surface),
5731             0,
5732             0,
5733         )
5734     }
5735 }
5736 
5737 unsafe fn get_parent_position_relative_to(
5738     node: *mut ffi::wlr_scene_node,
5739     ancestor: *mut ffi::wlr_scene_node,
5740 ) -> (i32, i32) {
5741     let mut x = 0;
5742     let mut y = 0;
5743     if !node.is_null() {
5744         let mut curr = ffi::river_scene_node_get_parent(node) as *mut ffi::wlr_scene_node;
5745         while !curr.is_null() && curr != ancestor {
5746             x += ffi::river_scene_node_get_x(curr);
5747             y += ffi::river_scene_node_get_y(curr);
5748             curr = ffi::river_scene_node_get_parent(curr) as *mut ffi::wlr_scene_node;
5749         }
5750     }
5751     (x, y)
5752 }
5753 
5754 unsafe fn wl_listener_remove_safe(listener: *mut ffi::wl_listener) {
5755     let prev = (*listener).link.prev;
5756     let next = (*listener).link.next;
5757     if !prev.is_null() && !next.is_null() && prev != listener as *mut ffi::wl_list && next != listener as *mut ffi::wl_list {
5758         ffi::wl_list_remove(&mut (*listener).link);
5759         (*listener).link.prev = std::ptr::null_mut();
5760         (*listener).link.next = std::ptr::null_mut();
5761     }
5762 }
5763 
5764 unsafe extern "C" fn handle_window_commit(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
5765     let window = crate::container_of!(listener, Window, commit);
5766     (*window).stream_dirty = true;
5767     let was_status = (*window).is_status_bar();
5768     // An X11 client committing under a left/top-edge drag: anchor on the
5769     // size it just committed, ahead of the render_finish below that places
5770     // the tree at `rendering_requested`. (xdg toplevels do the same in their
5771     // own commit handler, where the toplevel geometry is the authority.)
5772     //
5773     // The committed surface is PHYSICAL pixels — under `xwayland_hidpi` twice
5774     // the logical box, as the scale pass below says — while
5775     // `anchor_resize_commit` works in box_geom's logical units. Convert first,
5776     // and take the wine frame off after, the way `render_finish` reads the
5777     // xsurface size back. Feeding it the raw buffer width put the origin a
5778     // whole window-width to the left and made it track the pointer at double
5779     // speed, every X11 left/top drag at scale 2.
5780     if let WindowImpl::Xwayland(xwindow) = (*window).impl_type {
5781         if !xwindow.is_null() && !(*xwindow).xsurface.is_null() && (*window).resize_edges.is_some() {
5782             let surface = (*(*xwindow).xsurface).surface;
5783             if !surface.is_null() {
5784                 let s = crate::xwayland_window::x11_scale_for((*window).server, (*xwindow).xsurface);
5785                 let mut w = crate::xwayland_window::from_x11(
5786                     ffi::river_wlr_surface_get_width(surface), s);
5787                 let mut h = crate::xwayland_window::from_x11(
5788                     ffi::river_wlr_surface_get_height(surface), s);
5789                 let has_parent = !(*(*xwindow).xsurface).parent.is_null();
5790                 if (*window).is_wine() && !has_parent && !(*window).is_fullscreen() {
5791                     w = (w - crate::xwayland_window::WINE_MARGIN * 2).max(0);
5792                     h = (h - crate::xwayland_window::WINE_MARGIN * 2).max(0);
5793                 }
5794                 (*window).anchor_resize_commit(w, h);
5795             }
5796         }
5797     }
5798     (*window).render_finish();
5799     // The scene's own commit handler (registered before this one, so it has
5800     // already run) resets the committed buffer's dest size to natural. For
5801     // an X11 surface under xwayland_hidpi that is the physical size — twice
5802     // the logical box — and until the per-frame pass restores it every
5803     // pointer event hit-tests through the unscaled buffer and reaches the
5804     // client at HALF its coordinates. Houdini repaints on every hover
5805     // change, so hover flickered: each repaint opened the gap, the next
5806     // motion event landed elsewhere, the widget un-hovered, repeat.
5807     if (*window).x11_buffer_scale() != 1.0 {
5808         (*window).scale_only_render_finish();
5809     }
5810     // A status segment that changed size needs the bar re-arranged around
5811     // it. One that merely repainted (the clock, once a second; the cpu
5812     // meter) does not — and this used to dirty on every commit, which made
5813     // the status bar alone run a full manage/arrange/render transaction for
5814     // each of its ticks, all day. Compare the committed surface size against
5815     // the last commit's; the xdg commit handler tracks box_geom the same way.
5816     if was_status {
5817         let surface = (*window).root_surface();
5818         if !surface.is_null() {
5819             let size = (
5820                 ffi::river_wlr_surface_get_width(surface),
5821                 ffi::river_wlr_surface_get_height(surface),
5822             );
5823             if size != (*window).status_commit_size {
5824                 (*window).status_commit_size = size;
5825                 (*(*window).server).wm.dirty_windowing();
5826             }
5827         } else {
5828             (*(*window).server).wm.dirty_windowing();
5829         }
5830     }
5831 }
5832 
5833 #[cfg(test)]
5834 mod handle_disc_tests {
5835     use super::*;
5836 
5837     #[test]
5838     fn discs_follow_border_element_order_and_stay_inside() {
5839         let (c, r) = handle_disc_layout(400.0, 300.0, 0.0, 32.0);
5840         assert_eq!(r, 16.0);
5841         assert_eq!(c[BorderElement::Top.index()], (200.0, 16.0));
5842         assert_eq!(c[BorderElement::Bottom.index()], (200.0, 284.0));
5843         assert_eq!(c[BorderElement::Left.index()], (16.0, 150.0));
5844         assert_eq!(c[BorderElement::Right.index()], (384.0, 150.0));
5845         assert_eq!(c[BorderElement::TopLeft.index()], (16.0, 16.0));
5846         assert_eq!(c[BorderElement::BottomRight.index()], (384.0, 284.0));
5847         for &(x, y) in &c {
5848             assert!(x - r >= 0.0 && x + r <= 400.0 && y - r >= 0.0 && y + r <= 300.0);
5849         }
5850     }
5851 
5852     #[test]
5853     fn corner_disc_is_tangent_to_a_wider_corner_arc() {
5854         let (c, r) = handle_disc_layout(400.0, 300.0, 40.0, 32.0);
5855         let (tx, ty) = c[BorderElement::TopLeft.index()];
5856         assert_eq!(tx, ty);
5857         // Distance from the arc centre (40, 40) plus the disc radius is the
5858         // arc radius: tangent from the inside.
5859         let d = ((tx - 40.0).powi(2) + (ty - 40.0).powi(2)).sqrt();
5860         assert!((d + r - 40.0).abs() < 1e-9);
5861     }
5862 }