🔒 Repository is read-only – file editing is disabled.

luna/de/selene/src/compositor.rs main

1217 lines Raw ← Back
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
//! `wl_compositor`, `wl_surface`, `wl_region`, `wl_callback` — rdzeń powierzchni.

use std::sync::Mutex;

use libluna_core::Rect;
use libluna_render::ImageData;
use wayland_protocols::xdg::shell::server::{xdg_popup, xdg_surface, xdg_toplevel};
use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1;
use wayland_server::{
    protocol::{wl_buffer, wl_callback, wl_compositor, wl_region, wl_surface},
    Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
};

use crate::layer::LayerData;
use crate::shell::{XdgSurfaceData, XdgToplevelData};
use crate::shm::BufferData;
use crate::state::{State, Window};
use crate::sync::LockExt;

/// Dane powierzchni (`wl_surface`).
pub struct SurfaceData {
    /// Oczekujące `frame`-callbacki do wywołania przy następnym `commit`.
    pub frames: Mutex<Vec<wl_callback::WlCallback>>,
    /// Powiązana powierzchnia `xdg_surface` (jeśli klient ją utworzyła).
    pub xdg_surface: Mutex<Option<xdg_surface::XdgSurface>>,
    /// Ostatnio przypięty bufor (`wl_surface.attach`).
    pub buffer: Mutex<Option<wl_buffer::WlBuffer>>,
    /// Czy bieżący bufor czeka jeszcze na `wl_buffer.release`.
    ///
    /// Klient często robi `commit` bez nowego `attach` (np. po `ack_configure` albo
    /// zmianie regionu wejścia). Wysłanie `release` przy każdym `commit` kończy się
    /// **podwójnym** zwolnieniem bufora — GTK pada wtedy na asercji cairo
    /// (`cairo_surface_destroy`). Zwolnienie musi być dokładnie raz na `attach`.
    pub release_needed: Mutex<bool>,
    /// Slot okna w modelu (przydzielany przy pierwszym commicie).
    pub slot: Mutex<Option<usize>>,
    /// Powiązana powierzchnia warstwowa (`wlr-layer-shell`), jeśli klient ją utworzył.
    pub layer: Mutex<Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>>,
    /// Uchwyty `zwlr_foreign_toplevel` (gdy to okno najwyższego poziomu).
    pub toplevel: Mutex<Option<crate::foreign_toplevel::ToplevelInfo>>,
    /// Region wejścia (`wl_surface.set_input_region`); `None` = cała powierzchnia.
    pub input: Mutex<Option<Vec<Rect>>>,
    /// Czy to powierzchnia okna X11 (XWayland) — nie ma `xdg_surface`, a mimo to jest oknem.
    pub xwayland: Mutex<bool>,
    /// Serial powierzchni nadany przez `xwayland_surface_v1.set_serial` — po nim XWM
    /// wiąże okno X11 z tą powierzchnią.
    pub x11_serial: Mutex<u64>,
    /// Rola powierzchni potomnej (`wl_subsurface`): pozycja, tryb `sync`, obrazy.
    pub subsurface: Mutex<Option<crate::subsurface::SubsurfaceState>>,
    /// Powierzchnie potomne w kolejności stosu (od dołu).
    pub children: Mutex<Vec<wayland_server::protocol::wl_surface::WlSurface>>,
    /// Ostatni obraz **własnego** bufora (bez dzieci) — potrzebny do przemalowania
    /// rodzica, gdy dziecko w trybie `desync` zrobi commit.
    pub base_image: Mutex<Option<std::sync::Arc<ImageData>>>,
}

impl SurfaceData {
    pub fn new() -> Self {
        Self {
            frames: Mutex::new(Vec::new()),
            xdg_surface: Mutex::new(None),
            buffer: Mutex::new(None),
            release_needed: Mutex::new(false),
            slot: Mutex::new(None),
            layer: Mutex::new(None),
            toplevel: Mutex::new(None),
            input: Mutex::new(None),
            xwayland: Mutex::new(false),
            x11_serial: Mutex::new(0u64),
            subsurface: Mutex::new(None),
            children: Mutex::new(Vec::new()),
            base_image: Mutex::new(None),
        }
    }

    /// Zwraca `true` (raz) i czyści flagę — wołający ma wysłać `wl_buffer.release`.
    pub(crate) fn take_release(&self) -> bool {
        let mut f = self.release_needed.lock_or_recover();
        if *f {
            *f = false;
            true
        } else {
            false
        }
    }
}

impl Default for SurfaceData {
    fn default() -> Self {
        Self::new()
    }
}

impl GlobalDispatch<wl_compositor::WlCompositor, ()> for State {
    fn bind(
        _state: &mut Self,
        _handle: &DisplayHandle,
        _client: &Client,
        resource: New<wl_compositor::WlCompositor>,
        _data: &(),
        data_init: &mut DataInit<'_, Self>,
    ) {
        data_init.init(resource, ());
    }
}

impl Dispatch<wl_compositor::WlCompositor, ()> for State {
    fn request(
        state: &mut Self,
        _client: &Client,
        _resource: &wl_compositor::WlCompositor,
        request: wl_compositor::Request,
        _data: &(),
        _handle: &DisplayHandle,
        data_init: &mut DataInit<'_, Self>,
    ) {
        match request {
            wl_compositor::Request::CreateSurface { id } => {
                data_init.init(id, SurfaceData::new());
                state.surfaces += 1;
            }
            wl_compositor::Request::CreateRegion { id } => {
                data_init.init(id, RegionRects::default());
            }
            _ => {}
        }
    }
}

/// Prostokąty regionu `wl_region` (współdzielone; klient może je zmieniać).
pub type RegionRects = std::sync::Arc<Mutex<Vec<Rect>>>;

impl Dispatch<wl_region::WlRegion, RegionRects> for State {
    fn request(
        _state: &mut Self,
        _client: &Client,
        _resource: &wl_region::WlRegion,
        request: wl_region::Request,
        rects: &RegionRects,
        _handle: &DisplayHandle,
        _data_init: &mut DataInit<'_, Self>,
    ) {
        let mut r = rects.lock_or_recover();
        match request {
            wl_region::Request::Add {
                x,
                y,
                width,
                height,
            } => r.push(Rect::from_xywh(
                x as f64,
                y as f64,
                width as f64,
                height as f64,
            )),
            // `subtract` upraszczamy (rzadkie w regionach wejścia).
            wl_region::Request::Subtract { .. } => {}
            wl_region::Request::Destroy => r.clear(),
            _ => {}
        }
    }
}

