use std::borrow::Cow;
use std::cell::{RefCell, RefMut};
use std::mem;
use std::path::PathBuf;
use std::rc::{Rc, Weak};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, UNIX_EPOCH};

use forge_ipc::{AppIdMatcher, WindowScale};
use smithay::backend::drm::DrmEventMetadata;
use smithay::backend::renderer::element::{Element, RenderElementStates};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{DisplayHandle, Resource};
use smithay::utils::{Logical, Point, Rectangle};
use smithay::wayland::compositor;
use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
use smithay::wayland::session_lock::LockSurface;
use smithay::wayland::shell::wlr_layer::{
    KeyboardInteractivity, Layer, LayerSurface, LayerSurfaceCachedState,
};
use smithay::wayland::shell::xdg::{PopupSurface, ToplevelSurface};

use crate::drawing::{ForgeElement, Graphics, RenderTexture, Texture};
use crate::forge::Forge;
use crate::input::{HandleGesture, TouchState};
use crate::layer::Layers;
use crate::orientation::Orientation;
#[cfg(feature = "winit")]
use smithay::output::Mode;
use crate::output::{Canvas, Output, GESTURE_HANDLE_HEIGHT};
use crate::overview::{DragActionType, DragAndDrop, Overview};
use crate::windows::layout::{LayoutPosition, Layouts};
use crate::windows::surface::{ForgeLayerSurface, InputSurface, InputSurfaceKind, Surface};
use crate::windows::window::Window;

pub mod focus;
pub mod layout;
pub mod surface;
pub mod window;

use crate::windows::focus::FocusTarget;

const MAX_LOCKED_TRANSACTION_MILLIS: u64 = 100;
const MAX_TRANSACTION_MILLIS: u64 = 1000;

static TRANSACTION_START: AtomicU64 = AtomicU64::new(0);

pub fn start_transaction() {
    if TRANSACTION_START.load(Ordering::Relaxed) != 0 {
        return;
    }

    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
    TRANSACTION_START.store(now, Ordering::Relaxed);
}

macro_rules! with_all_windows {
    ($windows:expr, | $window:tt | $fn:expr) => {{
        match $windows.pending_view() {
            View::Lock(Some($window)) => $fn,
            _ => (),
        }

        for $window in $windows.layouts.windows() {
            $fn
        }
        for $window in $windows.layers.iter() {
            $fn
        }
    }};
}

macro_rules! with_all_windows_mut {
    ($windows:expr, | $window:tt | $fn:expr) => {{
        match $windows.pending_view_mut() {
            View::Lock(Some($window)) => $fn,
            _ => (),
        }

        for mut $window in $windows.layouts.windows_mut() {
            $fn
        }
        for $window in $windows.layers.iter_mut() {
            $fn
        }
    }};
}

#[derive(Debug)]
pub struct Windows {
    pub window_scales: Vec<(AppIdMatcher, WindowScale)>,

    /// Regex patterns matched against the toplevel `app_id` to force CSD on
    /// the rendering side. The xdg-decoration protocol covers clients that
    /// participate; this list is the user's safety valve for clients that
    /// paint their own title bar without binding the protocol (and would
    /// otherwise end up with two stacked bars). Loaded from
    /// `forge.toml -> decoration.csd_apps`. See the matching SSD render
    /// guard in `Layouts::textures_and_ssd`.
    pub csd_apps: Vec<AppIdMatcher>,

    orphan_popups: Vec<Window<PopupSurface>>,
    layouts: Layouts,
    layers: Layers,
    view: View,

    event_loop: LoopHandle<'static, Forge>,

    /// Single source of truth for input focus. Keyboard focus and the xdg
    /// `Activated` state are both derived from this each frame in
    /// [`Self::focus`], so they cannot drift apart. `None` falls back to
    /// the active layout's window.
    focus_target: Option<FocusTarget>,

    /// Reconciliation cache: the toplevel currently carrying the xdg
    /// `Activated` state. Driven by [`Self::focus`]; never set directly.
    activated: Option<ToplevelSurface>,
    transaction: Option<Transaction>,
    textures: Vec<ForgeElement>,
    start_time: Instant,
    output: Output,

    canvas: Canvas,
    unlocked_orientation: Orientation,
    orientation_locked: bool,
    dirty: bool,

    /// Last known compositor-space pointer position. Updated on every
    /// pointer motion via `pointer_moved`. Read at render time so the
    /// SSD title-bar can hit-test which window-control button (if any)
    /// is hovered and paint it with the theme's hover surface — without
    /// this, the close-button red-on-hover effect never fires because
    /// forge's SSD input pipeline is geometric and doesn't pump events
    /// into ltk's `UiSurface`.
    pointer_pos: Option<Point<f64, Logical>>,
    /// Identity of the SSD button under the cursor on the previous
    /// motion event, as `(window-rc-pointer, action)`. Used by
    /// `pointer_moved` to mark the compositor dirty only when crossing
    /// a button boundary, so plain motion across the desktop does not
    /// thrash the render loop. The window pointer is a raw address, not
    /// dereferenced — only compared for equality.
    last_ssd_hover: Option<(usize, crate::ssd::SsdAction)>,

    wallpaper: Option<RenderTexture>,
    wallpaper_loaded: bool,
    wallpaper_scale: f64,

    /// Most-recently-used window order by globally-unique `ObjectId`.
    /// Front of the list = most recent. Used by the Alt+Tab switcher.
    /// `ObjectId` is used (not `protocol_id`) because `protocol_id` is
    /// per-client: two unrelated clients can each have a surface whose
    /// `protocol_id()` returns the same `u32`, causing the wrong window
    /// to be skipped or misplaced in the switcher.
    mru: Vec<smithay::reexports::wayland_server::backend::ObjectId>,

    /// In-progress title-bar drag (desktop mode). `None` when no drag is
    /// active.
    move_drag: Option<MoveDrag>,

    move_drag_x11: Option<MoveDragX11>,

    /// In-progress edge-handle resize drag.
    resize_drag: Option<ResizeDrag>,

    resize_drag_x11: Option<ResizeDragX11>,

    /// Next offset to apply when spawning a new floating window, so that
    /// identical defaults cascade diagonally instead of stacking.
    next_cascade: i32,

    /// Windows whose toplevel was destroyed but that are still on screen,
    /// fading out for the close animation. Each frame their snapshot is
    /// rendered with a shrink+fade transform until the timer expires, then
    /// the entry drops on `tick_closing_animations`.
    closing_windows: Vec<ClosingWindow>,

    /// Mapped X11Surfaces. Populated from the `XwmHandler` impl on `Forge`,
    /// consumed in `import_buffers` and the textures pass.
    pub x11_windows: crate::x11_window::X11Windows,

    toplevel_z_order: Vec<WlSurface>,

    /// Per-app rect + maximized snapshot persisted to disk. Saved on
    /// every drag-end / maximize toggle for the focused toplevel,
    /// consulted in `surface_commit` to seed a freshly-mapped floating
    /// window with the geometry the user last left it at (eydos #7).
    geometry_store: crate::geometry_store::GeometryStore,
}

/// Tombstone for the close animation. Captured *before* `Layouts::reap`
/// drops the window, so the texture clone survives even though the
/// originating wl_surface has been destroyed.
#[derive(Debug)]
struct ClosingWindow {
    /// Last-rendered texture from the destroyed surface. `RenderTexture`
    /// is `Rc<Texture>` so cloning it is a refcount bump and the GL handle
    /// is kept alive for as long as this entry lives.
    texture:     crate::drawing::RenderTexture,
    /// On-screen rect (logical px) at the moment of destruction. Anchors
    /// the shrink so the animation starts from where the window was sitting.
    target_rect: Rectangle<i32, Logical>,
    start:       Instant,
    duration:    Duration,
}

/// Total duration of the close animation. Short enough that the user does
/// not perceive it as a delay between clicking close and the slot freeing
/// up, long enough that the eye registers the shrink.
const CLOSE_ANIM_MS: u64 = 160;

#[derive(Debug)]
pub struct MoveDragX11 {
    pub wl: WlSurface,
    pub start_pointer: Point<f64, Logical>,
    pub start_loc: Point<i32, Logical>,
}

#[derive(Debug)]
pub struct MoveDrag {
    pub window: Weak<RefCell<Window>>,
    pub start_pointer: Point<f64, Logical>,
    pub start_loc: Point<i32, Logical>,
    /// Snap zone the cursor is currently hovering, if any. `None` means
    /// "release here drops the window in place"; `Some(zone)` means
    /// "release here resizes / moves the window to the zone's rect".
    /// Updated on every `update_move_drag` and consumed by
    /// `end_move_drag`.
    pub snap: Option<SnapZone>,
}

/// Edge-snap target detected from the live drag cursor. Mirrors the
/// GNOME / KWin / Windows convention: top edge maximises, side edges
/// fill the matching half, corners fill the matching quarter. `Maximize`
/// is kept distinct from a hypothetical "full screen" so the un-snap
/// path can route through the existing `pre_max_rect` book-keeping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapZone {
    Maximize,
    Left,
    Right,
    NW,
    NE,
    SW,
    SE,
}

/// Pixel-distance from the screen edge that arms a snap. Generous
/// enough that a casual flick toward the edge counts; small enough that
/// users dragging a window to the side without intending to snap have
/// space to release in.
const SNAP_EDGE_THRESHOLD: f64 = 12.0;
/// Side length of the corner zone — a square in each output corner.
/// Slightly larger than [`SNAP_EDGE_THRESHOLD`] so corners take
/// precedence near the corner regions even if the cursor strays a bit.
const SNAP_CORNER_THRESHOLD: f64 = 80.0;

impl SnapZone {
    /// Detect the snap zone matching `pointer` against the available
    /// output rect, or return `None` when the cursor is in the middle.
    /// Corners take priority over the bare edges they would otherwise
    /// overlap (NW > North; NW > West; …).
    pub fn detect(
        pointer: Point<f64, Logical>,
        avail:   Rectangle<i32, Logical>,
    ) -> Option<Self> {
        let l = avail.loc.x as f64;
        let t = avail.loc.y as f64;
        let r = (avail.loc.x + avail.size.w) as f64;
        let b = (avail.loc.y + avail.size.h) as f64;
        let x = pointer.x;
        let y = pointer.y;

        let near_l = x - l < SNAP_EDGE_THRESHOLD;
        let near_r = r - x < SNAP_EDGE_THRESHOLD;
        let near_t = y - t < SNAP_EDGE_THRESHOLD;
        let near_b = b - y < SNAP_EDGE_THRESHOLD;

        let in_corner_box = |horiz_l: bool, vert_t: bool| {
            let h_ok = if horiz_l { x - l < SNAP_CORNER_THRESHOLD } else { r - x < SNAP_CORNER_THRESHOLD };
            let v_ok = if vert_t  { y - t < SNAP_CORNER_THRESHOLD } else { b - y < SNAP_CORNER_THRESHOLD };
            h_ok && v_ok
        };

        // Corner precedence — the cursor must be in the corner box AND
        // close to at least one of the two edges that bound the corner.
        if in_corner_box(true,  true)  && (near_t || near_l) { return Some(SnapZone::NW); }
        if in_corner_box(false, true)  && (near_t || near_r) { return Some(SnapZone::NE); }
        if in_corner_box(true,  false) && (near_b || near_l) { return Some(SnapZone::SW); }
        if in_corner_box(false, false) && (near_b || near_r) { return Some(SnapZone::SE); }

        if near_t { return Some(SnapZone::Maximize); }
        if near_l { return Some(SnapZone::Left);     }
        if near_r { return Some(SnapZone::Right);    }
        // The bottom edge intentionally has no snap — most users brush
        // the bottom while moving a window down, and a "minimize" gesture
        // there clashes with the gesture handle strip on mobile builds.
        None
    }

    /// Concrete rect this zone snaps a window to, given the available
    /// output area (which is already topbar-offset by `Output::available`).
    pub fn rect(self, avail: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
        let l = avail.loc.x;
        let t = avail.loc.y;
        let w = avail.size.w;
        let h = avail.size.h;
        let half_w = w / 2;
        let half_h = h / 2;
        match self {
            SnapZone::Maximize => avail,
            SnapZone::Left  => Rectangle::new((l, t).into(),                 (half_w, h).into()),
            SnapZone::Right => Rectangle::new((l + half_w, t).into(),        (w - half_w, h).into()),
            SnapZone::NW    => Rectangle::new((l, t).into(),                 (half_w, half_h).into()),
            SnapZone::NE    => Rectangle::new((l + half_w, t).into(),        (w - half_w, half_h).into()),
            SnapZone::SW    => Rectangle::new((l, t + half_h).into(),        (half_w, h - half_h).into()),
            SnapZone::SE    => Rectangle::new((l + half_w, t + half_h).into(), (w - half_w, h - half_h).into()),
        }
    }
}

/// State for an in-progress edge-handle resize drag.
#[derive(Debug)]
pub struct ResizeDrag {
    pub window: Weak<RefCell<Window>>,
    pub edge: crate::ssd::ResizeEdge,
    pub start_pointer: Point<f64, Logical>,
    pub start_rect: Rectangle<i32, Logical>,
}

#[derive(Debug)]
pub struct ResizeDragX11 {
    pub wl: WlSurface,
    pub edge: crate::ssd::ResizeEdge,
    pub start_pointer: Point<f64, Logical>,
    pub start_rect: Rectangle<i32, Logical>,
}

impl Windows {
    pub fn new(display: &DisplayHandle, event_loop: LoopHandle<'static, Forge>) -> Self {
        let output = Output::new_dummy(display);
        let canvas = *output.canvas();

        Self {
            event_loop,
            output,
            canvas,
            start_time: Instant::now(),
            orientation_locked: true,
            dirty: true,
            unlocked_orientation: Orientation::default(),
            orphan_popups: Vec::new(),
            window_scales: Vec::new(),
            csd_apps: Vec::new(),
            closing_windows: Vec::new(),
            x11_windows: crate::x11_window::X11Windows::new(),
            toplevel_z_order: Vec::new(),
            transaction: None,
            focus_target: None,
            activated: None,
            textures: Vec::new(),
            layouts: Layouts::default(),
            layers: Layers::default(),
            view: View::default(),
            wallpaper: None,
            wallpaper_loaded: false,
            wallpaper_scale: 0.0,
            mru: Vec::new(),
            move_drag: None,
            move_drag_x11: None,
            resize_drag: None,
            resize_drag_x11: None,
            next_cascade: 0,
            pointer_pos: None,
            last_ssd_hover: None,
            geometry_store: crate::geometry_store::GeometryStore::load(),
        }
    }

    /// Mark a window as most-recently-used. Front of the list = most recent.
    pub fn mru_touch(&mut self, oid: smithay::reexports::wayland_server::backend::ObjectId) {
        self.mru.retain(|id| id != &oid);
        self.mru.insert(0, oid);
        // Cap to keep growth bounded; old entries fall out.
        if self.mru.len() > 64 { self.mru.truncate(64); }
    }


    /// `ObjectId` of the currently focused toplevel, or `None` if there
    /// is no focus. Used by the app switcher to start the selection on
    /// the active window so Tab / Shift+Tab cycle relative to it.
    pub fn focused_object_id(
        &self,
    ) -> Option<smithay::reexports::wayland_server::backend::ObjectId> {
        if let Some(target) = &self.focus_target {
            match target {
                FocusTarget::Layout(weak) => {
                    if let Some(w) = weak.upgrade() {
                        return Some(w.borrow().surface.surface().id());
                    }
                },
                FocusTarget::Layer(s, _) | FocusTarget::X11(s) => return Some(s.id()),
            }
        }
        self.activated.as_ref().map(|s| s.wl_surface().id())
    }

