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

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