impl Dispatch<wl_callback::WlCallback, ()> for State {
    fn request(
        _state: &mut Self,
        _client: &Client,
        _resource: &wl_callback::WlCallback,
        _request: wl_callback::Request,
        _data: &(),
        _handle: &DisplayHandle,
        _data_init: &mut DataInit<'_, Self>,
    ) {
    }
}

/// Górny limit oczekujących frame-callbacków na powierzchnię (ochrona przed OOM).
const MAX_PENDING_FRAMES: usize = 256;

impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
    fn request(
        state: &mut Self,
        _client: &Client,
        resource: &wl_surface::WlSurface,
        request: wl_surface::Request,
        data: &SurfaceData,
        handle: &DisplayHandle,
        data_init: &mut DataInit<'_, Self>,
    ) {
        match request {
            wl_surface::Request::Frame { callback } => {
                let cb = data_init.init(callback, ());
                let mut frames = data.frames.lock_or_recover();
                // Limit oczekujących callbacków — klient bez `commit` nie wysypie pamięci.
                if frames.len() >= MAX_PENDING_FRAMES {
                    let _ = cb.send_event(wl_callback::Event::Done {
                        callback_data: state.time_ms(),
                    });
                } else {
                    frames.push(cb);
                }
            }
            wl_surface::Request::Attach { buffer, .. } => {
                let mut cur = data.buffer.lock_or_recover();
                let mut need = data.release_needed.lock_or_recover();
                // Poprzedni, jeszcze nieoddany bufor oddajemy klientowi teraz — inaczej
                // jego pula buforów się wyczerpuje i aplikacja staje.
                if *need {
                    if let Some(prev) = cur.take() {
                        let _ = prev.send_event(wl_buffer::Event::Release);
                    }
                }
                *need = buffer.is_some();
                *cur = buffer;
            }
            wl_surface::Request::SetInputRegion { region } => {
                // `None` = cała powierzchnia; inaczej migawka prostokątów regionu.
                *data.input.lock_or_recover() = region
                    .as_ref()
                    .and_then(|r| r.data::<RegionRects>().map(|m| m.lock_or_recover().clone()));
            }
            wl_surface::Request::Commit => {
                // Wywołaj oczekujące frame-callbacki.
                let now = state.time_ms();
                for cb in data.frames.lock_or_recover().drain(..) {
                    let _ = cb.send_event(wl_callback::Event::Done { callback_data: now });
                }
                // Pierwszy commit powierzchni z `xdg_surface` → wyślij konfigurację.
                maybe_configure_xdg(state, data);
                // Powierzchnia potomna nie tworzy okna — jej bufor składamy na rodzicu.
                if data.subsurface.lock_or_recover().is_some() {
                    crate::subsurface::commit(state, data);
                } else {
                    // Skomponuj okno (jeśli to toplevel z buforem).
                    composite(state, resource, data, handle);
                }
            }
            wl_surface::Request::Destroy => {
                // Oddaj bufor, jeśli jeszcze go trzymamy.
                if data.take_release() {
                    if let Some(b) = data.buffer.lock_or_recover().take() {
                        let _ = b.send_event(wl_buffer::Event::Release);
                    }
                }
            }
            _ => {}
        }
    }

    /// Gdy powierzchnia znika — bo klient ją zniszczył **albo padł** (crash, `kill`) —
    /// chowamy okno i zgłaszamy zamknięcie paskowi. Bez tego na ekranie zostaje
    /// „duch”: nieruchomy pasek tytułu i treść po zamkniętej aplikacji.
    fn destroyed(
        state: &mut Self,
        _client: wayland_server::backend::ClientId,
        resource: &wl_surface::WlSurface,
        data: &SurfaceData,
    ) {
        // Powierzchnia potomna znika — odczep ją od rodzica i przemaluj go.
        if let Some(sub) = data.subsurface.lock_or_recover().take() {
            if let Some(pd) = sub.parent.data::<SurfaceData>() {
                pd.children.lock_or_recover().retain(|s| s != resource);
            }
            crate::subsurface::repaint_parent(state, &sub.parent);
        }
        // Okno w modelu chowamy (nie usuwamy — indeks slotu musi zostać stabilny).
        // Ważne: guard `data.slot` zwalniamy **przed** `clear_focus_if` — inaczej
        // `broadcast_states` próbuje zamknąć ten sam mutex i wątek wisi.
        let slot = *data.slot.lock_or_recover();
        if let Some(slot) = slot {
            if let Some(win) = state.windows.lock_or_recover().get_mut(slot) {
                win.mapped = false;
                win.minimized = false;
                win.maximized = false;
                win.fullscreen = false;
                win.input_capture = false;
                win.input = None;
                // Zwolnij bufor ostatniej klatki (może mieć kilka MB).
                win.data = std::sync::Arc::new(ImageData::empty(1, 1));
            }
            // Fokus nie może wskazywać na powierzchnię, której już nie ma.
            crate::seat::clear_focus_if(state, slot);
            if std::env::var("SELENE_STATS").is_ok() {
                eprintln!("selene/win: powierzchnia zniszczona slot={slot}");
            }
        }
        // Pasek (foreign-toplevel) dowiaduje się, że okno zniknęło.
        crate::foreign_toplevel::close_for_data(data);
    }
}

/// Czy rysujemy nasze dekoracje (pasek tytułu) dla tego okna.
///
/// * popupy — nigdy (zamykane, bez paska),
/// * okna X11 — tak, chyba że klient wyraźnie poprosił o `client_side`,
/// * okna Waylanda — tylko gdy negocjowały `server_side`.
fn wants_ssd(is_x11: bool, is_popup: bool, deco: Option<crate::decoration::DecoMode>) -> bool {
    use crate::decoration::DecoMode;
    if is_popup {
        false
    } else if is_x11 {
        deco != Some(DecoMode::Client)
    } else {
        deco == Some(DecoMode::Server)
    }
}