    /// Load wallpaper texture if not already loaded, or reload on scale change.
    fn ensure_wallpaper(&mut self, renderer: &mut GlesRenderer) {
        let scale = self.canvas.scale();
        if self.wallpaper_loaded && (self.wallpaper_scale - scale).abs() < 0.01 {
            return;
        }
        self.wallpaper_loaded = true;
        self.wallpaper_scale = scale;

        let path = Self::find_wallpaper();
        let Some(path) = path else {
            self.wallpaper = None;
            return;
        };

        let img = match image::open(&path) {
            Ok(img) => img.into_rgba8(),
            Err(_) => {
                self.wallpaper = None;
                return;
            },
        };

        let phys_size = self.canvas.physical_size();
        let (tw, th) = (phys_size.w as u32, phys_size.h as u32);
        if tw == 0 || th == 0 {
            self.wallpaper = None;
            return;
        }

        let (iw, ih) = img.dimensions();
        // Scale to cover (crop excess)
        let sx = tw as f64 / iw as f64;
        let sy = th as f64 / ih as f64;
        let s = sx.max(sy);
        let sw = (iw as f64 * s) as u32;
        let sh = (ih as f64 * s) as u32;
        let ox = (sw.saturating_sub(tw)) / 2;
        let oy = (sh.saturating_sub(th)) / 2;

        let mut buf = vec![0u8; (tw * th * 4) as usize];
        for y in 0..th {
            for x in 0..tw {
                let src_x = ((x + ox) as f64 / s) as u32;
                let src_y = ((y + oy) as f64 / s) as u32;
                let src_x = src_x.min(iw - 1);
                let src_y = src_y.min(ih - 1);
                let src_off = ((src_y * iw + src_x) * 4) as usize;
                let dst_off = ((y * tw + x) * 4) as usize;
                buf[dst_off..dst_off + 4].copy_from_slice(&img.as_raw()[src_off..src_off + 4]);
            }
        }

        let tex = Texture::from_buffer(renderer, scale, &buf, tw as i32, th as i32, true);
        self.wallpaper = Some(RenderTexture::new(tex));
    }

    fn find_wallpaper() -> Option<PathBuf> {
        let config_dir = dirs::config_dir()?.join("forge");
        for name in &["wallpaper.png", "wallpaper.jpg", "wallpaper.jpeg"] {
            let p = config_dir.join(name);
            if p.exists() {
                return Some(p);
            }
        }
        None
    }

    pub fn add(&mut self, surface: ToplevelSurface, foreign_handle: Option<ForeignToplevelHandle>) {
        // Mobile spawns toplevels as full-canvas tiles, so it makes sense to
        // tell the client it's tiled on every edge (drops rounded corners and
        // shadows). On desktop the same hint is poison: GTK4/libadwaita and
        // other modern toolkits treat "fully tiled" as "this window is
        // constrained inside a tiling grid", hide their CSD resize handles
        // and refuse user resize. Desktop windows are floating, so the
        // initial state stays unset and the client gets to render its normal
        // resize handles.
        let is_mobile = self.canvas.is_mobile();
        surface.with_pending_state(|state| {
            if is_mobile {
                if surface.version() >= 2 {
                    state.states.set(State::TiledBottom);
                    state.states.set(State::TiledRight);
                    state.states.set(State::TiledLeft);
                    state.states.set(State::TiledTop);
                } else {
                    state.states.set(State::Maximized);
                }
            }

            state.states.set(State::Activated);
        });

        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();

        let mut window_inner = Window::new(surface, scale, transform, None);
        window_inner.foreign_handle = foreign_handle;
        let window = Rc::new(RefCell::new(window_inner));

        // Desktop mode: new toplevels spawn as floating with a cascaded
        // start position and a reasonable default size. Major clients
        // (Firefox, Chromium, GTK apps) don't negotiate a deterministic
        // auto-size when the compositor sends size=None — they fall
        // back to a very small buffer — so we suggest an initial size
        // and let the client override via `xdg_surface.set_window_geometry`
        // on its first commit if it really wants something else. The
        // acked_size fallback added earlier in surface.rs still covers
        // well-behaved clients that pass their own geometry.
        if !self.canvas.is_mobile() {
            let avail = self.output.available();

            // Cascade position. Size is left at (0, 0): the initial
            // configure ships size = None so the client picks its
            // preferred footprint (xdg-shell convention). The first
            // surface_commit adopts `acked_size`; if the client
            // negotiated nothing reasonable, `default_floating_size`
            // backfills with 1024x720 and re-configures.
            let default_h: i32 = 720;
            let max_step = (avail.size.h - default_h).max(1);
            let step = self.next_cascade.rem_euclid(max_step);
            self.next_cascade = (self.next_cascade + 32).rem_euclid(max_step);

            let loc = Point::<i32, Logical>::from((
                avail.loc.x + 32 + step,
                avail.loc.y + 32 + step,
            ));

            let mut w = window.borrow_mut();
            w.floating = true;
            w.set_floating_position(loc);
            // Mark for geometry restore on the first commit that resolves
            // a non-empty app_id. The restore reads from the on-disk store
            // populated by previous sessions (eydos #7). Tiled / mobile
            // windows are intentionally not flagged — their geometry is
            // owned by the layout, not the per-app saved rect.
            w.needs_geometry_restore = true;
        }

        let wl = window.borrow().surface.surface().clone();
        self.layouts.create(&self.output, window);
        self.z_promote(&wl);

        self.unfullscreen(None);
    }

    pub fn add_layer(&mut self, layer: Layer, surface: LayerSurface, namespace: String) {
        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();

        let surface = ForgeLayerSurface::new(layer, surface);
        let mut window = Window::new(surface, scale, transform, Some(namespace));

        window.enter(&self.output);
        self.layers.add(window);
    }

    pub fn add_popup(&mut self, popup: PopupSurface) {
        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();

        self.orphan_popups.push(Window::new(popup, scale, transform, None));
    }

    pub fn reposition_popup(&mut self, popup: &PopupSurface, token: u32) {
        let scale = self.output.scale();
        let output_size = self.output.size();
        for mut window in self.layouts.windows_mut() {
            let parent_loc = window.output_rectangle().loc;
            window.reposition_popup(scale, output_size, parent_loc, popup, token);
        }
    }

    pub fn set_lock_surface(&mut self, surface: LockSurface) {
        let transform = self.output.orientation().surface_transform();
        let scale = self.output.scale();
        let size = self.output.size();

        let View::Lock(lock_window) = self.pending_view_mut() else { return };

        let mut window = Window::new(surface, scale, transform, None);
        window.set_dimensions(scale, Rectangle::from_size(size));

        *lock_window = Some(Box::new(window));
    }

    pub fn lock(&mut self) {
        self.set_view(View::Lock(None));
    }

    pub fn is_locked(&self) -> bool {
        matches!(self.pending_view(), View::Lock(_))
    }

    pub fn unlock(&mut self) {
        self.start_transaction().view = Some(View::Workspace);
    }

    pub fn find_xdg(&mut self, wl_surface: &WlSurface) -> Option<RefMut<'_, Window>> {
        let mut root = Cow::Borrowed(wl_surface);
        while let Some(parent) = compositor::get_parent(&root) {
            root = Cow::Owned(parent);
        }