/// Skomponuj powierzchnię do modelu okien (toplevel, popup lub powierzchnia warstwowa).
fn composite(
    state: &mut State,
    surface: &wl_surface::WlSurface,
    data: &SurfaceData,
    handle: &DisplayHandle,
) {
    // Powierzchnia warstwowa (panel/dock) — własna ścieżka (bez xdg_surface).
    if let Some(ls) = data.layer.lock_or_recover().clone() {
        composite_layer(state, surface, data, &ls);
        return;
    }
    // Okno X11 (XWayland) rozpoznajemy po znaczniku z `xwayland_shell_v1`, ale tylko gdy
    // nie jest to `xdg_popup` — popupy Xwaylanda (menu, podpowiedzi) mają własny
    // `xdg_surface` i idą zwykłą ścieżką Waylanda.
    let xdg_surface = data.xdg_surface.lock_or_recover().clone();
    let xdg_is_popup = xdg_surface
        .as_ref()
        .and_then(|xs| xs.data::<XdgSurfaceData>())
        .map(|d| *d.is_popup.lock_or_recover())
        .unwrap_or(false);
    let is_x11 = *data.xwayland.lock_or_recover() && !xdg_is_popup;
    // Okna X11: brak `xdg_surface`, ale mamy XWM — z niego bierzemy tytuł, typ okna
    // i informację, czy klient rysuje własne dekoracje (CSD przez `_MOTIF_WM_HINTS`).
    let mut x11_title = String::new();
    let mut x11_popup = false;
    let mut x11_ssd = true;
    let mut x11_pos: Option<(f64, f64)> = None;
    if is_x11 {
        let serial = *data.x11_serial.lock_or_recover();
        if let Some(info) = crate::xwm::global().and_then(|x| x.info_for_serial(serial)) {
            // Menu i podpowiedzi (`override_redirect`/`Transient`) to popupy — bez
            // dekoracji i bez wpisu w panelu. Okno z własnymi dekoracjami (CSD) jest
            // zwykłym oknem, tylko nie dokładamy mu drugiego paska tytułu.
            x11_popup = info.override_redirect || info.kind == crate::xwm::X11Kind::Transient;
            x11_ssd = info.decorated();
            x11_title = info.title;
            // Menu X11 ma współrzędne absolutne — stawiamy je tam, gdzie klient chce,
            // a nie w kaskadzie (inaczej klik w widoczne menu trafia obok).
            if x11_popup && info.width > 0 && info.height > 0 {
                x11_pos = Some((info.x as f64, info.y as f64));
            }
        }
    }
    let (is_toplevel, is_popup) = if is_x11 {
        (true, x11_popup)
    } else {
        let Some(xdg_surface) = xdg_surface.as_ref() else {
            return;
        };
        match xdg_surface.data::<XdgSurfaceData>() {
            Some(d) => (
                d.toplevel.lock_or_recover().is_some(),
                *d.is_popup.lock_or_recover(),
            ),
            None => (false, false),
        }
    };
    if !is_toplevel && !is_popup {
        return;
    }
    let Some(buffer) = data.buffer.lock_or_recover().clone() else {
        return;
    };
    let Some(buf_data) = buffer.data::<BufferData>() else {
        return;
    };
    let (bw, bh) = (buf_data.width, buf_data.height);
    let Some(rgba) = buf_data.to_rgba() else {
        return;
    };
    let image = std::sync::Arc::new(ImageData::new(bw as u32, bh as u32, rgba));
    // Powierzchnie potomne (`wl_subsurface`) składamy na własny bufor rodzica:
    // najpierw aplikujemy stan oczekujący (`sync`), potem nakładamy obrazy dzieci.
    crate::subsurface::apply_pending(surface);
    *data.base_image.lock_or_recover() = Some(image.clone());
    let image = crate::subsurface::overlay_children(surface, image);
    let z = state.next_z();
    let title = if is_x11 {
        x11_title
    } else if is_toplevel {
        xdg_surface
            .as_ref()
            .and_then(|xs| xs.data::<XdgSurfaceData>())
            .and_then(|d| {
                d.toplevel.lock_or_recover().as_ref().and_then(|tl| {
                    tl.data::<XdgToplevelData>()
                        .map(|td| td.title.lock_or_recover().clone())
                })
            })
            .unwrap_or_default()
    } else {
        String::new()
    };

    let count = cascade_slots(&state.windows.lock_or_recover());
    // Tryb dekoracji zgłoszony przez klienta (jeśli ma obiekt `zxdg_toplevel_decoration_v1`).
    // Okna X11 też mają `xdg_toplevel` (tworzy go Xwayland), więc czytamy go tak samo.
    let deco_mode = xdg_surface
        .as_ref()
        .and_then(|xs| xs.data::<XdgSurfaceData>())
        .and_then(|d| {
            let tl = d.toplevel.lock_or_recover();
            tl.as_ref()
                .and_then(|tl| tl.data::<XdgToplevelData>())
                .map(|td| *td.deco.lock_or_recover())
        })
        .flatten();
    // Nasze dekoracje (SSD, tj. pasek tytułu) rysujemy:
    //  * oknom X11 — Xwayland sam nic nie rysuje (czeka na menedżera okien),
    //  * klientom Waylanda, które negocjowały tryb `server_side`.
    // Gdy klient X11 wyraźnie poprosił o `client_side` (`_MOTIF_WM_HINTS`), ustępujemy —
    // bez dublowania paska.
    let ssd = if is_x11 {
        x11_ssd
    } else {
        wants_ssd(false, is_popup, deco_mode)
    };
    // Widoczna geometria (`xdg_surface.set_window_geometry`). Okna z własnymi
    // dekoracjami (GTK/libadwaita, Firefox, Electron) mają bufor większy od okna
    // o cienie CSD. Model trzyma **widoczny** prostokąt (to on idzie w `configure`
    // i wyznacza trafienia), a bufor rysujemy przesunięty o `off`.
    let ((off_x, off_y), (mut w, mut h)) = if !is_x11 {
        let geom = xdg_surface
            .as_ref()
            .and_then(|xs| xs.data::<XdgSurfaceData>())
            .and_then(|d| *d.window_geometry.lock_or_recover());
        match geom {
            // Geometria spoza bufora bywa skutkiem wyścigu (klient ustawia ją przed
            // `attach`) — bierzemy tylko sensowne wartości.
            Some(g) if g.width() > 0.0 && g.height() > 0.0 => (
                (g.x(), g.y()),
                (
                    (g.width() as i32).min(bw).max(1),
                    (g.height() as i32).min(bh).max(1),
                ),
            ),
            _ => ((0.0, 0.0), (bw, bh)),
        }
    } else {
        ((0.0, 0.0), (bw, bh))
    };
    // Popup: pełne pozycjonowanie wg `xdg_positioner` (kotwica, grawitacja, flip/slide).
    // Menu i palety poleceń (Zed, VS Code, GTK) dostają wreszcie właściwe miejsce.
    let popup_pos = if is_popup {
        xdg_surface
            .as_ref()
            .map(|xs| popup_position(state, xs, (w as f64, h as f64)))
    } else {
        None
    };
    let (px, py) = match &popup_pos {
        Some((pos, _)) => *pos,
        None => x11_pos.unwrap_or_else(|| cascade_position(state, count)),
    };
    // Nowe okno nie może być większe niż obszar roboczy monitora — inaczej jego dolna
    // część (i przyciski klienta) ląduje poza ekranem. Klient dostanie mniejszy
    // `configure` i dorysuje treść w nowym rozmiarze.
    let mut shrunk = false;
    let new_window = data.slot.lock_or_recover().is_none();
    if new_window && !is_popup {
        let bar = if ssd { crate::state::TITLEBAR } else { 0.0 };
        let (ox, oy, ow, oh) = {
            let outs = state.outputs.lock_or_recover();
            let idx = crate::monitors::output_at(&outs, px, py);
            match outs.get(idx).or_else(|| outs.first()) {
                Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
                None => (0.0, 0.0, 1280.0, 800.0),
            }
        };
        // Miejsce od pozycji okna do dolnej/prawej krawędzi obszaru roboczego —
        // samo ograniczenie do wysokości monitora nie wystarczy, bo okno startuje
        // kilkadziesiąt pikseli niżej (kaskada pod panelem).
        let max_w = (ox + ow - px).max(160.0);
        let max_h = (oy + oh - py - bar).max(120.0);
        let fit_w = (f64::from(w)).min(max_w) as i32;
        let fit_h = (f64::from(h)).min(max_h) as i32;
        if fit_w != w || fit_h != h {
            shrunk = true;
            w = fit_w;
            h = fit_h;
        }
    }
    // Stan okna **przed** aktualizacją — po niej `upsert_window` nadpisuje rozmiar,
    // więc porównanie „czy geometria się zmieniła” musi być zrobione wcześniej.
    let prev_geom = data.slot.lock_or_recover().and_then(|i| {
        state.windows.lock_or_recover().get(i).map(|win| {
            (
                win.width,
                win.height,
                win.data.width as i32,
                win.data.height as i32,
                win.image_offset,
            )
        })
    });
    let (slot, created) = upsert_window(
        state, surface, data, px, py, w, h, z, &title, is_popup, false, false, ssd, image,
    );
    // Bufor rysujemy z przesunięciem cieni CSD — bez tego okno „uciekało” o kilkanaście
    // pikseli, a po maksymalizacji prawa krawędź z przyciskami ✕/□ wychodziła za ekran.
    let mut pos = (px, py);
    if let Some(win) = state.windows.lock_or_recover().get_mut(slot) {
        win.image_offset = (off_x, off_y);
        pos = (win.x, win.y);
    }
    let geom = (w, h, bw, bh, (off_x, off_y));
    if prev_geom.is_some() && prev_geom != Some(geom) && std::env::var("SELENE_STATS").is_ok() {
        eprintln!(
            "selene/win: geometria \"{title}\" widoczne={}x{} bufor={}x{} @ ({:.0},{:.0}) cien=({:.0},{:.0})",
            w, h, bw, bh, pos.0, pos.1, off_x, off_y
        );
    }
    // Diagnostyka: co robimy z danym oknem (X11 czy Wayland, tryb dekoracji, czy nasz pasek).
    if created && std::env::var("SELENE_STATS").is_ok() {
        let geom = state
            .windows
            .lock_or_recover()
            .get(slot)
            .map(|win| (win.x, win.y, win.width, win.height));
        if let Some((x, y, cw, ch)) = geom {
            eprintln!(
                "selene/win: \"{}\" x11={} popup={} deco={:?} ssd={} widoczne={}x{} @ ({:.0},{:.0}) cien=({:.0},{:.0})",
                title, is_x11, is_popup, deco_mode, ssd, cw, ch, x, y, off_x, off_y
            );
        }
    }
    // Xwayland **nie tworzy** `xdg_toplevel`, więc okna X11 nie są rejestrowane
    // w `foreign_toplevel` przez `shell.rs` — a bez tego nie ma ich w pasku zadań.
    // Rejestrujemy je tutaj (tytuł i stan aktualizuje później `xwm::sync`).
    if is_x11 && created && !is_popup {
        let first = {
            let mut t = data.toplevel.lock_or_recover();
            let first = t.is_none();
            if first {
                *t = Some(crate::foreign_toplevel::ToplevelInfo {
                    title: title.clone(),
                    ..Default::default()
                });
            }
            first
        };
        if first {
            let managers = state.toplevel_managers.clone();
            for mgr in &managers {
                crate::foreign_toplevel::announce(state, handle, mgr, surface);
            }
        }
    }
    // Popup: wyślij konfigurację (rozmiar + pozycja **względem geometrii rodzica**).
    if is_popup && created {
        if let (Some(xs), Some((abs, origin))) = (xdg_surface.as_ref(), popup_pos) {
            send_popup_configure(state, xs, (abs.0 - origin.0, abs.1 - origin.1), w, h);
        }
    }
    // Okno X11: powiedz klientowi, ile miejsca zabiera nasz pasek tytułu
    // (`_NET_FRAME_EXTENTS`) — dzięki temu sam ustawia treść pod dekoracją.
    if is_x11 && created && !is_popup {
        let serial = *data.x11_serial.lock_or_recover();
        if let Some(xwm) = crate::xwm::global() {
            if let Some(win) = xwm.window_for_serial(serial) {
                // Tylko okna z naszym paskiem mają ramkę; CSD same pilnują swoich marginesów.
                let top = if ssd { crate::xwm::FRAME_TOP } else { 0 };
                xwm.set_frame_extents(win, top);
                // Zsynchronizuj geometrię okna X11 z modelem: X11 nie zna naszych
                // dekoracji, więc treść zaczyna się `top` pikseli niżej.
                xwm.set_geometry(
                    win,
                    px as i32,
                    (py + f64::from(top)) as i32,
                    w as u32,
                    h as u32,
                );
            }
        }
    } else if shrunk {
        // Wayland: nowy rozmiar treści (xdg_toplevel.configure).
        crate::seat::send_configure(state, slot, f64::from(w), f64::from(h));
    }
    // Pozwól klientowi ponownie użyć bufora — **raz na `attach`**, nie na każdy `commit`.
    if data.take_release() {
        let _ = buffer.send_event(wl_buffer::Event::Release);
    }
}

/// Pozycja nowego okna (kaskada w obszarze roboczym pierwszego monitora).
///
/// Klasyczna kaskada `60 + n*36` po kilkunastu oknach wyjeżdżała poza ekran —
/// pasek tytułu lądował pod dolną krawędzią i nie dało się go złapać. Zawijamy
/// kaskadę w siatkę, która zawsze mieści się w obszarze roboczym (i pod panelem).
/// Liczba okien branych pod uwagę przy kaskadowym układaniu nowych okien.
///
/// Tylko **widoczne** okna wyznaczają kaskadę. Sloty zamkniętych i zminimalizowanych
/// okien zostają w modelu (indeks musi być stabilny), więc liczenie ich wszystkich
/// przesuwało każde nowe okno coraz dalej w prawo/dół.
pub(crate) fn cascade_slots(windows: &[crate::state::Window]) -> usize {
    windows
        .iter()
        .filter(|w| w.mapped && !w.layer && !w.popup)
        .count()
}

fn cascade_position(state: &State, count: usize) -> (f64, f64) {
    let (ox, oy, ow, oh) = {
        let outs = state.outputs.lock_or_recover();
        match outs.first() {
            Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
            None => (0.0, 0.0, 1280.0, 800.0),
        }
    };
    let step = 36.0;
    let cols = 6usize;
    let x = (60.0 + (count % cols) as f64 * step).min((ow - 280.0).max(0.0));
    let top = oy
        + state
            .top_insets
            .get(&0)
            .copied()
            .unwrap_or(0.0)
            .min(oh - 200.0);
    let y = (top + 20.0 + ((count / cols) % 5) as f64 * step).min((oy + oh - 200.0).max(top));
    (ox + x, y)
}