        self.layouts.windows_mut().find(|w| w.surface().eq(root.as_ref()))
    }

    pub fn surface_commit(&mut self, surface: &WlSurface) {
        let mut root = Cow::Borrowed(surface);
        while let Some(parent) = compositor::get_parent(&root) {
            root = Cow::Owned(parent);
        }

        macro_rules! find_window {
            ($windows:expr) => {
                $windows.find(|w| w.surface().eq(root.as_ref()))
            };
        }

        let scale = self.output.scale();

        if let View::Lock(Some(window)) = self.pending_view_mut() {
            if window.surface() == root.as_ref() {
                window.surface_commit_common(scale, &[], surface);
                return;
            }
        }

        if let Some(mut window) = find_window!(self.layouts.windows_mut()) {
            window.surface_commit_common(scale, &self.window_scales, surface);
            // Geometry-restore hook (eydos #7). The first commit that
            // brings a non-empty `app_id` consults the on-disk store
            // and adopts the saved rect; subsequent commits leave the
            // flag clear, so user-driven resizes are not clobbered.
            // Maximized is re-applied via toggle_maximize so the
            // pre_max_rect machinery works on the next un-maximize.
            if window.needs_geometry_restore {
                // Clearing the flag is gated on a *non-empty* app_id —
                // the first few commits of some clients (Firefox is the
                // canonical case) arrive before `xdg_toplevel.set_app_id`,
                // and clearing the flag there would mean we never try
                // the restore once the real app_id finally lands. Once
                // we have an identifier we get one shot at applying the
                // saved rect; the result of that shot is irreversible.
                let resolved_id: Option<String> = window
                    .app_id.as_ref()
                    .filter(|s| !s.is_empty())
                    .cloned();
                if let Some(app_id) = resolved_id {
                    window.needs_geometry_restore = false;
                    if let Some(saved) = self.geometry_store.lookup(&app_id) {
                        let avail = self.output.available();
                        let rect  = saved.to_rect_clamped(avail);
                        window.set_dimensions(scale, rect);
                        if saved.maximized && !window.is_maximized() {
                            let bar_h = if window.suppress_ssd(&self.csd_apps) { 0 }
                                        else { crate::ssd::SsdDecoration::bar_height() };
                            window.toggle_maximize(scale, avail, bar_h);
                        }
                    }
                }
            }
            return;
        }

        self.orphan_surface_commit(&root);

        let output_size = self.output.size();
        for mut window in self.layouts.windows_mut() {
            let parent_loc = window.output_rectangle().loc;
            if window.popup_surface_commit(scale, output_size, parent_loc, &root, surface) {
                return;
            }
        }
        for window in self.layers.iter_mut() {
            let parent_loc = window.output_rectangle().loc;
            if window.popup_surface_commit(scale, output_size, parent_loc, &root, surface) {
                return;
            }
        }

        if self.x11_windows.owns(&root) {
            self.x11_windows.surface_commit(surface);
            self.dirty = true;
            return;
        }

        let Some(window) = find_window!(self.layers.iter_mut()) else { return };

        let old_exclusive = *self.output.exclusive();
        let fullscreen_active = matches!(self.view, View::Fullscreen(_));
        window.surface_commit(&self.window_scales, &mut self.output, fullscreen_active, surface);

        // Auto-focus layer surfaces that request exclusive keyboard interactivity
        let exclusive_focus = if !window.deny_focus {
            let interactivity = compositor::with_states(window.surface.surface(), |states| {
                states
                    .cached_state
                    .get::<LayerSurfaceCachedState>()
                    .current()
                    .keyboard_interactivity
            });
            if interactivity == KeyboardInteractivity::Exclusive {
                Some((window.surface.surface().clone(), window.app_id.clone()))
            } else {
                None
            }
        } else {
            None
        };

        if let Some((surface, app_id)) = exclusive_focus {
            let same = matches!(&self.focus_target, Some(FocusTarget::Layer(s, _)) if s == &surface);
            if !same {
                self.set_focus(Some(FocusTarget::Layer(surface, app_id)));
            }
        }

        if self.output.exclusive() != &old_exclusive {
            self.resize_visible();
        }
    }

    pub fn orphan_surface_commit(&mut self, root: &WlSurface) -> Option<()> {
        let index = self.orphan_popups.iter().position(|p| p.surface() == root)?;
        let mut popup = self.orphan_popups.swap_remove(index);
        let parent = popup.parent()?;

        let active = self.layouts.active();
        if let Some(primary) = active.primary() {
            popup = primary.borrow_mut().add_popup(popup, &parent)?;
        }

        if let Some(secondary) = active.secondary() {
            popup = secondary.borrow_mut().add_popup(popup, &parent)?;
        }

        for window in self.layers.iter_mut() {
            popup = window.add_popup(popup, &parent)?;
        }

        popup.surface.send_popup_done();

        Some(())
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn import_buffers(&mut self, renderer: &mut GlesRenderer) {
        for mut window in self.layouts.windows_mut() {
            window.import_buffers(renderer);
        }

        for window in self.layers.iter_mut() {
            window.import_buffers(renderer);
        }

        if let View::Lock(Some(window)) = &mut self.view {
            window.import_buffers(renderer);
        }

        // Pull buffers from every mapped X11Surface's wl_surface tree.
        // Empty `x11_windows` is a no-op fast path.
        let scale = self.output.scale();
        self.x11_windows.import_buffers(renderer, scale);
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn textures(
        &mut self,
        renderer: &mut GlesRenderer,
        graphics: &mut Graphics,
        cursor: Option<crate::cursor::CursorRender>,
        text_renderer: &mut crate::text::TextRenderer,
        mpris: &mut crate::mpris::MprisState,
        polkit: &mut crate::polkit::PolkitAgent,
        power_menu: &mut crate::powermenu::PowerMenu,
        osd: &mut crate::osd::Osd,
        topbar: &mut crate::topbar::Topbar,
        sys_state: &crate::sysmon::SystemState,
        app_switcher: &mut crate::app_switcher::AppSwitcher,
    ) -> &[ForgeElement] {
        self.dirty = false;

        let scale = self.output.scale();
        self.textures.clear();

        // textures[0] is the front-most layer, so push the cursor before
        // every other overlay to keep it painted on top.
        if let Some(cursor) = cursor {
            // forge owns the resize edges of its own decorations, so a resize
            // shape there overrides whatever the client requested.
            let icon = match self.resize_cursor_edge(cursor.position) {
                Some(edge) => crate::drawing::resize_icon(edge),
                None => cursor.icon,
            };
            let (texture, hotspot) = graphics.cursor_for(renderer, &self.canvas, icon);
            let raw: smithay::utils::Point<i32, smithay::utils::Physical> =
                cursor.position.to_physical(scale).to_i32_round();
            let pos = raw - hotspot;
            let mut bounds = texture.geometry(scale.into());
            bounds.loc = pos;
            ForgeElement::add_cursor(&mut self.textures, texture, pos, bounds, None, scale);
        }

        match &self.view {
            View::Workspace | View::Overview(_) => {
                for layer in self.layers.foreground() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
                let bar_w = self.canvas.size().w;
                topbar.render(&mut self.textures, &self.canvas, renderer, text_renderer, sys_state, bar_w);
            }
            View::DragAndDrop(_) => {
                for layer in self.layers.foreground() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
            }
            _ => {}
        }

        if app_switcher.is_active() {
            let entries = self.app_switcher_entries();
            app_switcher.refresh_entries(entries);
        }
        app_switcher.tick_theme();
        app_switcher.tick_carousel();
        app_switcher.render(&mut self.textures, &self.canvas, renderer, text_renderer);
        if app_switcher.animating() {
            self.dirty = true;
        }

        // Polkit authentication dialog — modal, on top of everything
        polkit.textures(&mut self.textures, &self.canvas, renderer, text_renderer);

        // Power menu — modal, on top of everything
        power_menu.textures(&mut self.textures, &self.canvas, renderer, text_renderer);

        // MPRIS media controls (normal mode, not lockscreen)
        mpris.textures(&mut self.textures, &self.canvas, renderer, text_renderer, false);

        // OSD volume/brightness overlay — on top of everything else
        osd.textures(&mut self.textures, &self.canvas, renderer, text_renderer);

        // Snap-preview overlay — translucent accent rectangle over the
        // zone the in-flight title-bar drag would commit to on release.
        // Drawn above everything else so the user sees the target even
        // when dragging across foreground layer surfaces (e.g. crustace
        // overlays). No-op when no drag is in flight or the cursor is
        // not near a snap edge.
        if let Some(drag) = self.move_drag.as_ref() {
            if let Some(zone) = drag.snap {
                let avail = self.output.available();
                let target = zone.rect(avail);
                let phys = target.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    graphics.snap_preview.clone(),
                    phys.loc, phys, None, scale,
                );
            }
        }

        if self.canvas.is_mobile() && !matches!(self.view, View::Fullscreen(_) | View::Lock(_)) {
            let gesture_handle = graphics.gesture_handle(renderer, &self.canvas);

            let mut bounds = gesture_handle.geometry(scale.into());
            let output_height = self.canvas.physical_size().h;
            let handle_location = (0, output_height - bounds.size.h);
            bounds.loc = handle_location.into();

            ForgeElement::add_element(
                &mut self.textures,
                gesture_handle,
                handle_location,
                bounds,
                None,
                scale,
            );
        }

        let needs_wallpaper = matches!(
            self.view,
            View::Workspace | View::DragAndDrop(_) | View::Overview(_)
        );

        match &mut self.view {
            View::Workspace => {
                if self.canvas.is_mobile() {
                    self.layouts.textures_and_ssd(
                        &mut self.textures, &self.canvas, renderer, text_renderer,
                        &self.csd_apps,
                        self.pointer_pos,
                    );
                    self.x11_windows.paint_all(
                        &mut self.textures, &self.canvas, renderer, text_renderer,
                        self.pointer_pos,
                    );
                } else {
                    use std::collections::HashSet;
                    let order = self.toplevel_z_order.clone();
                    let mut painted: HashSet<u32> = HashSet::new();
                    use smithay::reexports::wayland_server::Resource as _;
                    for wl in order.iter().rev() {
                        if self.layouts.paint_one(
                            wl, &mut self.textures, &self.canvas, renderer, text_renderer,
                            &self.csd_apps, self.pointer_pos,
                        ) { painted.insert(wl.id().protocol_id()); continue; }
                        if self.x11_windows.paint_one(
                            wl, &mut self.textures, &self.canvas, renderer, text_renderer,
                            self.pointer_pos,
                        ) { painted.insert(wl.id().protocol_id()); }
                    }
                    self.layouts.paint_remaining(
                        &painted, &mut self.textures, &self.canvas, renderer, text_renderer,
                        &self.csd_apps, self.pointer_pos,
                    );
                    self.x11_windows.paint_remaining(
                        &painted, &mut self.textures, &self.canvas, renderer, text_renderer,
                        self.pointer_pos,
                    );
                }

                // Closing-window tombstones render after live windows so
                // their shrink animation paints on top of whatever just
                // appeared underneath, but below the foreground layer-shell
                // strip so a closing app does not bleed through the topbar.
                Self::render_closing_windows(&self.closing_windows, scale, &mut self.textures);

                for layer in self.layers.background() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
            },
            View::DragAndDrop(dnd) => {
                dnd.textures(&mut self.textures, &self.canvas, graphics);

                self.layouts.textures_and_ssd(
                    &mut self.textures, &self.canvas, renderer, text_renderer,
                    &self.csd_apps,
                    self.pointer_pos,
                );

                for layer in self.layers.background() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
            },
            View::Overview(overview) => {
                overview.textures(
                    &mut self.textures,
                    &self.output,
                    &self.canvas,
                    &self.layouts,
                    graphics,
                );

                for layer in self.layers.background() {
                    layer.textures(&mut self.textures, scale, None, None);
                }
            },
            View::Fullscreen(window) => {
                for layer in self.layers.overlay() {
                    layer.textures(&mut self.textures, scale, None, None);
                }

                window.borrow().textures(&mut self.textures, scale, None, None);
            },
            View::Lock(window_opt) => {
                // On-screen keyboard above the lock. A session-lock compositor
                // otherwise presents only the lock surface, so squeekboard's
                // layer surface (namespace "osk", on top/overlay) would never
                // show while locked. Composited first so it sits above the lock
                // surface; everything else on the foreground layers stays
                // hidden, as the lock spec requires.
                for osk in self.layers.foreground().filter(|w| w.app_id.as_deref() == Some("osk")) {
                    osk.textures(&mut self.textures, scale, None, None);
                }
                // Lock-surface texture next; the solid fallback follows so it
                // sits *behind* the surface. Without the fallback the previous
                // frame would stay on screen until the lock client commits its
                // first buffer, and would also stay on screen forever if the
                // client died — the ext-session-lock-v1 spec mandates that the
                // session stays visually locked in both cases.
                if let Some(window) = window_opt {
                    window.textures(&mut self.textures, scale, None, None);
                }
                let size_log = self.canvas.size();
                let logical: Rectangle<i32, Logical> =
                    Rectangle::new(Point::from((0, 0)), size_log);
                let phys = logical.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    graphics.lock_fallback.clone(),
                    phys.loc, phys, None, scale,
                );
            },
        }

        // Wallpaper — rendered behind everything as the bottom-most layer
        if needs_wallpaper {
            self.ensure_wallpaper(renderer);
            if let Some(wp) = &self.wallpaper {
                let size = self.canvas.size();
                let wp_rect: Rectangle<i32, Logical> =
                    Rectangle::new(Point::from((0, 0)), size);
                let physical_wp = wp_rect.to_physical_precise_round(scale);
                ForgeElement::add_element(
                    &mut self.textures,
                    wp.clone(),
                    physical_wp.loc,
                    physical_wp,
                    None,
                    scale,
                );
            }
        }

        self.textures.as_slice()
    }

    pub fn request_frames(&mut self) {
        let runtime = self.runtime();
        self.x11_windows.request_frames(runtime);

        match &self.view {
            View::Fullscreen(window) => {
                for overlay in self.layers.overlay() {
                    overlay.request_frame(runtime);
                }
                window.borrow().request_frame(runtime);
            },
            View::Overview(_) | View::DragAndDrop(_) => {
                for window in self.layers.background() {
                    window.request_frame(runtime);
                }
            },
            View::Workspace => {
                self.layers.request_frames(runtime);
                if self.canvas.is_mobile() {
                    self.layouts.with_visible(|w| w.request_frame(runtime));
                } else {
                    // Desktop stacks every toplevel in its own Layout and
                    // renders all of them simultaneously, but `active_layout`
                    // only tracks one. Frame callbacks must go to every
                    // visible window — otherwise focusing a non-"active"
                    // window (e.g. clicking a terminal that's not the active
                    // layout) starves it of callbacks: the client processes
                    // input but can't commit a new buffer, so typed
                    // characters never appear until something else nudges
                    // the layout active set.
                    for window in self.layouts.windows() {
                        window.request_frame(runtime);
                    }
                }
            },
            View::Lock(window_opt) => {
                if let Some(window) = window_opt {
                    window.request_frame(runtime);
                }
                // Keep squeekboard drawing while locked (see the lock render arm).
                for osk in self.layers.foreground().filter(|w| w.app_id.as_deref() == Some("osk")) {
                    osk.request_frame(runtime);
                }
            },
        }
    }

    pub fn mark_presented(
        &mut self,
        states: &RenderElementStates,
        metadata: &Option<DrmEventMetadata>,
    ) {
        for mut window in self.layouts.windows_mut() {
            window.mark_presented(states, metadata, &self.output, &self.start_time);
        }

        for layer in self.layers.iter_mut() {
            layer.mark_presented(states, metadata, &self.output, &self.start_time);
        }
    }

    pub fn reap_xdg(&mut self, surface: &ToplevelSurface) {
        self.z_remove(surface.wl_surface());
        self.unfullscreen(surface);

        // Capture a tombstone snapshot *before* the layout reap drops the
        // window — `Layouts::reap` releases the only `Rc<RefCell<Window>>`
        // on the layout side, after which the texture cache is gone and
        // we'd have nothing to fade out. The snapshot survives because
        // `RenderTexture` is `Rc<Texture>`: cloning it bumps the refcount
        // and the GL texture lives until the closing animation completes.
        // The animation runs in both mobile and desktop mode (#3).
        if let Some(tombstone) = self.snapshot_for_close(surface) {
            self.closing_windows.push(tombstone);
        }

        self.layouts.reap(&self.output, surface);
    }

    /// Build a [`ClosingWindow`] entry for the destroyed `surface`, returning
    /// `None` when the window has no rendered texture yet (a destroy that
    /// races the first frame, very rare in practice). Pure read — no state
    /// mutation — so the caller decides whether to push the result onto the
    /// `closing_windows` list.
    fn snapshot_for_close(&self, surface: &ToplevelSurface) -> Option<ClosingWindow> {
        let window_rc = self.layouts.find_window(surface.surface())?;
        let w = window_rc.borrow();
        if w.minimized { return None; }
        let texture = w.thumbnail_texture()?;
        // Use output_rectangle (the compositor-tracked position/size) rather
        // than bounds() so the animation anchor is stable even when the
        // texture_cache has already been flushed by a null-buffer commit
        // (foot terminal pattern). bounds() adds an internal_offset derived
        // from texture_cache.size(), which is (0,0) after the null commit,
        // shifting the rect down by half the window height.
        let target_rect = w.allocated_rect();
        Some(ClosingWindow {
            texture,
            target_rect,
            start:    Instant::now(),
            duration: Duration::from_millis(CLOSE_ANIM_MS),
        })
    }

    /// Drop closing-window entries whose timer has expired. Called from the
    /// frame tick alongside the other animation pumps.
    pub fn tick_closing_animations(&mut self) {
        let now = Instant::now();
        let before = self.closing_windows.len();
        self.closing_windows
            .retain(|c| now.saturating_duration_since(c.start) < c.duration);
        if self.closing_windows.len() != before {
            self.dirty = true;
        }
    }

    /// Render every active closing-window tombstone into `textures`. Each
    /// entry shrinks from full size to zero on a quadratic ease-in over
    /// `CLOSE_ANIM_MS`, anchored on the centre of its original on-screen
    /// rect so the user sees the window pull inward toward the spot it
    /// used to occupy. Free function (no `self`) so the caller can hand
    /// in `&mut self.textures` and `&self.closing_windows` from inside
    /// `textures()` without a borrow conflict — Rust's split-borrow
    /// inference doesn't reach across method calls.
    fn render_closing_windows(
        closing: &[ClosingWindow],
        scale:   f64,
        textures: &mut Vec<ForgeElement>,
    ) {
        let now = Instant::now();
        for c in closing {
            let elapsed = now.saturating_duration_since(c.start).as_secs_f32();
            let total   = c.duration.as_secs_f32().max(0.001);
            let t       = (elapsed / total).clamp(0.0, 1.0);
            // Quadratic ease-in: starts at near-full size and accelerates
            // inward. Visually feels like the window collapses *into*
            // itself instead of sliding away linearly.
            let shrink = 1.0 - (t * t);
            if shrink <= 0.001 { continue; }

            let rect_phys: smithay::utils::Rectangle<i32, smithay::utils::Physical> =
                c.target_rect.to_physical_precise_round(scale);
            let cx = rect_phys.loc.x + rect_phys.size.w / 2;
            let cy = rect_phys.loc.y + rect_phys.size.h / 2;
            let new_w = (rect_phys.size.w as f32 * shrink) as i32;
            let new_h = (rect_phys.size.h as f32 * shrink) as i32;
            if new_w <= 0 || new_h <= 0 { continue; }

            let new_loc = smithay::utils::Point::<i32, smithay::utils::Physical>::from((
                cx - new_w / 2,
                cy - new_h / 2,
            ));
            // Pin the texture's window_scale to `shrink` so smithay's
            // RescaleRenderElement does the actual pixel-side resampling;
            // pass the shrunk rect as the crop bounds so opaque-region
            // tracking stays in sync with what is actually drawn.
            let bounds = smithay::utils::Rectangle::<i32, smithay::utils::Physical>::new(
                new_loc, (new_w, new_h).into(),
            );
            ForgeElement::add_element(
                textures,
                c.texture.clone(),
                new_loc,
                bounds,
                Some(shrink as f64 * scale),
                scale,
            );
        }
    }

    pub fn reap_layer(&mut self, surface: &LayerSurface) {
        start_transaction();

        let old_exclusive = *self.output.exclusive();
        if let Some(window) = self.layers.iter().find(|l| l.surface.eq(surface)) {
            self.output.exclusive().reset(window.surface.anchor, window.surface.exclusive_zone);
        }

        if matches!(&self.focus_target, Some(FocusTarget::Layer(s, _)) if s == surface.wl_surface()) {
            self.focus_target = None;
        }

        if self.output.exclusive() != &old_exclusive {
            self.resize_visible();
        }
    }

    pub fn refresh_popups(&mut self) {
        for mut window in self.layouts.windows_mut() {
            window.refresh_popups();
        }
    }

    pub fn start_dnd(&mut self, layout_position: LayoutPosition) {
        let View::Overview(overview) = &mut self.view else { return };

        let Some(window) = self.layouts.window_at(layout_position).cloned() else { return };

        let dnd = DragAndDrop::new(&self.output, overview, layout_position, window);
        self.set_view(View::DragAndDrop(dnd));
    }

    pub fn fullscreen(&mut self, surface: &ToplevelSurface) {
        if let Some(window) = self.layouts.find_window(surface.surface()) {
            {
                let mut w = window.borrow_mut();
                // Snapshot the pre-fullscreen rect so unfullscreen can land
                // the window back on its previous geometry. Floating windows
                // need this — Layout::resize skips them, so without the
                // snapshot they would keep the fullscreen rectangle and end
                // up covering the topbar after exiting.
                w.enter_fullscreen();
                w.surface.set_state(|state| {
                    state.states.set(State::Fullscreen);
                });
            }

            self.set_view(View::Fullscreen(window.clone()));
            self.resize_visible();
        }
    }

    pub fn unfullscreen<'a>(&mut self, surface: impl Into<Option<&'a ToplevelSurface>>) {
        let surface = surface.into();

        let View::Fullscreen(window) = &self.view else { return };
        let window_rc = window.clone();
        let mut window = window_rc.borrow_mut();

        if surface.is_none_or(|s| s == &window.surface) {
            window.surface.set_state(|state| {
                state.states.unset(State::Fullscreen);
            });
            // Hand the window back its pre-fullscreen rect before the
            // generic resize_visible path runs. resize_visible only re-tiles
            // tracked layout windows — floating windows would otherwise
            // keep the fullscreen rectangle (no topbar offset) and cover
            // the topbar permanently after a single F11.
            let scale = self.output.scale();
            window.exit_fullscreen(scale);
            drop(window);

            self.set_view(View::Workspace);
            self.resize_visible();
        }
    }

    /// Per-frame focus resolver. Returns the keyboard surface (and its app
    /// id), reconciling the xdg `Activated` state along the way so "focused"
    /// and "has the keyboard" never drift apart.
    ///
    /// Two notions feed it: the *baseline* is the active layout's window
    /// ([`Layouts::focus`], updated by layout operations and by clicks on a
    /// toplevel) and is what carries `Activated`; the *overlay* is a focused
    /// layer-shell or X11 surface ([`Self::focus_target`]) that takes the
    /// keyboard over the baseline without de-activating it.
    pub fn focus(&mut self) -> Option<(WlSurface, Option<String>)> {
        if let View::Lock(window) = &self.view {
            return window.as_ref().map(|w| (w.surface().clone(), w.app_id.clone()));
        }

        let baseline = match self.layouts.focus.as_ref().map(Weak::upgrade) {
            Some(Some(window)) => {
                window.borrow_mut().urgent = false;
                Some(window)
            },
            Some(None) => {
                let primary = self.layouts.pending_active().primary().cloned();
                if let Some(w) = &primary {
                    w.borrow_mut().urgent = false;
                }
                self.layouts.focus = primary.as_ref().map(Rc::downgrade);
                primary
            },
            None => None,
        };

        let active_toplevel = baseline.as_ref().map(|w| w.borrow().surface.clone());
        if self.activated.as_ref().map(|s| s.surface())
            != active_toplevel.as_ref().map(|s| s.surface())
        {
            if let Some(activated) = self.activated.take() {
                activated.set_state(|state| {
                    state.states.unset(State::Activated);
                });
            }
            if let Some(surface) = &active_toplevel {
                surface.set_state(|state| {
                    state.states.set(State::Activated);
                });
            }
            self.activated = active_toplevel;
        }

        // A focused layer/X11 overlay takes the keyboard over the baseline,
        // mirroring the old `layers.focus.or(baseline)` precedence. A stale
        // overlay (its surface gone) falls through to the baseline.
        if let Some(t @ (FocusTarget::Layer(..) | FocusTarget::X11(_))) = &self.focus_target {
            if self.is_focusable(t) {
                return t.resolve();
            }
        }

        baseline.map(|w| {
            let w = w.borrow();
            (w.surface.surface().clone(), w.app_id.clone())
        })
    }

    fn is_focusable(&self, target: &FocusTarget) -> bool {
        match target {
            FocusTarget::Layout(weak) => match weak.upgrade() {
                Some(w) => {
                    let w = w.borrow();
                    if w.minimized {
                        return false;
                    }
                    if self.canvas.is_mobile() {
                        return self.layouts.position(w.surface.surface()).map(|p| p.index)
                            == self.layouts.active_index();
                    }
                    true
                },
                None => false,
            },
            FocusTarget::Layer(surface, _) => {
                self.layers.iter().any(|l| l.surface.surface() == surface)
            },
            FocusTarget::X11(surface) => self.x11_windows.entries().iter().any(|e| {
                !e.minimized && e.surface.wl_surface().as_ref() == Some(surface)
            }),
        }
    }

    /// The single entry point for changing focus. Brings the target to the
    /// front (z-order + desktop stacking), refreshes the MRU and updates the
    /// baseline/overlay focus so keyboard focus and activation follow on the
    /// next [`Self::focus`]. `None` clears focus entirely (tap on the
    /// background).
    pub fn set_focus(&mut self, target: Option<FocusTarget>) {
        match &target {
            Some(FocusTarget::Layout(weak)) => {
                if let Some(w) = weak.upgrade() {
                    let surface = w.borrow().surface.surface().clone();
                    self.mru_touch(surface.id());
                    self.z_promote(&surface);
                    if !self.canvas.is_mobile() {
                        if let Some(id) = self.layouts.layout_id_for(&surface) {
                            self.layouts.raise_to_top(id);
                        }
                    }
                    self.layouts.focus = Some(weak.clone());
                }
            },
            Some(FocusTarget::X11(surface)) => {
                self.mru_touch(surface.id());
                self.z_promote(surface);
            },
            Some(FocusTarget::Layer(..)) => {},
            None => self.layouts.focus = None,
        }
        self.focus_target = target;
        self.dirty = true;
    }

    fn start_transaction(&mut self) -> &mut Transaction {
        start_transaction();
        self.transaction.get_or_insert(Transaction::new())
    }

    pub fn update_transaction(&mut self) -> Option<Duration> {
        let start = TRANSACTION_START.load(Ordering::Relaxed);
        if start == 0 {
            return None;
        }

        let elapsed = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 - start;
        let timeout = self.transaction_timeout();

        if elapsed <= timeout {
            let lock_ready = match self.pending_view() {
                View::Lock(Some(window)) => window.transaction_done(),
                View::Lock(None) => false,
                _ => true,
            };

            let finished = lock_ready
                && self.layouts.windows().all(|w| w.transaction_done())
                && self.layers.iter().all(Window::transaction_done);

            if !finished {
                return Some(Duration::from_millis(timeout - elapsed));
            }
        } else {
            for mut window in self.layouts.windows_mut().filter(|w| !w.transaction_done()) {
                window.set_ignore_transactions();
            }
            for window in self.layers.iter_mut().filter(|w| !w.transaction_done()) {
                window.set_ignore_transactions();
            }
        }

        TRANSACTION_START.store(0, Ordering::Relaxed);

        let old_layout_count = self.layouts.active().window_count();
        let old_layer_count = self.layers.len();

        if let Some(view) = self.transaction.take().and_then(|t| t.view) {
            self.dirty = true;
            self.view = view;
        }

        self.dirty |= self.layouts.apply_transaction(&self.output);

        self.layers.apply_transaction();

        if let View::Lock(Some(window)) = &mut self.view {
            window.apply_transaction();
        }

        let old_canvas = mem::replace(&mut self.canvas, *self.output.canvas());
        self.dirty |= old_canvas.orientation() != self.canvas.orientation()
            || old_canvas.scale() != self.canvas.scale();

        if self.layouts.is_empty()
            && matches!(self.view, View::Overview(_) | View::DragAndDrop(_) | View::Fullscreen(_))
        {
            self.view = View::Workspace;
        }

        self.dirty |= old_layout_count != self.layouts.active().window_count()
            || old_layer_count != self.layers.len();

        None
    }

    fn transaction_timeout(&self) -> u64 {
        match self.pending_view() {
            View::Lock(_) => MAX_LOCKED_TRANSACTION_MILLIS,
            _ => MAX_TRANSACTION_MILLIS,
        }
    }

    pub fn resize_all(&mut self) {
        let available_fullscreen = self.output.available_fullscreen();
        let output_size = self.output.size();
        let scale = self.output.scale();

        let fullscreen_window = match self.pending_view_mut() {
            View::Fullscreen(window) => {
                let mut w = window.borrow_mut();
                w.set_dimensions(scale, available_fullscreen);
                Some(w.surface().clone())
            },
            View::Lock(Some(window)) => {
                window.set_dimensions(scale, Rectangle::from_size(output_size));
                None
            },
            _ => None,
        };
        let fullscreen_window = fullscreen_window.as_ref();

        for layout in self.layouts.layouts() {
            if layout.windows().any(|w| Some(w.borrow().surface()) == fullscreen_window) {
                continue;
            }
            layout.resize(&self.output);
        }

        for window in self.layers.iter_mut() {
            let fullscreened =
                window.surface.layer() == Layer::Overlay && fullscreen_window.is_some();
            window.update_dimensions(&mut self.output, fullscreened);
        }
    }

    pub fn resize_visible(&mut self) {
        let available_fullscreen = self.output.available_fullscreen();
        let scale = self.output.scale();
        let size = self.output.size();

        match self.pending_view_mut() {
            View::Fullscreen(window) => {
                window.borrow_mut().set_dimensions(scale, available_fullscreen);

                for window in self.layers.overlay_mut() {
                    window.update_dimensions(&mut self.output, true);
                }
            },
            View::Lock(Some(window)) => {
                window.set_dimensions(scale, Rectangle::from_size(size));
            },
            _ => {
                self.layouts.pending_active().resize(&self.output);

                for window in self.layers.iter_mut() {
                    window.update_dimensions(&mut self.output, false);
                }
            },
        }
    }

    pub fn set_orientation(&mut self, orientation: Orientation) {
        start_transaction();

        self.output.set_orientation(orientation);

        let transform = orientation.surface_transform();
        with_all_windows!(self, |window| window.update_transform(transform));

        self.resize_all();
    }

    pub fn update_orientation(&mut self, orientation: Orientation) {
        self.unlocked_orientation = orientation;

        if self.orientation_locked {
            return;
        }

        self.set_orientation(orientation);
    }

    pub fn lock_orientation(&mut self, orientation: Option<Orientation>) {
        if let Some(orientation) = orientation {
            self.set_orientation(orientation);
        }

        self.orientation_locked = true;
    }

    pub fn unlock_orientation(&mut self) {
        self.orientation_locked = false;
        self.update_orientation(self.unlocked_orientation);
    }

    pub fn orientation_locked(&self) -> bool {
        self.orientation_locked
    }

    pub fn damaged(&mut self) -> bool {
        if self.dirty {
            return true;
        }

        // Drop expired close-animation entries first; this also flips
        // `self.dirty` to true on the very tick the last entry expires so
        // the frame loop renders one more clean frame after the animation
        // ends.
        self.tick_closing_animations();
        if self.dirty || !self.closing_windows.is_empty() {
            return true;
        }

        match &self.view {
            View::Overview(overview) if overview.dirty() => true,
            View::Workspace | View::Overview(_) => {
                self.layouts.windows().any(|w| w.dirty()) || self.layers.iter().any(Window::dirty)
            },
            View::Fullscreen(window) => {
                window.borrow().dirty() || self.layers.overlay().any(Window::dirty)
            },
            View::Lock(window) => window.as_ref().is_some_and(|w| w.dirty()),
            View::DragAndDrop(_) => false,
        }
    }

    pub fn on_touch_start(&mut self, point: Point<f64, Logical>) {
        let View::Overview(overview) = &mut self.view else { return };

        if let Some(pos) = overview.layout_position(&self.output, &self.layouts, point) {
            overview.start_hold(&self.event_loop, pos);
        }

        overview.drag_action = Default::default();
        overview.last_drag_point = point;
        overview.y_offset = 0.0;
    }

    pub fn on_tap(&mut self, point: Point<f64, Logical>) {
        let View::Overview(overview) = &mut self.view else {
            return;
        };

        overview.cancel_hold(&self.event_loop);

        if let Some(pos) = overview.layout_position(&self.output, &self.layouts, point) {
            self.layouts.set_active(&self.output, Some(pos), true);
        }

        self.set_view(View::Workspace);
    }

    pub fn on_double_tap(&mut self, point: Point<f64, Logical>) {
        if !matches!(self.view, View::Workspace) {
            return;
        }

        let canvas_size = self.canvas.size().to_f64();
        if point.y < (canvas_size.h - GESTURE_HANDLE_HEIGHT as f64) {
            return;
        }

        if point.x >= canvas_size.w / 1.5 {
            self.layouts.cycle_active(&self.output, 1);
        } else if point.x < canvas_size.w / 3.0 {
            self.layouts.cycle_active(&self.output, -1);
        }
    }

    pub fn on_drag(&mut self, touch_state: &mut TouchState, mut point: Point<f64, Logical>) {
        match &mut self.view {
            View::Overview(overview) => {
                let delta = point - mem::replace(&mut overview.last_drag_point, point);

                if matches!(overview.drag_action.action_type, DragActionType::None) {
                    if delta.x.abs() < delta.y.abs() {
                        overview.drag_action = overview
                            .layout_position(&self.output, &self.layouts, point)
                            .and_then(|pos| self.layouts.window_at(pos))
                            .map(|w| DragActionType::Close(Rc::downgrade(w)).into())
                            .unwrap_or_default();
                    } else {
                        overview.drag_action = DragActionType::Cycle.into();
                    }
                }

                match overview.drag_action.action_type {
                    DragActionType::Cycle => {
                        let sensitivity = self.output.physical_size().w as f64 * 0.4;
                        overview.x_offset += delta.x / sensitivity;
                    },
                    DragActionType::Close(_) => overview.y_offset += delta.y,
                    DragActionType::None => (),
                }

                if overview.cycle_edge_reached(self.layouts.len()) {
                    touch_state.cancel_velocity();
                }

                overview.cancel_hold(&self.event_loop);

                self.dirty = true;
            },
            View::DragAndDrop(dnd) => {
                let output_size = self.output.wm_size().to_f64();
                if point.x < 0.0
                    || point.x > output_size.w
                    || point.y < 0.0
                    || point.y > output_size.h
                {
                    point.x = point.x.clamp(0.0, output_size.w - 1.0);
                    point.y = point.y.clamp(0.0, output_size.h - 1.0);
                    touch_state.cancel_velocity();
                }

                let delta = point - mem::replace(&mut dnd.touch_position, point);
                dnd.window_position += delta;

                self.dirty = true;
            },
            View::Fullscreen(_) | View::Lock(_) | View::Workspace => (),
        }
    }

    pub fn on_drag_release(&mut self) {
        match &mut self.view {
            View::Overview(overview) => overview.drag_action.done = true,
            View::DragAndDrop(dnd) => {
                let (primary_bounds, secondary_bounds) = dnd.drop_bounds(&self.output);
                if primary_bounds.to_f64().contains(dnd.touch_position) {
                    let surface = dnd.window.borrow().surface().clone();
                    if let Some(pos) = self.layouts.position(&surface) {
                        self.layouts.set_primary(&self.output, pos);
                        self.set_view(View::Workspace);
                    }
                } else if secondary_bounds.to_f64().contains(dnd.touch_position) {
                    let surface = dnd.window.borrow().surface().clone();
                    if let Some(pos) = self.layouts.position(&surface) {
                        self.layouts.set_secondary(&self.output, pos);
                        self.set_view(View::Workspace);
                    }
                } else {
                    let overview = Overview::new(dnd.overview_x_offset, None);
                    self.set_view(View::Overview(overview));
                }
            },
            View::Fullscreen(_) | View::Lock(_) | View::Workspace => (),
        }
    }

    /// True when the homescreen is what the user sees — no *visible* app on
    /// the active workspace (it is empty, or every window is minimised). The
    /// bottom swipe-up then reveals the launcher rather than shrinking a
    /// foreground app, so a quick swipe after "go home" opens the launcher
    /// while a dwell still raises the switcher of the minimised apps.
    pub fn homescreen_active(&self) -> bool {
        matches!(self.view, View::Workspace)
            && self.layouts.active().windows().all(|w| w.borrow().minimized)
    }

    /// True when the foreground layer surface *under `position`* takes keyboard
    /// focus — the open launcher, or a fullscreen modal greeter / lock surface.
    /// The swipe gestures must not arm over it: its scrolling and dismissal are
    /// that surface's own (ltk) gestures, and a fullscreen lock owns all input.
    /// Checked at the press point so an unrelated partial surface elsewhere
    /// (e.g. the on-screen keyboard at the opposite edge) doesn't block a
    /// gesture that starts on the bare homescreen.
    pub fn has_exclusive_foreground_at(&self, position: Point<f64, Logical>) -> bool {
        let scale = self.canvas.scale();
        self.layers
            .foreground_surface_at(scale, position)
            .is_some_and(|(w, _)| !w.deny_focus)
    }

    /// True when a dismissable crustace panel (Quick Settings, calendar,
    /// pickers…) is open — a focus-denying overlay surface. A bottom
    /// swipe-up then dismisses it instead of revealing the launcher.
    pub fn has_dismissable_panel(&self) -> bool {
        self.layers.overlay().any(|w| w.deny_focus)
    }

    /// Enter Overview to follow the finger for an app-context bottom
    /// swipe-up. No-op on the bare homescreen (handled by the launcher).
    pub fn begin_swipe_overview(&mut self) {
        if matches!(self.view, View::Workspace) && !self.layouts.active().is_empty() {
            let overview = Overview::new(self.layouts.active_offset(), Some(1.0));
            self.set_view(View::Overview(overview));
            self.dirty = true;
        }
    }

    /// Drive the in-progress Overview shrink from the swipe progress.
    pub fn drive_swipe_overview(&mut self, progress: f64) {
        if let View::Overview(overview) = &mut self.view {
            overview.set_swipe_progress(progress);
            self.dirty = true;
        }
    }

    /// Cancel an app-context swipe below the commit threshold: grow the
    /// window back to full size and return to the workspace.
    pub fn cancel_swipe_overview(&mut self) {
        if matches!(self.view, View::Overview(_)) {
            self.set_view(View::Workspace);
            self.resize_visible();
            self.dirty = true;
        }
    }

    /// Commit an app-context swipe past the home threshold: minimise the
    /// foreground app and land on the homescreen.
    pub fn commit_swipe_home(&mut self) {
        self.minimize_all();
    }

    /// Settle into the opened Overview grid — the desktop recents view a
    /// dwelled bottom swipe-up lands on.
    pub fn open_swipe_overview_grid(&mut self) {
        let off = match &self.view {
            View::Overview(overview) => overview.x_offset,
            _ => self.layouts.active_offset(),
        };
        self.set_view(View::Overview(Overview::new(off, None)));
        self.dirty = true;
    }

    /// Returns `true` when the caller should open the app launcher.
    pub fn on_gesture(&mut self, touch_state: &mut TouchState, gesture: HandleGesture) -> bool {
        match (gesture, &mut self.view) {
            (HandleGesture::Vertical(position), View::Overview(overview)) => {
                overview.set_open_percentage(&self.output, position);

                if overview.gesture_threshold_passed() && !touch_state.touching() {
                    touch_state.cancel_velocity();
                    return self.on_gesture_done(gesture);
                }
            },
            (HandleGesture::Vertical(position), View::Workspace) if !self.layouts.is_empty() => {
                let available = self.canvas.available_overview().to_f64();
                if position < available.loc.y || position >= available.loc.y + available.size.h {
                    return false;
                }

                let active_empty = self.layouts.active().is_empty();
                if active_empty {
                    // No windows: open app launcher directly
                    self.set_view(View::Workspace);
                    self.dirty = true;
                    touch_state.cancel_velocity();
                    return true;
                } else {
                    let primary_pct = Some(1.0);
                    let overview = Overview::new(self.layouts.active_offset(), primary_pct);
                    self.set_view(View::Overview(overview));
                }
            },
            (HandleGesture::Vertical(_), View::Workspace) => {
                // No windows: stay on workspace (launcher is in crustace)
                self.dirty = true;
                touch_state.cancel_velocity();
            },
            (HandleGesture::Vertical(_) | HandleGesture::Horizontal, _) => (),
        }
        false
    }

    /// Returns `true` when the caller should open the app launcher.
    pub fn on_gesture_done(&mut self, gesture: HandleGesture) -> bool {
        match (gesture, &mut self.view) {
            (HandleGesture::Vertical(position), View::Overview(overview)) if overview.opened => {
                overview.set_open_percentage(&self.output, position);

                if overview.gesture_threshold_passed() {
                    // Swipe up past threshold in opened overview → open launcher
                    self.set_view(View::Workspace);
                    self.dirty = true;
                    return true;
                } else {
                    let overview = Overview::new(overview.x_offset, None);
                    self.set_view(View::Overview(overview));
                }
            },
            (HandleGesture::Vertical(position), View::Overview(overview)) if !overview.opened => {
                overview.set_open_percentage(&self.output, position);

                if overview.gesture_threshold_passed() {
                    overview.opened = true;
                } else {
                    self.set_view(View::Workspace);
                }
            },
            (HandleGesture::Vertical(position), View::Fullscreen(window)) => {
                let available = self.canvas.available_overview().to_f64();
                if position < available.loc.y || position >= available.loc.y + available.size.h {
                    return false;
                }

                window.borrow().surface.set_state(|state| {
                    state.states.unset(State::Fullscreen);
                });

                self.set_view(View::Workspace);
                self.resize_visible();
            },
            (HandleGesture::Vertical(_) | HandleGesture::Horizontal, _) => (),
        }
        false
    }

    pub fn z_promote(&mut self, surface: &WlSurface) {
        self.toplevel_z_order.retain(|s| s != surface);
        self.toplevel_z_order.push(surface.clone());
    }

    pub fn z_remove(&mut self, surface: &WlSurface) {
        self.toplevel_z_order.retain(|s| s != surface);
    }

    fn z_index(&self, surface: &WlSurface) -> Option<usize> {
        self.toplevel_z_order.iter().position(|s| s == surface)
    }

    pub fn foreground_layer_at(&self, position: Point<f64, Logical>) -> bool {
        let scale = self.canvas.scale();
        self.layers.foreground_surface_at(scale, position).is_some()
    }

    /// Pointer focus restricted to foreground layer surfaces (dock, topbar).
    /// Used while the app switcher is up so those surfaces keep getting
    /// hover/motion — and thus tooltips — without leaking motion to the
    /// client windows that sit under the carousel thumbnails.
    pub fn foreground_layer_surface_at(
        &self,
        position: Point<f64, Logical>,
    ) -> Option<InputSurface> {
        let scale = self.canvas.scale();
        let (w, mut surface) = self.layers.foreground_surface_at(scale, position)?;
        if !w.deny_focus {
            let wl_surface = w.surface().clone();
            let app_id = w.app_id.clone();
            surface.toplevel = Some(InputSurfaceKind::Layer((wl_surface, app_id)));
        }
        Some(surface)
    }

    pub fn surface_at(&mut self, position: Point<f64, Logical>) -> Option<InputSurface> {
        let scale = self.canvas.scale();

        macro_rules! focus_layer_surface {
            ($window:expr, $surface:expr) => {{
                if !$window.deny_focus {
                    let wl_surface = $window.surface().clone();
                    let app_id = $window.app_id.clone();
                    $surface.toplevel = Some(InputSurfaceKind::Layer((wl_surface, app_id)));
                }
            }};
        }

        match &self.view {
            View::Workspace => (),
            View::Fullscreen(window) => {
                if let Some((w, mut surface)) = self.layers.overlay_surface_at(scale, position) {
                    focus_layer_surface!(w, surface);
                    return Some(surface);
                }

                let window_ref = window.borrow();
                let mut surface = window_ref.surface_at(scale, position)?;

                let app_id = window_ref.app_id.clone();
                let weak = Rc::downgrade(window);
                surface.toplevel = Some(InputSurfaceKind::Layout((weak, app_id)));

                return Some(surface);
            },
            View::Lock(window_opt) => {
                // Touch over the OSK goes to squeekboard; the rest of the
                // locked screen goes to the lock surface. Other foreground
                // layer surfaces stay non-interactive while locked.
                if let Some((w, mut surface)) = self.layers.foreground_surface_at(scale, position) {
                    if w.app_id.as_deref() == Some("osk") {
                        focus_layer_surface!(w, surface);
                        return Some(surface);
                    }
                }
                return window_opt.as_ref().and_then(|w| w.surface_at(scale, position));
            },
            _ => return None,
        };

        if let Some((w, mut surface)) = self.layers.foreground_surface_at(scale, position) {
            focus_layer_surface!(w, surface);
            return Some(surface);
        }

        let desktop = !self.canvas.is_mobile();
        let layouts_hit = self.layouts.touch_surface_at(scale, position, desktop);
        let x11_hit = self.x11_windows.surface_at(position).map(|(wl, origin)| {
            let mut s = InputSurface::new(wl.clone(), origin, 1.0);
            s.toplevel = Some(InputSurfaceKind::X11(wl));
            s
        });
        match (layouts_hit, x11_hit) {
            (Some(l), Some(x)) => {
                // Compare the owning toplevel's root z, not `l.surface`: a
                // popup or subsurface hit carries the child surface, which is
                // not tracked in `toplevel_z_order` (z_index would be `None`
                // and the X window would wrongly win underneath the popup).
                let l_root = match &l.toplevel {
                    Some(InputSurfaceKind::Layout((weak, _))) => {
                        weak.upgrade().map(|w| w.borrow().surface.surface().clone())
                    },
                    _ => Some(l.surface.clone()),
                };
                let l_z = l_root.as_ref().and_then(|s| self.z_index(s));
                let x_z = self.z_index(&x.surface);
                let pick = match (l_z, x_z) {
                    (Some(a), Some(b)) => if a > b { l } else { x },
                    (Some(_), None) => l,
                    (None, Some(_)) => x,
                    (None, None) => x,
                };
                return Some(pick);
            },
            (Some(l), None) => return Some(l),
            (None, Some(x)) => return Some(x),
            (None, None)    => (),
        }

        if let Some((w, mut surface)) = self.layers.background_surface_at(scale, position) {
            focus_layer_surface!(w, surface);
            return Some(surface);
        }

        None
    }

    pub fn add_window_scale(&mut self, app_id: AppIdMatcher, scale: WindowScale) {
        self.window_scales.retain(|(m, _)| m.base() != app_id.base());

        match scale {
            WindowScale::Additive(s) | WindowScale::Subtractive(s) if s == 0.0 => (),
            WindowScale::Multiplicative(s) | WindowScale::Divisive(s) if s == 1.0 => (),
            _ => self.window_scales.push((app_id, scale)),
        }

        let window_scales = mem::take(&mut self.window_scales);
        let output_scale = self.output.scale();
        with_all_windows_mut!(self, |window| window.set_window_scale(&window_scales, output_scale));
        self.window_scales = window_scales;
    }

    pub fn surface_visible(&self, surface: &WlSurface) -> bool {
        if let View::Lock(Some(window)) = self.pending_view() {
            if window.owns_surface(surface) {
                return true;
            }
        }

        let mut visible_xdg = false;
        if self.canvas.is_mobile() {
            self.layouts.with_visible(|w| visible_xdg |= w.owns_surface(surface));
        } else {
            // Desktop renders every stacked toplevel, not just the active
            // layout. Mirror that here so idle-inhibitor checks (and any
            // other "is this surface on screen" check) don't treat
            // background-but-visible windows as hidden.
            visible_xdg = self.layouts.windows().any(|w| w.owns_surface(surface));
        }

        visible_xdg || self.layers.iter().any(|l| l.owns_surface(surface))
    }

    pub fn runtime(&self) -> u32 {
        self.start_time.elapsed().as_millis() as u32
    }

    fn pending_view(&self) -> &View {
        self.transaction.as_ref().and_then(|t| t.view.as_ref()).unwrap_or(&self.view)
    }

    fn pending_view_mut(&mut self) -> &mut View {
        self.transaction.as_mut().and_then(|t| t.view.as_mut()).unwrap_or(&mut self.view)
    }

    fn set_view(&mut self, view: View) {
        if let View::Lock(_) = self.pending_view() {
            return;
        }

        match view {
            View::Overview(_) => match &mut self.transaction {
                Some(transaction) => {
                    transaction.view = None;
                    self.view = view;
                },
                None => {
                    self.dirty = true;
                    self.view = view;
                },
            },
            _ => self.start_transaction().view = Some(view),
        }
    }

    pub fn set_output(&mut self, output: Output) {
        self.canvas = *output.canvas();
        self.output = output;
    }

    #[cfg(feature = "winit")]
    pub fn resize_output(&mut self, mode: Mode) {
        // Update mode without changing the transform (winit needs Flipped180)
        self.output.smithay_output().change_current_state(
            Some(mode), None, None, None,
        );
        self.output.smithay_output().set_preferred(mode);
        self.output.canvas_mut().set_mode(mode);
        self.canvas = *self.output.canvas();
        self.resize_all();
    }

    pub fn canvas(&self) -> &Canvas {
        &self.canvas
    }

    pub fn available_top_y(&self) -> i32 {
        self.output.available().loc.y
    }

    pub fn set_scale(&mut self, scale: f64) {
        self.start_transaction();
        self.output.set_scale(scale);

        with_all_windows!(self, |window| window.update_scale(scale));

        self.resize_all();
    }

    pub fn configure_topbar(&mut self, height: i32) {
        self.output.canvas_mut().set_topbar_height(height);
        self.canvas.set_topbar_height(height);
        self.dirty = true;
        self.resize_all();
    }

    pub fn set_mobile_override(&mut self, enabled: Option<bool>) {
        self.output.canvas_mut().set_mobile_override(enabled);
        self.canvas.set_mobile_override(enabled);
        self.dirty = true;
        self.resize_all();
    }

    pub fn set_dirty(&mut self) {
        self.dirty = true;
    }

    /// Record a pointer motion. Stores the position so the SSD render
    /// path can compute hover state for window-control buttons, and
    /// marks the compositor dirty when the pointer crosses into / out
    /// of a button (so the next frame can repaint the hover surface).
    /// Pointer motion that stays inside the same button — or moves
    /// across non-button areas — does not dirty.
    pub fn pointer_moved(&mut self, pos: Point<f64, Logical>) {
        self.pointer_pos = Some(pos);
        let cur = self.ssd_button_hover_summary(pos);
        if cur != self.last_ssd_hover {
            self.last_ssd_hover = cur;
            self.set_dirty();
        }
    }

    /// Compress the current pointer-vs-SSD-buttons relationship to
    /// `(window-identity, button-action)` for transition detection.
    /// Returns `None` when the pointer is off all SSD bars or sitting
    /// on the move-area of a bar (Move never paints a hover surface,
    /// so it should not trigger a redraw).
    fn ssd_button_hover_summary(
        &self,
        pos: Point<f64, Logical>,
    ) -> Option<(usize, crate::ssd::SsdAction)> {
        let (w, action) = self.ssd_window_hit_test(pos)?;
        if matches!(action, crate::ssd::SsdAction::Move) { return None; }
        let addr = std::rc::Rc::as_ptr(&w) as usize;
        Some((addr, action))
    }

    pub fn raise(&mut self, surface: &WlSurface) {
        if let Some(pos) = self.layouts.position(surface) {
            self.layouts.set_active(&self.output, Some(pos), false);
            if let Some(id) = self.layouts.layout_id_for(surface) {
                self.layouts.raise_to_top(id);
            }
            self.mru_touch(surface.id());
        }
    }

    pub fn set_urgent(&mut self, surface: &WlSurface, urgent: bool) {
        if let Some(window) = self.layouts.find_window(surface) {
            window.borrow_mut().urgent = urgent;
        }
    }

    pub fn parent_geometry(&self, surface: &WlSurface) -> Rectangle<i32, Logical> {
        with_all_windows!(self, |window| {
            if window.owns_surface(surface) {
                return window.bounds(self.output.scale());
            }
        });

        Rectangle::from_size(self.output.size())
    }


    /// Switch to a specific workspace by index (0-based).
    pub fn switch_workspace(&mut self, index: usize) {
        if self.layouts.is_empty() {
            return;
        }

        if matches!(self.view, View::Fullscreen(_)) {
            self.set_view(View::Workspace);
        }

        self.layouts.switch_to(&self.output, index);
        self.dirty = true;
    }

    /// True when no live windows remain in any workspace.
    pub fn has_no_windows(&self) -> bool
    {
        self.layouts.windows().all( |w| !w.alive() )
    }

    /// Get total number of workspaces.
    pub fn workspace_count(&self) -> usize {
        self.layouts.len()
    }

    /// Get active workspace index.
    pub fn active_workspace(&self) -> Option<usize> {
        self.layouts.active_index()
    }

    /// Close a window by app_id.
    pub fn close_by_app_id(&self, app_id: &str) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.app_id.as_deref() == Some(app_id) {
                    w.surface.send_close();
                    return;
                }
            }
        }
    }

    /// Close the currently focused toplevel window (Alt+F4).
    pub fn close_focused(&mut self) {
        let active = self.layouts.pending_active();
        if let Some(window) = active.primary() {
            window.borrow().surface.send_close();
        }
    }

    /// Build the flat window list used by the Alt+Tab switcher, sorted MRU.
    pub fn app_switcher_entries(&mut self) -> Vec<crate::app_switcher::WindowEntry> {
        let mut all: Vec<crate::app_switcher::WindowEntry> = Vec::new();
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                let wl = w.surface.surface();
                let surface_id = wl.id().protocol_id();
                let app_id = w.app_id.clone().unwrap_or_default();
                let title = smithay::wayland::compositor::with_states(wl, |states| {
                    states
                        .data_map
                        .get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
                        .and_then(|d| d.lock().ok())
                        .and_then(|d| d.title.clone())
                        .unwrap_or_default()
                });
                let thumbnail  = w.thumbnail_texture();
                let thumb_size = w.thumbnail_size();
                let object_id  = Some(wl.id());
                tracing::debug!(
                    "app_switcher: wayland window surface_id={} app_id={:?} title={:?} minimized={}",
                    surface_id, app_id, title, w.minimized
                );
                all.push(crate::app_switcher::WindowEntry {
                    surface_id, object_id, app_id, title, thumbnail, thumb_size,
                });
            }
        }

        for entry in self.x11_windows.entries() {
            if entry.surface.is_override_redirect() { continue; }
            let Some(wl) = entry.surface.wl_surface() else { continue };
            let surface_id = wl.id().protocol_id();
            let title = entry.surface.title();
            let app_id = entry.surface.class();
            let title = if title.is_empty() { app_id.clone() } else { title };
            tracing::debug!(
                "app_switcher: x11 window surface_id={} app_id={:?} title={:?}",
                surface_id, app_id, title
            );
            all.push(crate::app_switcher::WindowEntry {
                surface_id,
                object_id: Some(wl.id()),
                app_id,
                title,
                thumbnail: entry.thumbnail_texture(),
                thumb_size: entry.thumbnail_size(),
            });
        }

        tracing::debug!("app_switcher: {} total entries before sort", all.len());

        // Stable alphabetical order: app_id, then title, then surface_id
        // as a final tie-break. Keeps the layout deterministic across
        // re-renders so users build muscle memory for tile positions.
        all.sort_by(|a, b| {
            a.app_id.cmp(&b.app_id)
                .then_with(|| a.title.cmp(&b.title))
                .then_with(|| a.surface_id.cmp(&b.surface_id))
        });
        all
    }

    /// Activate a window by the full `ObjectId` of its `wl_surface`.
    /// `protocol_id` is per-client and collides across clients, so two
    /// distinct windows can share the same `u32` — Alt+Tab would then
    /// activate whichever appears first in layout iteration order rather
    /// than the one the user selected; the `ObjectId` is unique.
    pub fn activate_by_object_id(
        &mut self,
        oid: &smithay::reexports::wayland_server::backend::ObjectId,
    ) {
        let target: Option<(usize, Rc<RefCell<Window>>)> = self
            .layouts
            .layouts()
            .iter()
            .enumerate()
            .find_map(|(ws_idx, layout)| {
                layout
                    .windows()
                    .find_map(|window| {
                        (&window.borrow().surface.surface().id() == oid)
                            .then(|| (ws_idx, window.clone()))
                    })
            });
        let Some((ws_idx, window_rc)) = target else { return };

        if Some(ws_idx) != self.active_workspace() {
            self.switch_workspace(ws_idx);
        }
        window_rc.borrow_mut().minimized = false;
        let surface = window_rc.borrow().surface.surface().clone();
        self.raise(&surface);
        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
        self.set_view(View::Workspace);
        self.dirty = true;
    }

    pub fn activate_x11_by_object_id(
        &mut self,
        oid: &smithay::reexports::wayland_server::backend::ObjectId,
    ) -> Option<smithay::xwayland::X11Surface> {
        let wl = self.x11_windows.entries().iter().find_map(|e| {
            let s = e.surface.wl_surface()?;
            (s.id() == *oid).then_some(s)
        })?;
        let surface = self.x11_windows.find(&wl)?.clone();
        self.x11_windows.set_minimized(&wl, false);
        self.set_focus(Some(FocusTarget::X11(wl)));
        self.set_view(View::Workspace);
        self.dirty = true;
        Some(surface)
    }

    /// List all toplevel windows for the shell's overview/task switcher.
    pub fn list_toplevel_windows(&self) -> Vec<serde_json::Value> {
        let mut result = Vec::new();
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                let wl = w.surface.surface();
                let id = wl.id().protocol_id() as u64;
                let app_id = w.app_id.clone().unwrap_or_default();

                // Get title from xdg-shell attributes
                let title = smithay::wayland::compositor::with_states(wl, |states| {
                    states
                        .data_map
                        .get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
                        .and_then(|d| d.lock().ok())
                        .and_then(|d| d.title.clone())
                        .unwrap_or_default()
                });

                result.push(serde_json::json!({
                    "id": id,
                    "title": title,
                    "app_id": app_id,
                }));
            }
        }
        result
    }

    /// Activate (focus + raise) a window by its protocol ID.
    pub fn activate_window_by_id(&mut self, id: u64) {
        let mut target_ws = None;
        let mut target_window: Option<Rc<RefCell<Window>>> = None;
        for (ws_idx, layout) in self.layouts.layouts().iter().enumerate() {
            for window in layout.windows() {
                if window.borrow().surface.surface().id().protocol_id() as u64 == id {
                    target_ws = Some(ws_idx);
                    target_window = Some(window.clone());
                    break;
                }
            }
            if target_ws.is_some() { break; }
        }
        if let Some(ws_idx) = target_ws {
            if Some(ws_idx) != self.active_workspace() {
                self.switch_workspace(ws_idx);
            }
            // Raise the window in its workspace so it becomes primary.
            if let Some(w) = target_window {
                // Un-minimize on activation so the switcher can restore a
                // minimized entry.
                w.borrow_mut().minimized = false;
                let surface = w.borrow().surface.surface().clone();
                self.raise(&surface);
                self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&w))));
            }
            self.set_view(View::Workspace);
            self.dirty = true;
        }
    }

    /// Bring the first window matching `app_id` to the front. Drives
    /// activation directly off the `WlSurface` rather than the
    /// protocol id: `id().protocol_id()` is only unique within one
    /// Wayland client, so two clients can both have a surface whose
    /// `protocol_id` returns the same `u32`, and the by-id path would
    /// happily activate the wrong one.
    pub fn activate_window_by_app_id(&mut self, app_id: &str) {
        let target: Option<(usize, Rc<RefCell<Window>>)> = self.layouts.layouts().iter()
            .enumerate()
            .find_map(|(ws_idx, layout)| {
                layout.windows().find_map(|window| {
                    (window.borrow().app_id.as_deref() == Some(app_id))
                        .then(|| (ws_idx, window.clone()))
                })
            });
        if let Some((ws_idx, window_rc)) = target {
            if Some(ws_idx) != self.active_workspace() {
                self.switch_workspace(ws_idx);
            }
            window_rc.borrow_mut().minimized = false;
            let surface = window_rc.borrow().surface.surface().clone();
            self.raise(&surface);
            self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
            self.set_view(View::Workspace);
            self.dirty = true;
            return;
        }

        let x11_match = self.x11_windows.entries().iter().find_map(|e| {
            (e.surface.class().as_str() == app_id
                || e.surface.instance().as_str() == app_id)
                .then(|| e.surface.wl_surface())
                .flatten()
        });
        if let Some(wl) = x11_match {
            self.x11_windows.set_minimized(&wl, false);
            self.set_focus(Some(FocusTarget::X11(wl)));
            self.set_view(View::Workspace);
            self.dirty = true;
        }
    }

    /// Resize every Wayland toplevel whose `app_id` matches to `(w, h)`,
    /// preserving the current location. Used by `forge --winit --size`
    /// so the dev session can force its window to the requested size
    /// even when the host compositor has a saved geometry for that
    /// app_id in `geometry_store`.
    pub fn resize_window_by_app_id(&mut self, app_id: &str, w: i32, h: i32) {
        let scale = self.canvas.scale();
        let avail = self.output.available();
        let mut hit = false;
        for mut window in self.layouts.windows_mut() {
            if window.app_id.as_deref() == Some(app_id) {
                // Un-maximize first; `bar_h` is only consulted on the maximize
                // branch of `toggle_maximize`, which we never hit here because
                // the guard above means we are in the unmaximize branch.
                if window.is_maximized() {
                    window.toggle_maximize(scale, avail, 0);
                }
                let loc = window.output_rectangle().loc;
                window.set_dimensions(scale, Rectangle::new(loc, (w, h).into()));
                hit = true;
            }
        }
        if hit {
            self.dirty = true;
        }
    }

    pub fn close_windows_by_app_id(&self, app_id: &str) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.app_id.as_deref() == Some(app_id) {
                    w.surface.send_close();
                }
            }
        }
        for entry in self.x11_windows.entries() {
            if entry.surface.class().as_str() == app_id
                || entry.surface.instance().as_str() == app_id
            {
                let _ = entry.surface.close();
            }
        }
    }

    /// Close a window by its protocol ID (sends xdg_toplevel.close).
    pub fn close_window_by_id(&self, id: u64) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.surface.surface().id().protocol_id() as u64 == id {
                    w.surface.send_close();
                    return;
                }
            }
        }
        for entry in self.x11_windows.entries() {
            let Some(wl) = entry.surface.wl_surface() else { continue };
            if wl.id().protocol_id() as u64 == id {
                let _ = entry.surface.close();
                return;
            }
        }
    }

    pub fn close_window_by_object_id(
        &self,
        oid: &smithay::reexports::wayland_server::backend::ObjectId,
    ) {
        for layout in self.layouts.layouts() {
            for window in layout.windows() {
                let w = window.borrow();
                if w.surface.surface().id() == *oid {
                    w.surface.send_close();
                    return;
                }
            }
        }
        for entry in self.x11_windows.entries() {
            let Some(wl) = entry.surface.wl_surface() else { continue };
            if wl.id() == *oid {
                let _ = entry.surface.close();
                return;
            }
        }
    }

    /// Minimize all windows in the currently active layout.
    pub fn minimize_all(&mut self) {
        for window in self.layouts.active().windows() {
            window.borrow_mut().minimized = true;
        }
        self.set_view(View::Workspace);
        self.dirty = true;
    }

    /// Toggle overview (app switcher). Opens if in workspace, closes if already open.
    pub fn toggle_overview(&mut self) {
        match &self.view {
            View::Overview(_) => {
                self.set_view(View::Workspace);
                self.dirty = true;
            }
            View::Workspace => {
                let overview = Overview::new(0.0, None);
                self.set_view(View::Overview(overview));
                self.dirty = true;
            }
            _ => {}
        }
    }

    /// Toggle SSD-maximize for the focused window (title-bar Max button).
    /// On desktop outputs this toggles the xdg Maximized state and resizes
    /// the window to the work area. On mobile falls back to fullscreen so
    /// the button still does something useful.
    pub fn toggle_maximize_focused(&mut self) {
        if self.canvas.is_mobile() {
            self.toggle_fullscreen();
            return;
        }
        let active = self.layouts.pending_active();
        let Some(window) = active.primary() else { return };
        let avail = self.output.available();
        let scale = self.output.scale();
        let bar_h = self.bar_h_for(&window.borrow());
        window.borrow_mut().toggle_maximize(scale, avail, bar_h);
        self.record_geometry(&window.borrow());
        let wl = window.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    fn bar_h_for(&self, w: &Window) -> i32 {
        if w.suppress_ssd(&self.csd_apps) { 0 } else { crate::ssd::SsdDecoration::bar_height() }
    }

    /// Persist `w`'s floating geometry (or its pre-maximize rect when
    /// currently maximized) under `app_id` in the on-disk geometry
    /// store. Called from every user-driven geometry change so that
    /// the next launch of the same app can adopt the rect via the
    /// restore hook in `surface_commit`. No-op when `app_id` is
    /// missing or empty.
    fn record_geometry(&mut self, w: &Window) {
        let Some(app_id) = w.app_id.as_deref() else { return };
        if app_id.is_empty() { return; }
        self.geometry_store.record(app_id, w.persist_rect(), w.is_maximized());
    }

    /// Toggle fullscreen for the focused window (F11).
    pub fn toggle_fullscreen(&mut self) {
        if let View::Fullscreen(_) = &self.view {
            self.unfullscreen(None);
        } else {
            let active = self.layouts.pending_active();
            if let Some(window) = active.primary() {
                let surface = window.borrow().surface.clone();
                self.fullscreen(&surface);
            }
        }
    }

    /// Minimize only the focused window (SSD title-bar Min button).
    pub fn minimize_focused( &mut self )
    {
        let active = self.layouts.pending_active();
        if let Some( window ) = active.primary() {
            window.borrow_mut().minimized = true;
            self.dirty = true;
        }
    }

    /// Hit-test the server-side decoration of any window at `point`,
    /// returning the target window reference and the action for drag
    /// interactions.
    pub fn ssd_window_hit_test(
        &self,
        point: smithay::utils::Point<f64, Logical>,
    ) -> Option<( Rc<RefCell<Window>>, crate::ssd::SsdAction )>
    {
        let scale = self.canvas.scale();
        let bar_h = crate::ssd::SsdDecoration::bar_height();

        // Desktop: topmost floating window first.
        if !self.canvas.is_mobile() {
            for layout in self.layouts.stacking_layouts() {
                for window_rc in layout.windows() {
                    let (rect, bar, surface) = {
                        let w = window_rc.borrow();
                        if w.minimized { continue; }
                        let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                        (w.bounds(scale), bar, w.surface.surface().clone())
                    };
                    if let Some(action) = crate::ssd::hit_test(rect, point, bar) {
                        let this_z = self.z_index(&surface);
                        for entry in self.x11_windows.entries() {
                            if !entry.surface.is_mapped() || entry.minimized { continue; }
                            let Some(other_wl) = entry.surface.wl_surface() else { continue };
                            let geo = entry.surface.geometry();
                            if !crate::ssd::body_hit_test(geo, point, bar_h) { continue; }
                            match (self.z_index(&other_wl), this_z) {
                                (Some(a), Some(b)) if a > b => return None,
                                _ => {}
                            }
                        }
                        return Some((window_rc.clone(), action));
                    }
                    if crate::ssd::body_hit_test(rect, point, bar) {
                        return None;
                    }
                }
            }
            return None;
        }

        for window_rc in self.layouts.active().windows()
        {
            let (rect, bar) = {
                let w = window_rc.borrow();
                if w.minimized { continue; }
                let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                (w.bounds(scale), bar)
            };
            if let Some( action ) = crate::ssd::hit_test( rect, point, bar )
            {
                return Some(( window_rc.clone(), action ));
            }
        }
        None
    }

    pub fn ssd_x11_hit_test(
        &self,
        point: Point<f64, Logical>,
    ) -> Option<(WlSurface, crate::ssd::SsdAction)> {
        if self.canvas.is_mobile() { return None; }
        let (wl, action) = self.x11_windows.ssd_hit_test(point)?;
        if self.x11_hit_occluded(&wl, point) { return None; }
        Some((wl, action))
    }

    pub fn apply_ssd_button_x11(
        &mut self,
        wl: &WlSurface,
        action: crate::ssd::SsdAction,
    ) -> bool {
        use crate::ssd::SsdAction;
        self.z_promote(wl);
        match action {
            SsdAction::Close => {
                if let Some(s) = self.x11_windows.find(wl) { let _ = s.close(); }
            }
            SsdAction::Maximize => {
                let avail = self.output.available();
                self.x11_windows.toggle_maximize(wl, avail);
            }
            SsdAction::Minimize => {
                self.x11_windows.set_minimized(wl, true);
            }
            SsdAction::Move => return false,
        }
        self.dirty = true;
        true
    }

    /// Mark whether a toplevel wants client-side decorations (disables
    /// forge's SSD for that window). Called from the xdg-decoration
    /// handler on mode-change requests.
    pub fn set_wants_csd(&mut self, surface: &ToplevelSurface, wants_csd: bool) {
        for layout in self.layouts.layouts() {
            for w in layout.windows() {
                if &w.borrow().surface == surface {
                    w.borrow_mut().wants_csd = wants_csd;
                    self.dirty = true;
                    return;
                }
            }
        }
    }

    /// Check whether the SSD should be suppressed for `surface` based on its
    /// current `app_id` vs `self.csd_apps`. Used by the decoration
    /// `unset_mode` handler to pick the right decoration mode when the client
    /// says "don't care".
    pub fn should_suppress_ssd_lookup(&self, surface: &ToplevelSurface) -> bool {
        for layout in self.layouts.layouts() {
            for w in layout.windows() {
                if &w.borrow().surface == surface {
                    return w.borrow().suppress_ssd(&self.csd_apps);
                }
            }
        }
        false
    }

    fn x11_hit_occluded(&self, x11_wl: &WlSurface, point: Point<f64, Logical>) -> bool {
        let this_z = self.z_index(x11_wl);
        let scale = self.canvas.scale();
        let bar_h = crate::ssd::SsdDecoration::bar_height();

        for layout in self.layouts.stacking_layouts() {
            for window_rc in layout.windows() {
                let (rect, bar, surface) = {
                    let w = window_rc.borrow();
                    if w.minimized { continue; }
                    let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                    (w.bounds(scale), bar, w.surface.surface().clone())
                };
                if !crate::ssd::body_hit_test(rect, point, bar) { continue; }
                if matches!((self.z_index(&surface), this_z), (Some(a), Some(b)) if a > b) {
                    return true;
                }
            }
        }

        for entry in self.x11_windows.entries() {
            if !entry.surface.is_mapped() || entry.minimized { continue; }
            let Some(other_wl) = entry.surface.wl_surface() else { continue };
            if &other_wl == x11_wl { continue; }
            let geo = entry.surface.geometry();
            if !crate::ssd::body_hit_test(geo, point, bar_h) { continue; }
            if matches!((self.z_index(&other_wl), this_z), (Some(a), Some(b)) if a > b) {
                return true;
            }
        }

        false
    }

    /// Return the resize edge currently under `pointer`, if any. While a
    /// resize drag is active the drag's edge is returned even if the
    /// pointer has moved outside the grab band, so the cursor stays stable
    /// through the interaction.
    pub fn resize_cursor_edge(
        &self,
        pointer: Point<f64, Logical>,
    ) -> Option<crate::ssd::ResizeEdge> {
        if self.canvas.is_mobile() {
            return None;
        }
        if let Some(drag) = &self.resize_drag {
            return Some(drag.edge);
        }
        if let Some(drag) = &self.resize_drag_x11 {
            return Some(drag.edge);
        }
        if let Some((wl, edge, _)) = self.x11_windows.resize_hit_test(pointer) {
            if !self.x11_hit_occluded(&wl, pointer) {
                return Some(edge);
            }
        }

        // Walk stacking order topmost-first. If the pointer is inside a
        // window's visible footprint (body + SSD title bar) before we find a
        // resize edge, that window owns the pixel and any resize edges below
        // it are occluded — stop. Without this early-out, a 510 px wide
        // window A with a 8 px right-edge grab zone reaching into x=510..518
        // would still arm its Right resize when the pointer is at x=512
        // inside the body of window B that sits flush to its right.
        let scale = self.canvas.scale();
        let bar_h = crate::ssd::SsdDecoration::bar_height();
        for layout in self.layouts.stacking_layouts() {
            for window_rc in layout.windows() {
                let (rect, eligible, minimized, bar) = {
                    let w = window_rc.borrow();
                    let eligible = w.floating && !w.minimized && !w.is_maximized();
                    let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                    (w.bounds(scale), eligible, w.minimized, bar)
                };
                if minimized { continue; }
                if crate::ssd::body_hit_test(rect, pointer, bar) {
                    return None;
                }
                if !eligible { continue; }
                if let Some(edge) = crate::ssd::resize_hit_test(rect, pointer, bar) {
                    return Some(edge);
                }
            }
        }
        None
    }

    /// Handle a click on an SSD button of a specific window (desktop mode).
    /// Returns true if an action was taken. The target window is raised to
    /// top and made focused so subsequent key events go to it.
    pub fn apply_ssd_button(
        &mut self,
        window_rc: &Rc<RefCell<Window>>,
        action: crate::ssd::SsdAction,
    ) -> bool {
        use crate::ssd::SsdAction;

        let surface = window_rc.borrow().surface.surface().clone();
        self.raise_toplevel(&surface);

        match action {
            SsdAction::Close => {
                window_rc.borrow().surface.send_close();
            }
            SsdAction::Maximize => {
                let avail = self.output.available();
                let scale = self.output.scale();
                let bar_h = self.bar_h_for(&window_rc.borrow());
                window_rc.borrow_mut().toggle_maximize(scale, avail, bar_h);
                self.record_geometry(&window_rc.borrow());
            }
            SsdAction::Minimize => {
                window_rc.borrow_mut().minimized = true;
            }
            SsdAction::Move => return false,
        }
        self.dirty = true;
        true
    }

    /// True while a title-bar drag is in progress.
    pub fn is_move_dragging(&self) -> bool {
        self.move_drag.is_some() || self.move_drag_x11.is_some()
    }

    /// Start a title-bar drag on the window under `pointer`, if any.
    /// Returns `true` if a drag was started. Only applies to floating
    /// windows in desktop mode.
    pub fn start_move_drag(&mut self, pointer: Point<f64, Logical>) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        if let Some((wl, action)) = self.x11_windows.ssd_hit_test(pointer) {
            if !self.x11_hit_occluded(&wl, pointer) {
                if action != crate::ssd::SsdAction::Move { return false; }
                let Some(start_loc) = self.x11_windows.find(&wl).map(|s| s.geometry().loc) else {
                    return false;
                };
                self.set_focus(Some(FocusTarget::X11(wl.clone())));
                self.move_drag_x11 = Some(MoveDragX11 { wl, start_pointer: pointer, start_loc });
                self.dirty = true;
                return true;
            }
        }

        let Some(( window_rc, action )) = self.ssd_window_hit_test( pointer ) else {
            return false;
        };
        if action != crate::ssd::SsdAction::Move {
            return false;
        }

        // Always raise the clicked window to the front and focus it — both
        // floating and tiled. Tiled windows skip the actual drag (only
        // floating windows reposition), but the press still brings them to
        // the front, moves keyboard focus and counts as an SSD interaction
        // for the caller's double-click detection.
        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));

        let start_loc = {
            let mut w = window_rc.borrow_mut();
            if !w.floating {
                self.dirty = true;
                return true;
            }
            // Windows-style tear-off: dragging a maximized window restores
            // its previous size and repositions it so the title bar stays
            // under the pointer.
            if w.is_maximized() {
                let old_rect = w.output_rectangle();
                w.unmaximize(self.output.scale());
                let new_size = w.output_rectangle().size;
                let ratio = if old_rect.size.w > 0 {
                    ((pointer.x as i32) - old_rect.loc.x) as f64 / old_rect.size.w as f64
                } else {
                    0.5
                };
                let new_x = (pointer.x as i32) - (ratio * new_size.w as f64) as i32;
                let bar_h = crate::ssd::SsdDecoration::bar_height();
                let new_y = (pointer.y as i32) - bar_h / 2;
                w.set_floating_position((new_x, new_y).into());
            }
            w.output_rectangle().loc
        };

        self.move_drag = Some(MoveDrag {
            window: Rc::downgrade(&window_rc),
            start_pointer: pointer,
            start_loc,
            snap: None,
        });

        self.dirty = true;
        true
    }

    pub fn set_maximized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        if window_rc.borrow().is_maximized() {
            return;
        }
        let avail = self.output.available();
        let scale = self.output.scale();
        let bar_h = self.bar_h_for(&window_rc.borrow());
        window_rc.borrow_mut().toggle_maximize(scale, avail, bar_h);
        self.record_geometry(&window_rc.borrow());
        let wl = window_rc.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    pub fn unset_maximized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        if !window_rc.borrow().is_maximized() {
            return;
        }
        window_rc.borrow_mut().unmaximize(self.output.scale());
        self.record_geometry(&window_rc.borrow());
        let wl = window_rc.borrow().surface.surface().clone();
        self.raise_toplevel(&wl);
        self.dirty = true;
    }

    /// Bring a toplevel to the front in both the render z-order and the
    /// desktop hit-test stacking, without touching keyboard focus or the
    /// active layout. Used by client-driven (un)maximize so the window is
    /// not left buried behind another.
    fn raise_toplevel(&mut self, surface: &WlSurface) {
        self.z_promote(surface);
        if let Some(id) = self.layouts.layout_id_for(surface) {
            self.layouts.raise_to_top(id);
        }
    }

    pub fn set_minimized(&mut self, surface: &ToplevelSurface) {
        let Some(window_rc) = self.find_toplevel(surface) else { return };
        window_rc.borrow_mut().minimized = true;
        self.dirty = true;
    }

    fn find_toplevel(&self, surface: &ToplevelSurface) -> Option<Rc<RefCell<Window>>> {
        self.layouts.layouts()
            .iter()
            .flat_map(|l| l.windows())
            .find(|w| &w.borrow().surface == surface)
            .cloned()
    }

    pub fn start_move_drag_for(
        &mut self,
        surface: &ToplevelSurface,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        let Some(window_rc) = self.find_toplevel(surface) else { return false };

        let start_loc = {
            let mut w = window_rc.borrow_mut();
            if !w.floating {
                return false;
            }
            if w.is_maximized() {
                let old_rect = w.output_rectangle();
                w.unmaximize(self.output.scale());
                let new_size = w.output_rectangle().size;
                let ratio = if old_rect.size.w > 0 {
                    ((pointer.x as i32) - old_rect.loc.x) as f64 / old_rect.size.w as f64
                } else {
                    0.5
                };
                let new_x = (pointer.x as i32) - (ratio * new_size.w as f64) as i32;
                let new_y = (pointer.y as i32) - 16;
                w.set_floating_position((new_x, new_y).into());
            }
            w.output_rectangle().loc
        };

        self.move_drag = Some(MoveDrag {
            window: Rc::downgrade(&window_rc),
            start_pointer: pointer,
            start_loc,
            snap: None,
        });

        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
        self.dirty = true;
        true
    }

    /// Start an interactive resize on `surface` with the given `edge`, anchored
    /// at the current `pointer` position. Mirrors `start_move_drag_for` for the
    /// `xdg_toplevel.resize` request issued by CSD clients (Brave/Electron drag
    /// from a corner of their own header bar). The hit-test based
    /// `start_resize_drag` keeps serving SSD pointer presses on the outer edge
    /// band; this surface-driven variant exists so CSD clients can arm a resize
    /// without ever entering the SSD strip.
    pub fn start_resize_drag_for(
        &mut self,
        surface: &ToplevelSurface,
        edge: crate::ssd::ResizeEdge,
        pointer: Point<f64, Logical>,
    ) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        let Some(window_rc) = self.find_toplevel(surface) else { return false };

        let scale = self.canvas.scale();
        let start_rect = {
            let w = window_rc.borrow();
            if !w.floating || w.minimized || w.is_maximized() {
                return false;
            }
            w.bounds(scale)
        };

        self.resize_drag = Some(ResizeDrag {
            window: Rc::downgrade(&window_rc),
            edge,
            start_pointer: pointer,
            start_rect,
        });

        self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
        window_rc.borrow_mut().set_resizing(true);
        self.dirty = true;
        true
    }

    /// Update the dragged window's position from the current pointer.
    pub fn update_move_drag(&mut self, pointer: Point<f64, Logical>) {
        if let Some(drag) = &self.move_drag_x11 {
            let dx = (pointer.x - drag.start_pointer.x).round() as i32;
            let dy = (pointer.y - drag.start_pointer.y).round() as i32;
            let mut loc = drag.start_loc;
            loc.x += dx;
            loc.y += dy;
            if let Some(s) = self.x11_windows.find(&drag.wl) {
                let mut geo = s.geometry();
                geo.loc = loc;
                let _ = s.configure(geo);
            }
            self.dirty = true;
            return;
        }

        let avail = self.output.available();
        let new_snap = SnapZone::detect(pointer, avail);

        let Some(drag) = &mut self.move_drag else { return };
        let Some(window_rc) = drag.window.upgrade() else {
            self.move_drag = None;
            return;
        };

        // Track the snap target on the drag state so the textures pass
        // can paint the live preview overlay and `end_move_drag` knows
        // what to commit. Repaint when the zone changes (preview moves).
        let snap_changed = drag.snap != new_snap;
        drag.snap = new_snap;

        let dx = (pointer.x - drag.start_pointer.x).round() as i32;
        let dy = (pointer.y - drag.start_pointer.y).round() as i32;
        let mut loc = drag.start_loc;
        loc.x += dx;
        loc.y += dy;

        // Clamp so the title bar stays at least partially reachable.
        let bar_h = self.bar_h_for(&window_rc.borrow());
        let min_y = avail.loc.y + bar_h; // window top can go down to 0 from bar's POV
        let max_y = avail.loc.y + avail.size.h - 16; // keep a sliver visible
        let min_x = avail.loc.x - 200; // allow partial offscreen
        let max_x = avail.loc.x + avail.size.w - 64;
        loc.x = loc.x.clamp(min_x, max_x);
        loc.y = loc.y.clamp(min_y, max_y);

        window_rc.borrow_mut().set_floating_position(loc);
        self.dirty |= snap_changed;
        self.dirty = true;
    }

    /// End a title-bar drag. When the cursor was hovering over a snap
    /// zone, commit the snap as a maximize / half / quarter resize;
    /// otherwise just leave the window where the drag last placed it.
    /// The maximize path goes through the existing `pre_max_rect`
    /// machinery so the user can drag the window back off the snap to
    /// restore the original geometry.
    pub fn end_move_drag(&mut self) {
        if self.move_drag_x11.take().is_some() {
            self.dirty = true;
            return;
        }
        let Some(drag) = self.move_drag.take() else { return };
        let Some(window_rc) = drag.window.upgrade() else { return };

        // Commit a snap-zone target before persisting, so the saved
        // rect reflects the user's final intent (drag to half/quarter
        // / maximize), not the floating rect mid-drag.
        if let Some(zone) = drag.snap {
            let avail = self.output.available();
            let target = zone.rect(avail);
            let scale  = self.output.scale();
            let bar_h  = self.bar_h_for(&window_rc.borrow());
            let mut w = window_rc.borrow_mut();
            match zone {
                SnapZone::Maximize => {
                    // Reuse the maximize path so the user can drag the
                    // window back off the snap zone and recover the
                    // pre-snap rect via the standard restore.
                    if !w.is_maximized() {
                        w.toggle_maximize(scale, avail, bar_h);
                    }
                }
                _ => {
                    w.set_dimensions(scale, target);
                }
            }
        }
        // Persist for next-launch restore (eydos #7). Even a no-snap
        // drag updates the saved rect: the user moved the window, we
        // remember where they put it. `record_geometry` is a no-op
        // when app_id is empty, so the very first commit (before the
        // client has set its app_id) doesn't store an unkeyable rect.
        self.record_geometry(&window_rc.borrow());
        self.dirty = true;
    }

    pub fn is_resize_dragging(&self) -> bool {
        self.resize_drag.is_some() || self.resize_drag_x11.is_some()
    }

    /// Look for a resize grab zone under `pointer` on any visible floating
    /// window in the active layout; start a resize drag if found.
    ///
    /// Uses the same topmost-first-with-body-occlusion walk as
    /// [`resize_cursor_edge`] so a resize drag cannot start on a window whose
    /// grab band is covered by another window stacked on top of it.
    pub fn start_resize_drag(&mut self, pointer: Point<f64, Logical>) -> bool {
        if self.canvas.is_mobile() {
            return false;
        }

        if let Some((wl, edge, rect)) = self.x11_windows.resize_hit_test(pointer) {
            if !self.x11_hit_occluded(&wl, pointer) {
                self.set_focus(Some(FocusTarget::X11(wl.clone())));
                self.resize_drag_x11 = Some(ResizeDragX11 {
                    wl, edge, start_pointer: pointer, start_rect: rect,
                });
                self.dirty = true;
                return true;
            }
        }

        let scale = self.canvas.scale();
        let bar_h = crate::ssd::SsdDecoration::bar_height();

        let mut hit: Option<(Rc<RefCell<Window>>, crate::ssd::ResizeEdge, Rectangle<i32, Logical>)> = None;
        'outer: for layout in self.layouts.stacking_layouts() {
            for window_rc in layout.windows() {
                let (rect, eligible, minimized, bar) = {
                    let w = window_rc.borrow();
                    let eligible = w.floating && !w.minimized && !w.is_maximized();
                    let bar = if w.suppress_ssd(&self.csd_apps) { 0 } else { bar_h };
                    (w.bounds(scale), eligible, w.minimized, bar)
                };
                if minimized { continue; }
                if crate::ssd::body_hit_test(rect, pointer, bar) {
                    break 'outer;
                }
                if !eligible { continue; }
                if let Some(edge) = crate::ssd::resize_hit_test(rect, pointer, bar) {
                    hit = Some((window_rc.clone(), edge, rect));
                    break 'outer;
                }
            }
        }

        if let Some((window_rc, edge, rect)) = hit {
            self.resize_drag = Some(ResizeDrag {
                window: Rc::downgrade(&window_rc),
                edge,
                start_pointer: pointer,
                start_rect: rect,
            });
            self.set_focus(Some(FocusTarget::Layout(Rc::downgrade(&window_rc))));
            window_rc.borrow_mut().set_resizing(true);
            self.dirty = true;
            return true;
        }
        false
    }

    pub fn update_resize_drag(&mut self, pointer: Point<f64, Logical>) {
        if let Some(drag) = &self.resize_drag_x11 {
            use crate::ssd::ResizeEdge;
            let dx = (pointer.x - drag.start_pointer.x).round() as i32;
            let dy = (pointer.y - drag.start_pointer.y).round() as i32;
            let start = drag.start_rect;
            let (mut x, mut y, mut w, mut h) = (start.loc.x, start.loc.y, start.size.w, start.size.h);
            match drag.edge {
                ResizeEdge::E  => { w = start.size.w + dx; }
                ResizeEdge::W  => { x = start.loc.x + dx; w = start.size.w - dx; }
                ResizeEdge::S  => { h = start.size.h + dy; }
                ResizeEdge::N  => { y = start.loc.y + dy; h = start.size.h - dy; }
                ResizeEdge::SE => { w = start.size.w + dx; h = start.size.h + dy; }
                ResizeEdge::NE => { w = start.size.w + dx; y = start.loc.y + dy; h = start.size.h - dy; }
                ResizeEdge::SW => { x = start.loc.x + dx; w = start.size.w - dx; h = start.size.h + dy; }
                ResizeEdge::NW => { x = start.loc.x + dx; w = start.size.w - dx; y = start.loc.y + dy; h = start.size.h - dy; }
            }
            let (min_w, min_h) = (100, 80);
            if w < min_w {
                if matches!(drag.edge, ResizeEdge::W | ResizeEdge::SW | ResizeEdge::NW) {
                    x = start.loc.x + start.size.w - min_w;
                }
                w = min_w;
            }
            if h < min_h {
                if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
                    y = start.loc.y + start.size.h - min_h;
                }
                h = min_h;
            }
            let bar_h = crate::ssd::SsdDecoration::bar_height();
            if y < bar_h { y = bar_h; }
            let rect = Rectangle::<i32, Logical>::new((x, y).into(), (w, h).into());
            if let Some(s) = self.x11_windows.find(&drag.wl) {
                let _ = s.configure(rect);
            }
            self.dirty = true;
            return;
        }

        let Some(drag) = &self.resize_drag else { return };
        let Some(window_rc) = drag.window.upgrade() else {
            self.resize_drag = None;
            return;
        };

        use crate::ssd::ResizeEdge;
        let dx = (pointer.x - drag.start_pointer.x).round() as i32;
        let dy = (pointer.y - drag.start_pointer.y).round() as i32;

        let start = drag.start_rect;
        let (mut x, mut y, mut w, mut h) =
            (start.loc.x, start.loc.y, start.size.w, start.size.h);

        match drag.edge {
            ResizeEdge::E                          => { w = start.size.w + dx; }
            ResizeEdge::W                          => { x = start.loc.x + dx; w = start.size.w - dx; }
            ResizeEdge::S                          => { h = start.size.h + dy; }
            ResizeEdge::N                          => { y = start.loc.y + dy; h = start.size.h - dy; }
            ResizeEdge::SE                         => { w = start.size.w + dx; h = start.size.h + dy; }
            ResizeEdge::NE                         => { w = start.size.w + dx; y = start.loc.y + dy; h = start.size.h - dy; }
            ResizeEdge::SW                         => { x = start.loc.x + dx; w = start.size.w - dx; h = start.size.h + dy; }
            ResizeEdge::NW                         => { x = start.loc.x + dx; w = start.size.w - dx; y = start.loc.y + dy; h = start.size.h - dy; }
        }

        let (min_size, max_size) = window_rc.borrow().size_constraints();
        // Toolkits that set their own minimum (Firefox: ~450×300, GTK: 360×220,
        // electron: ~320×240) used to clamp at our local minimum first because
        // we max'd against (100, 80). The .max(client) below now respects the
        // larger of the two: if the client reported a sane minimum we use
        // that; if it didn't (min_size.{w,h} == 0), we fall back to (100, 80)
        // so an absent set_min_size never leaves the window with a single-
        // pixel rect. Same idea for max — clients that set a max get clamped
        // accordingly; everything else is unbounded.
        let min_w = min_size.w.max(100);
        let min_h = min_size.h.max(80);
        let max_w = if max_size.w > 0 { max_size.w } else { i32::MAX };
        let max_h = if max_size.h > 0 { max_size.h } else { i32::MAX };

        // Clamp size, and if a west/north edge is shrinking past the min,
        // pin the opposite edge by adjusting loc.
        if w < min_w {
            if matches!(drag.edge, ResizeEdge::W | ResizeEdge::SW | ResizeEdge::NW) {
                x = start.loc.x + start.size.w - min_w;
            }
            w = min_w;
        }
        if h < min_h {
            if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
                y = start.loc.y + start.size.h - min_h;
            }
            h = min_h;
        }
        w = w.min(max_w);
        h = h.min(max_h);

        // Stop the N / NE / NW edges from pulling the window up under the
        // topbar — the topbar eats pointer events in that strip, so a
        // window whose top edge crossed into it could no longer be moved
        // or resized. Clamp `y` to the top of the available area (which
        // already accounts for the topbar height + any layer-shell
        // exclusive zones) and keep the SE corner of the window pinned by
        // shrinking `h` to compensate.
        if matches!(drag.edge, ResizeEdge::N | ResizeEdge::NE | ResizeEdge::NW) {
            let avail = self.output.available();
            if y < avail.loc.y {
                let delta = avail.loc.y - y;
                y = avail.loc.y;
                h = (h - delta).max(min_h);
            }
        }

        let rect = Rectangle::<i32, Logical>::new((x, y).into(), (w, h).into());

        // Trace a single line per first-of-kind edge so a stuck resize is
        // diagnosable from the journal without flooding the log on every
        // pointer motion. Firefox not resizing is the canonical bug here:
        // the configure goes out, the client acks a size that doesn't match
        // (toolkit-rounded, fractional-scale-snapped, etc) and the
        // transaction never completes. The log captures requested vs acked
        // so the divergence is visible.
        {
            let w_borrow = window_rc.borrow();
            let app_id = w_borrow.app_id.as_deref().unwrap_or("?");
            let acked  = w_borrow.acked_size;
            tracing::debug!(
                "resize {app_id}: edge={:?} req=({}x{}) acked=({}x{}) min=({}x{}) max=({}x{})",
                drag.edge, w, h, acked.w, acked.h,
                min_size.w, min_size.h, max_size.w, max_size.h,
            );
        }

        window_rc.borrow_mut().set_dimensions_interactive(self.output.scale(), rect);
        self.dirty = true;
    }

    pub fn end_resize_drag(&mut self) {
        if self.resize_drag_x11.take().is_some() {
            self.dirty = true;
            return;
        }
        let Some(drag) = self.resize_drag.take() else { return };
        if let Some(window_rc) = drag.window.upgrade() {
            window_rc.borrow_mut().set_resizing(false);
            // The rect was committed live during update_resize_drag via
            // set_dimensions_interactive — by the time end_resize_drag
            // runs the window already holds the user's final size. Save
            // it so the next launch of the same app picks it up.
            self.record_geometry(&window_rc.borrow());
        }
    }
}