/// Skomponuj powierzchnię warstwową (`wlr-layer-shell`): konfiguracja + kotwiczenie.
fn composite_layer(
    state: &mut State,
    surface: &wl_surface::WlSurface,
    data: &SurfaceData,
    ls: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
) {
    let Some(ld) = ls.data::<LayerData>() else {
        return;
    };
    let out_idx = ld.output.unwrap_or(0);
    // `configure` liczymy dla monitora, do którego powierzchnia jest przypisana (albo
    // pierwszego, gdy `output=None`); samo rysowanie robimy niżej dla każdego wyjścia.
    let (_, _, out_w, out_h) = output_rect(state, out_idx);
    let buffer = data.buffer.lock_or_recover().clone();
    let debug = std::env::var("SELENE_STATS").is_ok();
    // Pierwszy `commit` (zwykle bez bufora) → wyślij `configure` z rozmiarem.
    if !*ld.configured.lock_or_recover() {
        let st = *ld.state.lock_or_recover();
        let (cw, ch) = st.configure_size(out_w, out_h);
        let serial = state.next_serial();
        let _ = ls.send_event(zwlr_layer_surface_v1::Event::Configure {
            serial,
            width: cw.max(0) as u32,
            height: ch.max(0) as u32,
        });
        *ld.configured.lock_or_recover() = true;
        if debug {
            eprintln!("selene/layer: configure {}x{}", cw.max(0), ch.max(0));
        }
        if buffer.is_none() {
            return;
        }
    }
    let Some(buffer) = buffer else {
        return;
    };
    let Some(buf_data) = buffer.data::<BufferData>() else {
        if debug {
            eprintln!("selene/layer: commit bez BufferData");
        }
        return;
    };
    let (w, h) = (buf_data.width, buf_data.height);
    let Some(rgba) = buf_data.to_rgba() else {
        if debug {
            eprintln!(
                "selene/layer: bufor odrzucony (to_rgba None) {}x{} stride={} offset={}",
                buf_data.width, buf_data.height, buf_data.stride, buf_data.offset
            );
        }
        return;
    };
    let input_rects = opaque_input_rects(&rgba, w as usize, h as usize);
    let active_h = input_rects
        .iter()
        .map(|r| r.bottom() as usize)
        .max()
        .unwrap_or(0);
    // Jeśli dolna część bufora warstwy jest całkowicie przezroczysta (np. pasek 40 px na buforze 1080p),
    // przytnij bufor w miejscu do faktycznie używanej wysokości — zapobiega to blendowaniu 2 mln pustych pikseli.
    let (effective_h, final_rgba) = if active_h > 0 && active_h < h as usize {
        let mut r = rgba;
        r.truncate(w as usize * active_h * 4);
        (active_h as i32, r)
    } else {
        (h, rgba)
    };
    let image = std::sync::Arc::new(
        ImageData::new(w as u32, effective_h as u32, final_rgba).with_sparse({
            // Panele/popupy mają dużo w pełni przezroczystych obszarów; wtedy renderer
            // może pomijać `alpha == 0` zamiast blendować cały prostokąt.
            let covered: f64 = input_rects.iter().map(|r| r.width() * r.height()).sum();
            let total = (w as f64) * (effective_h as f64);
            total > 0.0 && covered < total * 0.75
        }),
    );
    let st = *ld.state.lock_or_recover();
    let z = crate::layer::layer_z(st.layer);
    // Strefa wyłączna górnego panelu → obszar roboczy. Bez tego maksymalizowane okna
    // zaczynałyby się pod panelem, chowając własny pasek tytułu. Zapamiętujemy ją
    // **per monitor** — panel jest tylko na jednym wyjściu.
    let out_idx = ld.output.unwrap_or(0);
    if st.layer == 2 && st.anchor & crate::layer::ANCHOR_TOP != 0 && st.exclusive_zone > 0 {
        let inset = state.top_insets.entry(out_idx).or_insert(0.0);
        let next = inset.max(st.exclusive_zone as f64);
        if next != *inset && std::env::var("SELENE_STATS").is_ok() {
            eprintln!(
                "selene/layer: panel rezerwuje {:.0} px na wyjściu {out_idx}",
                st.exclusive_zone
            );
        }
        *inset = next;
    }
    // Jeden klient = jedna powierzchnia na **jednym** monitorze. Panele na kolejnych
    // monitorach to osobne instancje powłoki (każda wiąże swój `wl_output`). Kopiowanie
    // bufora warstwy na wszystkie wyjścia dawało tę samą treść (panel + popupy) wszędzie.
    let (ox, oy, out_w, out_h) = output_rect(state, out_idx);
    // Region wejścia: klient (jeśli ustawił) albo nasz z kanału alfa — w układzie bufora.
    // Pusty jawny region = „wróć do alfy”; region pokrywający całą powierzchnię =
    // przechwytywanie kliknięć (otwarty popup powłoki).
    let explicit_input = data.input.lock_or_recover().clone();
    let (base_input, input_capture): (Vec<Rect>, bool) = match explicit_input {
        Some(rects) if !rects.is_empty() => {
            let capture = rects.iter().any(|r| covers_surface(r, w as f64, h as f64));
            (rects, capture)
        }
        _ => (input_rects.clone(), false),
    };
    let (rx, ry) = st.position(out_w, out_h, w as f64, effective_h as f64);
    let mut slots = ld.slots.lock_or_recover();
    slots.resize(1, None);
    let (slot, created) = upsert_window_at(
        state,
        &mut slots[0],
        surface,
        ox + rx,
        oy + ry,
        w,
        effective_h,
        z,
        "",
        false,
        true,
        true,
        false,
        image,
        Some(base_input),
        input_capture,
    );
    drop(slots);
    // `SurfaceData.slot` — używane m.in. przy `destroy` powierzchni warstwowej.
    if let Some(sd) = surface.data::<SurfaceData>() {
        *sd.slot.lock_or_recover() = Some(slot);
    }
    if debug && created {
        eprintln!(
            "selene/layer: okno utworzone {}x{} @ ({:.0},{:.0}) na wyjściu {}, z={}",
            w,
            effective_h,
            ox + rx,
            oy + ry,
            out_idx,
            z
        );
    }
    if data.take_release() {
        let _ = buffer.send_event(wl_buffer::Event::Release);
    }
}