#[derive(Debug)]
struct Transaction {
    view: Option<View>,
}

impl Transaction {
    fn new() -> Self {
        Self { view: None }
    }
}

#[derive(Default, Debug)]
enum View {
    Overview(Overview),
    DragAndDrop(DragAndDrop),
    Fullscreen(Rc<RefCell<Window>>),
    Lock(Option<Box<Window<LockSurface>>>),
    #[default]
    Workspace,
}

#[cfg(test)]
mod snap_tests {
    use super::SnapZone;
    use smithay::utils::{Logical, Point, Rectangle};

    fn avail() -> Rectangle<i32, Logical> {
        // 1920×1080 starting at (0, 28) — the topbar offset typical for the
        // built-in 28 px topbar. Tests exercise the absolute coordinates so
        // the available rect's origin matters, not just the size.
        Rectangle::new((0, 28).into(), (1920, 1080).into())
    }

    fn p(x: f64, y: f64) -> Point<f64, Logical> {
        Point::from((x, y))
    }

    // ── detect ──

    #[test]
    fn detect_top_edge_returns_maximize() {
        // Top edge band is 12 px below avail.loc.y (= 28). 5 px above: still
        // outside the band? actually y < t means we want y - t < 12; y=30
        // gives 30-28 = 2 < 12, so this is "inside" the top band → Maximize.
        assert_eq!(SnapZone::detect(p(960.0, 30.0), avail()), Some(SnapZone::Maximize));
    }

    #[test]
    fn detect_left_edge_returns_left() {
        assert_eq!(SnapZone::detect(p(5.0, 540.0), avail()), Some(SnapZone::Left));
    }

    #[test]
    fn detect_right_edge_returns_right() {
        assert_eq!(SnapZone::detect(p(1915.0, 540.0), avail()), Some(SnapZone::Right));
    }

    #[test]
    fn detect_bottom_edge_returns_none() {
        // Bottom edge intentionally inert (clashes with mobile gesture-handle
        // strip; users brush it incidentally while moving windows).
        assert_eq!(SnapZone::detect(p(960.0, 1100.0), avail()), None);
    }

    #[test]
    fn detect_centre_returns_none() {
        assert_eq!(SnapZone::detect(p(960.0, 540.0), avail()), None);
    }

    #[test]
    fn detect_corner_takes_precedence_over_edge() {
        // (5, 30) is near both the top edge AND the left edge; it must
        // resolve to NW (the corner), not Maximize or Left.
        assert_eq!(SnapZone::detect(p(5.0, 30.0), avail()), Some(SnapZone::NW));
    }

    #[test]
    fn detect_all_four_corners() {
        assert_eq!(SnapZone::detect(p(5.0,    30.0),   avail()), Some(SnapZone::NW));
        assert_eq!(SnapZone::detect(p(1915.0, 30.0),   avail()), Some(SnapZone::NE));
        // SW / SE corners need the cursor near the bottom edge band; the
        // bottom *edge* is inert but a *corner* still arms.
        assert_eq!(SnapZone::detect(p(5.0,    1100.0), avail()), Some(SnapZone::SW));
        assert_eq!(SnapZone::detect(p(1915.0, 1100.0), avail()), Some(SnapZone::SE));
    }