/// Czy prostokąt pokrywa całą powierzchnię (z tolerancją na zaokrąglenia).
fn covers_surface(r: &Rect, w: f64, h: f64) -> bool {
    r.x() <= 0.5 && r.y() <= 0.5 && r.right() >= w - 0.5 && r.bottom() >= h - 0.5
}

/// Region wejścia z kanału alfa: nieprzezroczyste bloki scalane poziomo w prostokąty.
fn opaque_input_rects(rgba: &[u8], iw: usize, ih: usize) -> Vec<Rect> {
    const BLOCK: usize = 16;
    const ALPHA_THRESHOLD: u8 = 8;
    if iw == 0 || ih == 0 || rgba.len() < iw * ih * 4 {
        return Vec::new();
    }
    let bw = iw.div_ceil(BLOCK);
    let bh = ih.div_ceil(BLOCK);
    let mut rects = Vec::new();
    for by in 0..bh {
        let y0 = by * BLOCK;
        let y1 = (y0 + BLOCK).min(ih);

        // Szybki filtr: jeśli żaden piksel w rzędzie y0..y1 nie przekracza progu alfy,
        // cały rząd 120 bloków pomijamy natychmiast (ogromne przyspieszenie dla 95% ekranu).
        let mut row_has_alpha = false;
        'check_row: for y in y0..y1 {
            let row = y * iw * 4;
            let slice = &rgba[row..row + iw * 4];
            for p in slice.chunks_exact(4) {
                if p[3] > ALPHA_THRESHOLD {
                    row_has_alpha = true;
                    break 'check_row;
                }
            }
        }
        if !row_has_alpha {
            continue;
        }

        let mut run: Option<usize> = None;
        for bx in 0..bw {
            let x0 = bx * BLOCK;
            let x1 = (x0 + BLOCK).min(iw);
            let mut opaque = false;
            'scan: for y in y0..y1 {
                let row = y * iw * 4;
                for x in x0..x1 {
                    if rgba[row + x * 4 + 3] > ALPHA_THRESHOLD {
                        opaque = true;
                        break 'scan;
                    }
                }
            }
            match (run, opaque) {
                (None, true) => run = Some(bx),
                (Some(s), false) => {
                    rects.push(block_run(s, bx, by, BLOCK, iw, ih));
                    run = None;
                }
                _ => {}
            }
        }
        if let Some(s) = run {
            rects.push(block_run(s, bw, by, BLOCK, iw, ih));
        }
    }
    rects
}

/// Prostokąt dla ciągu bloków `[bx0, bx1)` w rzędzie `by`.
fn block_run(bx0: usize, bx1: usize, by: usize, block: usize, iw: usize, ih: usize) -> Rect {
    let x = bx0 * block;
    let y = by * block;
    let w = (bx1 * block).min(iw).saturating_sub(x);
    let h = (y + block).min(ih) - y;
    Rect::from_xywh(x as f64, y as f64, w as f64, h as f64)
}

#[cfg(test)]
mod input_region_tests {
    use super::*;

    #[test]
    fn opaque_rects_track_alpha() {
        // 32×32, nieprzezroczysta tylko lewa połowa (blok 16 px).
        let mut rgba = vec![0u8; 32 * 32 * 4];
        for y in 0..32 {
            for x in 0..16 {
                rgba[(y * 32 + x) * 4 + 3] = 255;
            }
        }
        let rects = opaque_input_rects(&rgba, 32, 32);
        assert!(!rects.is_empty());
        assert!(rects.iter().all(|r| r.x() < 16.0));
        assert!(rects.iter().all(|r| r.x() + r.width() <= 16.001));
    }

    #[test]
    fn fully_transparent_yields_no_rects() {
        let rects = opaque_input_rects(&vec![0u8; 40 * 40 * 4], 40, 40);
        assert!(rects.is_empty());
    }

    #[test]
    fn full_surface_region_marks_input_capture() {
        let full = Rect::from_xywh(0.0, 0.0, 1920.0, 680.0);
        assert!(covers_surface(&full, 1920.0, 680.0));
        // Region tylko paska nie przechwytuje całej powierzchni.
        let bar = Rect::from_xywh(0.0, 0.0, 1920.0, 32.0);
        assert!(!covers_surface(&bar, 1920.0, 680.0));
    }
}

#[cfg(test)]
mod ssd_tests {
    use super::*;
    use crate::decoration::DecoMode;

    #[test]
    fn popup_never_gets_our_bar() {
        assert!(!wants_ssd(false, true, Some(DecoMode::Server)));
        assert!(!wants_ssd(true, true, None));
    }

    #[test]
    fn x11_windows_are_decorated_by_us() {
        // Xwayland nie tworzy obiektu dekoracji — pasek rysujemy sami.
        assert!(wants_ssd(true, false, None));
        // …ale jawne `client_side` od klienta X11 szanujemy.
        assert!(!wants_ssd(true, false, Some(DecoMode::Client)));
        assert!(wants_ssd(true, false, Some(DecoMode::Server)));
    }

    #[test]
    fn cascade_counts_only_visible_toplevels() {
        use std::sync::Arc;
        let make = |mapped: bool, layer: bool, popup: bool| Window {
            x: 0.0,
            y: 0.0,
            width: 10,
            height: 10,
            image_offset: (0.0, 0.0),
            z: 1,
            title: String::new(),
            popup,
            layer,
            mapped,
            workspace: 0,
            input: None,
            input_capture: false,
            minimized: false,
            maximized: false,
            fullscreen: false,
            ssd: true,
            saved: None,
            data: Arc::new(ImageData::empty(10, 10)),
        };
        let windows = vec![
            make(true, false, false),
            make(false, false, false), // zamknięte/zminimalizowane — nie liczy się
            make(true, true, false),   // panel
            make(true, false, true),   // popup
        ];
        assert_eq!(cascade_slots(&windows), 1);
        assert_eq!(cascade_slots(&[]), 0);
    }

    #[test]
    fn wayland_only_when_server_side_negotiated() {
        assert!(wants_ssd(false, false, Some(DecoMode::Server)));
        // Klienty GTK/libadwaita: `client_side` → nie dublujemy paska.
        assert!(!wants_ssd(false, false, Some(DecoMode::Client)));
        // Brak obiektu dekoracji → brak naszego paska.
        assert!(!wants_ssd(false, false, None));
    }
}

/// Dodaje nowe okno lub aktualizuje istniejące (slot trzymany w [`SurfaceData`]).
#[allow(clippy::too_many_arguments)]
fn upsert_window(
    state: &mut State,
    surface: &wl_surface::WlSurface,
    data: &SurfaceData,
    x: f64,
    y: f64,
    w: i32,
    h: i32,
    z: i32,
    title: &str,
    popup: bool,
    layer: bool,
    reposition: bool,
    ssd: bool,
    image: std::sync::Arc<ImageData>,
) -> (usize, bool) {
    let input = data.input.lock_or_recover().clone();
    let mut slot = data.slot.lock_or_recover();
    upsert_window_at(
        state, &mut slot, surface, x, y, w, h, z, title, popup, layer, reposition, ssd, image,
        input, false,
    )
}

/// Jak [`upsert_window`], ale slot podajemy wprost.
///
/// Potrzebne dla powierzchni warstwowych na wielu monitorach: jedna `wl_surface`
/// odpowiada wtedy kilku oknom (po jednym na wyjście), więc jeden slot w `SurfaceData`
/// nie wystarcza.
#[allow(clippy::too_many_arguments)]
fn upsert_window_at(
    state: &mut State,
    slot: &mut Option<usize>,
    surface: &wl_surface::WlSurface,
    x: f64,
    y: f64,
    w: i32,
    h: i32,
    z: i32,
    title: &str,
    popup: bool,
    layer: bool,
    reposition: bool,
    ssd: bool,
    image: std::sync::Arc<ImageData>,
    input: Option<Vec<Rect>>,
    input_capture: bool,
) -> (usize, bool) {
    let ws = *state.ws_current.lock_or_recover();
    let mut windows = state.windows.lock_or_recover();
    let mut created = false;
    let idx = match *slot {
        Some(i) => {
            if let Some(win) = windows.get_mut(i) {
                // Pozycję stałą okna zmieniamy tylko dla powierzchni kotwiczonych.
                if reposition {
                    win.x = x;
                    win.y = y;
                    win.z = z;
                }
                win.width = w;
                win.height = h;
                win.mapped = true;
                win.popup = popup;
                win.layer = layer;
                win.ssd = ssd;
                if !title.is_empty() {
                    win.title = title.to_owned();
                }
                win.input = input.clone();
                win.input_capture = input_capture;
                win.data = image;
            }
            i
        }
        None => {
            let i = windows.len();
            windows.push(Window {
                x,
                y,
                width: w,
                height: h,
                image_offset: (0.0, 0.0),
                z,
                title: title.to_owned(),
                popup,
                layer,
                mapped: true,
                workspace: ws,
                input,
                input_capture,
                minimized: false,
                maximized: false,
                fullscreen: false,
                ssd,
                saved: None,
                data: image,
            });
            *slot = Some(i);
            created = true;
            i
        }
    };
    drop(windows);
    // Zarejestruj powierzchnię okna (indeks = slot) do trasowania wejścia.
    register_slot(state, idx, surface);
    (idx, created)
}

/// Wpisuje powierzchnię w [`State::slots`] pod indeksem okna.
///
/// Wcześniej robiliśmy to tylko, gdy `slots.len() == idx` — przy powierzchni
/// tworzącej naraz kilka okien (panel na kilku monitorach) albo przy luce w
/// indeksach rejestracja po cichu się gubiła i wejście trafiało w złą powierzchnię.
fn register_slot(state: &mut State, idx: usize, surface: &wl_surface::WlSurface) {
    if state.slots.len() <= idx {
        state.slots.resize(idx + 1, surface.clone());
    }
    state.slots[idx] = surface.clone();
}