    // ── rect ──

    #[test]
    fn rect_maximize_fills_available() {
        assert_eq!(SnapZone::Maximize.rect(avail()), avail());
    }

    #[test]
    fn rect_left_is_left_half() {
        let r = SnapZone::Left.rect(avail());
        assert_eq!(r.loc.x, 0);
        assert_eq!(r.loc.y, 28);
        assert_eq!(r.size.w, 960);
        assert_eq!(r.size.h, 1080);
    }

    #[test]
    fn rect_right_covers_remaining_width() {
        // Right half should reach the right edge — including any odd
        // pixel left over by integer division (`w - half_w`).
        let r = SnapZone::Right.rect(avail());
        assert_eq!(r.loc.x, 960);
        assert_eq!(r.size.w, 960);
        assert_eq!(r.loc.x + r.size.w, 1920);
    }

    #[test]
    fn rect_corners_partition_available() {
        // The four corners must perfectly tile the available rect — no
        // gap rows / columns from off-by-one rounding.
        let nw = SnapZone::NW.rect(avail());
        let ne = SnapZone::NE.rect(avail());
        let sw = SnapZone::SW.rect(avail());
        let se = SnapZone::SE.rect(avail());

        assert_eq!(nw.loc, avail().loc);
        assert_eq!(ne.loc.x, nw.loc.x + nw.size.w);
        assert_eq!(sw.loc.y, nw.loc.y + nw.size.h);
        assert_eq!(se.loc.x, sw.loc.x + sw.size.w);
        assert_eq!(se.loc.y, ne.loc.y + ne.size.h);

        // Total width / height of the four-quarter mosaic equals the
        // available rect's width / height.
        assert_eq!(nw.size.w + ne.size.w, avail().size.w);
        assert_eq!(nw.size.h + sw.size.h, avail().size.h);
    }
}