/// Geometria monitora `idx` (x, y, szerokość, wysokość); `idx` poza zakresem → pierwszy.
fn output_rect(state: &State, idx: usize) -> (f64, f64, f64, f64) {
    let outs = state.outputs.lock_or_recover();
    let o = outs.get(idx).or_else(|| outs.first());
    match o {
        Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
        None => (0.0, 0.0, 1280.0, 800.0),
    }
}

/// Pozycja popupu względem treści rodzica (prostokąt kotwicy + offset).
fn popup_position(
    state: &State,
    xdg_surface: &xdg_surface::XdgSurface,
    size: (f64, f64),
) -> ((f64, f64), (f64, f64)) {
    let Some(d) = xdg_surface.data::<XdgSurfaceData>() else {
        return ((0.0, 0.0), (0.0, 0.0));
    };
    let geom = *d.geom.lock_or_recover();
    // Prostokąt treści rodzica: współrzędne positionera są względem jego geometrii okna.
    let parent_rect = d
        .parent
        .lock_or_recover()
        .clone()
        .and_then(|ps| {
            ps.data::<SurfaceData>()
                .map(|sd| *sd.slot.lock_or_recover())
        })
        .flatten()
        .and_then(|slot| {
            state
                .windows
                .lock_or_recover()
                .get(slot)
                .map(|w| w.content())
        })
        .unwrap_or_else(|| Rect::from_xywh(0.0, 0.0, 1280.0, 800.0));
    let work = work_area_at(state, parent_rect.center().x, parent_rect.center().y);
    let pos = crate::popup::place(&geom, parent_rect, work, size);
    (pos, (parent_rect.x(), parent_rect.y()))
}

/// Obszar roboczy monitora pod punktem (pełny ekran minus górny panel).
fn work_area_at(state: &State, x: f64, y: f64) -> Rect {
    let outs = state.outputs.lock_or_recover();
    let idx = crate::monitors::output_at(&outs, x, y);
    match outs.get(idx).or_else(|| outs.first()) {
        Some(o) => {
            let top = state.top_insets.get(&idx).copied().unwrap_or(0.0);
            Rect::from_xywh(
                o.x as f64,
                o.y as f64 + top,
                o.width as f64,
                (o.height as f64 - top).max(1.0),
            )
        }
        None => Rect::from_xywh(0.0, 0.0, 1280.0, 800.0),
    }
}

/// Wysyła `xdg_popup.configure` + `xdg_surface.configure` (raz).
fn send_popup_configure(
    state: &mut State,
    xdg_surface: &xdg_surface::XdgSurface,
    (x, y): (f64, f64),
    w: i32,
    h: i32,
) {
    let serial = state.next_serial();
    if let Some(d) = xdg_surface.data::<XdgSurfaceData>() {
        if let Some(popup) = d.popup.lock_or_recover().as_ref() {
            let _ = popup.send_event(xdg_popup::Event::Configure {
                x: x as i32,
                y: y as i32,
                width: w,
                height: h,
            });
        }
        if !*d.configured.lock_or_recover() {
            let _ = xdg_surface.send_event(xdg_surface::Event::Configure { serial });
            *d.configured.lock_or_recover() = true;
        }
    }
}

/// Jeśli toplevel nie ma jeszcze konfiguracji, wyślij `configure` (popupy mają własną).
fn maybe_configure_xdg(state: &mut State, data: &SurfaceData) {
    let Some(xdg_surface) = data.xdg_surface.lock_or_recover().clone() else {
        return;
    };
    let Some(xdg_data) = xdg_surface.data::<XdgSurfaceData>() else {
        return;
    };
    if *xdg_data.is_popup.lock_or_recover() {
        return;
    }
    if *xdg_data.configured.lock_or_recover() {
        return;
    }
    let serial = state.next_serial();
    // Dekoracja **przed** konfiguracją powierzchni: klient stosuje tryb dekoracji przy
    // zatwierdzeniu tej właśnie konfiguracji. Bez tego część toolkitów (GTK) nie zrzuca
    // własnych dekoracji i na oknie widać podwójny pasek tytułu + czarną ramkę treści.
    if let Some(td) = xdg_data
        .toplevel
        .lock_or_recover()
        .as_ref()
        .and_then(|tl| tl.data::<XdgToplevelData>())
    {
        if let Some(deco) = td.decoration.lock_or_recover().as_ref() {
            use wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1 as deco_proto;
            let _ = deco.send_event(deco_proto::Event::Configure {
                mode: WEnum::Value(deco_proto::Mode::ServerSide),
            });
        }
    }
    if let Some(toplevel) = xdg_data.toplevel.lock_or_recover().as_ref() {
        let _ = toplevel.send_event(xdg_toplevel::Event::Configure {
            // 0×0 = „klient wybiera rozmiar”. Wcześniej twarde 1024×720 kurczyło
            // **każde** okno (np. demo libLuny prosi o 1200×1028) i rozjeżdżało
            // późniejsze trafienia w kontrolki. Rozmiar i tak jest potem ograniczany
            // do obszaru roboczego przy pierwszym `commit` z buforem.
            width: 0,
            height: 0,
            // Tablica **u32** (4 bajty na stan), nie pojedyncze bajty — patrz
            // `seat::window_states`.
            states: 4u32.to_ne_bytes().to_vec(), // ACTIVATED
        });
        // UWAGA: **nie** wysyłamy tutaj `wm_capabilities` — to zdarzenie istnieje dopiero
        // od wersji 5 `xdg_toplevel`, a nasz `xdg_wm_base` ogłaszamy jako 1. Wysłanie go
        // klientowi v1 kończy się twardym przerwaniem klienta (Xwayland: „listener
        // function for opcode 3 of xdg_toplevel is NULL” → SIGABRT).
        // Gdy podniesiemy wersję globalu, trzeba je wysyłać warunkowo (`version() >= 5`).
    }
    let _ = xdg_surface.send_event(xdg_surface::Event::Configure { serial });
    *xdg_data.configured.lock_or_recover() = true;
}