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

PaganDE/pagan-compositor/src/state.rs main

1578 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578
//! Stan kompozytora i implementacje handlerów protokołów Waylanda.
//!
//! PO CO osobny `state.rs`: trzymamy tu cały "mózg" kompozytora — przestrzeń
//! okien, seat (mysz/klawiatura), stany protokołów (xdg-shell, decoration, shm)
//! oraz implementacje traitów Smithay. Logika renderowania jest celowo
//! oddzielona (`render.rs`), a obsługa wejścia (`input.rs`), zgodnie z
//! wymaganiem modularności.

use std::{
    cell::RefCell,
    os::unix::io::OwnedFd,
    sync::{atomic::AtomicBool, Arc, Mutex},
};

use tracing::{info, warn};

use smithay::{
    backend::{
        allocator::{dmabuf::Dmabuf, Format},
        renderer::utils::on_commit_buffer_handler,
    },
    delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_fractional_scale,
    delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
    delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_shm,
    delegate_text_input_manager, delegate_viewporter, delegate_xdg_activation,
    delegate_xdg_decoration, delegate_xdg_shell,
    desktop::{
        find_popup_root_surface, layer_map_for_output, LayerSurface as DesktopLayerSurface,
        PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Space,
        Window, WindowSurfaceType,
    },
    input::{
        keyboard::XkbConfig,
        pointer::{CursorImageStatus, Focus, PointerHandle},
        Seat, SeatHandler, SeatState,
    },
    output::Output,
    reexports::{
        calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction},
        wayland_protocols::xdg::{
            decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode,
            shell::server::xdg_toplevel,
        },
        wayland_server::{
            backend::{ClientData, ClientId, DisconnectReason},
            protocol::{wl_data_source::WlDataSource, wl_output, wl_seat, wl_surface::WlSurface},
            Client, Display, DisplayHandle, Resource,
        },
    },
    utils::{IsAlive, Logical, Point, Rectangle, Serial, Size, SERIAL_COUNTER as SCOUNTER},
    wayland::{
        buffer::BufferHandler,
        compositor::{
            get_parent, with_states, CompositorClientState, CompositorHandler, CompositorState,
            SurfaceAttributes,
        },
        dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier},
        fractional_scale::{
            with_fractional_scale, FractionalScaleHandler, FractionalScaleManagerState,
        },
        output::{OutputHandler, OutputManagerState},
        pointer_constraints::{
            with_pointer_constraint, PointerConstraintsHandler, PointerConstraintsState,
        },
        pointer_gestures::PointerGesturesState,
        relative_pointer::RelativePointerManagerState,
        seat::WaylandFocus,
        selection::{
            data_device::{
                set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
                ServerDndGrabHandler,
            },
            primary_selection::{
                set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
            },
            SelectionHandler,
        },
        shell::{
            wlr_layer::{
                Layer as WlrLayer, LayerSurface as WlrLayerSurface, LayerSurfaceData,
                WlrLayerShellHandler, WlrLayerShellState,
            },
            xdg::{
                decoration::{XdgDecorationHandler, XdgDecorationState},
                Configure, PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler,
                XdgShellState, XdgToplevelSurfaceData,
            },
        },
        shm::{ShmHandler, ShmState},
        socket::ListeningSocketSource,
        text_input::TextInputManagerState,
        viewporter::ViewporterState,
        xdg_activation::{
            XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
        },
    },
};

// `import_dmabuf` jest potrzebne tylko w backendzie udev (sprawdzamy import na GPU).
#[cfg(feature = "udev")]
use smithay::backend::renderer::ImportDma;

// XWayland (aplikacje X11).
use smithay::{
    wayland::{
        xwayland_keyboard_grab::XWaylandKeyboardGrabState, xwayland_shell::XWaylandShellState,
    },
    xwayland::{X11Wm, XWaylandClientData},
};

use crate::{
    focus::{KeyboardFocusTarget, PointerFocusTarget},
    foreign_toplevel::ForeignToplevelState,
    grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeState, SurfaceData},
    window::PaganWindow,
};

/// Krok kaskadowego rozmieszczania nowych okien (w pikselach logicznych).
/// PO CO: nowe okna mają się pojawiać "schodkowo", żeby nie nakładały się
/// dokładnie na siebie i żeby było widać, że jest ich kilka.
pub const CASCADE_STEP: i32 = 20;

/// Dane klienta Waylanda. `CompositorClientState` jest wymagany przez Smithay
/// do poprawnego działania protokołu `wl_surface` (m.in. buforów i fence'ów).
#[derive(Debug, Default)]
pub struct ClientState {
    pub compositor_state: CompositorClientState,
}

impl ClientData for ClientState {
    fn initialized(&self, _client_id: ClientId) {}
    fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}

/// Ikona przeciągania i upuszczania (drag & drop) trzymana na czas operacji.
///
/// PO CO: DnD działa funkcjonalnie bez rysowania ikony, ale użytkownik powinien
/// widzieć, co niesie. Trzymamy powierzchnię i przesunięcie; rysowanie podłączone
/// jest w `render.rs` (`output_elements`).
#[derive(Debug, Clone)]
pub struct DndIcon {
    pub surface: WlSurface,
    pub offset: Point<i32, Logical>,
}

/// Okno odłożone na bok przez minimalizację.
///
/// PO CO zapamiętujemy pozycję: bez menedżera zadań (paska zadań) okno musi
/// wrócić dokładnie tam, gdzie było. Trzymamy więc okno ORAZ jego ostatnią
/// pozycję w przestrzeni.
#[derive(Debug, Clone)]
pub struct MinimizedWindow {
    pub window: PaganWindow,
    pub location: Point<i32, Logical>,
}

/// Główny stan kompozytora.
pub struct MyCompositor {
    /// Przestrzeń okien — kolejność elementów to Z-order (od spodu do wierzchu).
    pub space: Space<PaganWindow>,
    /// Pozycja kursora w przestrzeni logicznej (lustrzana do stanu pointera).
    pub pointer_location: Point<f64, Logical>,
    /// Nazwa socketa Waylanda (dla klientów: `WAYLAND_DISPLAY`).
    pub socket_name: Option<String>,
    /// Flaga sterująca pętlą główną.
    pub running: Arc<AtomicBool>,
    /// Uchwyt do pętli zdarzeń — pozwala rejestrować źródła z wnętrza stanu.
    pub display_handle: DisplayHandle,
    pub handle: LoopHandle<'static, MyCompositor>,

    // Stany protokołów (globals).
    pub compositor_state: CompositorState,
    pub shm_state: ShmState,
    pub output_manager_state: OutputManagerState,
    pub seat_state: SeatState<MyCompositor>,
    pub xdg_shell_state: XdgShellState,
    pub xdg_decoration_state: XdgDecorationState,

    // Wejście.
    pub seat: Seat<MyCompositor>,
    pub pointer: PointerHandle<MyCompositor>,

    /// Menedżer popupów (menu rozwijane klientów).
    pub popups: PopupManager,
    /// Aktualny stan kursora (kształt/schowanie).
    pub cursor_status: CursorImageStatus,

    // --- Protokoły "biurkowe" (schowek, aktywacja, skala, ograniczenia) ---
    /// Schowek (`wl_data_device`) — bez tego kopiuj/wklej nie działa.
    pub data_device_state: DataDeviceState,
    /// Selekcja pierwotna (primary selection, wklejanie środkowym przyciskiem).
    pub primary_selection_state: PrimarySelectionState,
    /// Aktywacja okien (`xdg-activation`) — aplikacje mogą "poprosić o uwagę".
    pub xdg_activation_state: XdgActivationState,
    /// Skalowanie ułamkowe (`fractional-scale`) — podstawa HiDPI dla klientów.
    pub fractional_scale_manager_state: FractionalScaleManagerState,
    /// Ograniczenia wskaźnika (`pointer-constraints`) — np. gry przechwytujące kursor.
    pub pointer_constraints_state: PointerConstraintsState,
    /// Ikona przeciągania (DnD) na czas operacji.
    pub dnd_icon: Option<DndIcon>,

    /// Zminimalizowane okna (unmapped z `space`) razem z ostatnią pozycją.
    ///
    /// PO CO poza `space`: `Space` rysuje wszystko, co zawiera — nie ma pojęcia
    /// "ukryte". Odkładamy więc okno tutaj i przywracamy na żądanie
    /// (skrót klawiszowy / przyszły pasek zadań).
    pub minimized: Vec<MinimizedWindow>,

    /// Stan protokołu `wlr-foreign-toplevel-management` (lista okien dla paneli).
    pub foreign_toplevel: ForeignToplevelState,

    /// Uchwyty globala `pagan_app_menu_manager_v1` (menu aplikacji w pasku SSD).
    ///
    /// PO CO trzymamy: obiekty menedżera żyją tak długo, jak ich klient — musimy
    /// je znać, żeby posprzątać po rozłączeniu i móc wysyłać zdarzenia.
    pub app_menu_managers:
        Vec<crate::app_menu::protocol::server::pagan_app_menu_manager_v1::PaganAppMenuManagerV1>,

    /// Podgląd („duch”) geometrii docelowej przy doklejaniu okna.
    ///
    /// PO CO: pokazujemy, gdzie okno wyląduje, ZANIM użytkownik puści przycisk —
    /// sam snap stosujemy dopiero przy puszczeniu (`grabs.rs`).
    pub snap_preview: Option<Rectangle<i32, Logical>>,

    // --- dmabuf + layer-shell ---
    /// Delegat protokołu `linux-dmabuf` (bufory GPU).
    pub dmabuf_state: DmabufState,
    /// Global dmabuf — trzymany, by nie został usunięty.
    pub dmabuf_global: Option<DmabufGlobal>,
    /// Stan warstw (`wlr-layer-shell`) — panele, tła, nakładki.
    pub layer_shell_state: WlrLayerShellState,
    /// Kolejka przechwyceń ekranu (`wlr-screencopy`) do wykonania w pętli renderu.
    pub screencopy_queue: Vec<crate::screencopy::PendingCapture>,

    // --- XWayland (aplikacje X11) ---
    /// Global protokołu `xwayland_shell` — bez niego XWayland nie ma jak
    /// skojarzyć okna X11 z powierzchnią Waylanda.
    pub xwayland_shell_state: XWaylandShellState,
    /// Menedżer okien X11 (reparenting WM). `None`, dopóki XWayland nie zgłosi
    /// gotowości (albo gdy binarki `Xwayland` nie ma w systemie).
    pub xwm: Option<X11Wm>,
    /// Numer wyświetlacza X (dla `DISPLAY=:N` uruchamianych aplikacji X11).
    pub xdisplay: Option<u32>,

    /// Konfiguracja doklejania okien (próg + dolna krawędź), z `PAGAN_SNAP_*`.
    pub snap_config: crate::snap::SnapConfig,

    /// Stan backendu tty-udev (DRM/KMS + libinput). `None` dla backendu winit.
    #[cfg(feature = "udev")]
    pub udev: Option<crate::udev::UdevState>,

    /// Następna pozycja dla kaskadowego rozmieszczania (współdzielona z `output.rs`).
    pub(crate) next_cascade: Point<i32, Logical>,
}

impl MyCompositor {
    /// Tworzy stan kompozytora, rejestruje socket Waylanda, źródło dispatchu
    /// klientów oraz wszystkie globalne protokoły.
    ///
    /// PO CO tutaj, a nie w `run_winit`: to jest logika niezależna od backendu —
    /// ten sam `init` używają oba backendy (winit i tty-udev). Wyjściami zajmuje
    /// się `output.rs`/backend, bo dopiero one znają tryby i skalę.
    pub fn init(
        display: Display<Self>,
        handle: LoopHandle<'static, Self>,
        listen_on_socket: bool,
    ) -> Self {
        let dh = display.handle();

        // Socket, na którym klienci łączą się z kompozytorem.
        let socket_name = if listen_on_socket {
            let source =
                ListeningSocketSource::new_auto().expect("nie udało się utworzyć socketa Wayland");
            let name = source.socket_name().to_string_lossy().into_owned();
            handle
                .insert_source(source, |stream, _, data: &mut Self| {
                    if let Err(err) = data
                        .display_handle
                        .insert_client(stream, Arc::new(ClientState::default()))
                    {
                        warn!("Błąd dodawania klienta Waylanda: {}", err);
                    }
                })
                .expect("nie udało się zarejestrować źródła socketa");
            info!(name = name, "PaganDE: nasłuchuję na sockecie Wayland");
            Some(name)
        } else {
            None
        };

        // Źródło calloop czytające zdarzenia przychodzące od klientów.
        handle
            .insert_source(
                Generic::new(display, Interest::READ, Mode::Level),
                |_, display, data: &mut Self| {
                    // Bezpieczeństwo: `Display` żyje tak długo jak źródło calloop,
                    // więc wskaźnik jest ważny przez cały czas działania pętli.
                    unsafe {
                        display
                            .get_mut()
                            .dispatch_clients(data)
                            .expect("dispatch_clients nie powiodło się");
                    }
                    Ok(PostAction::Continue)
                },
            )
            .expect("nie udało się zarejestrować źródła display");

        // Globalne protokoły.
        let compositor_state = CompositorState::new::<Self>(&dh);
        let shm_state = ShmState::new::<Self>(&dh, vec![]);
        let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&dh);
        let xdg_shell_state = XdgShellState::new::<Self>(&dh);
        let xdg_decoration_state = XdgDecorationState::new::<Self>(&dh);

        // Protokoły "biurkowe": schowek, primary selection, aktywacja, skala,
        // ograniczenia wskaźnika oraz text-input (global dla kompatybilności).
        let data_device_state = DataDeviceState::new::<Self>(&dh);
        let primary_selection_state = PrimarySelectionState::new::<Self>(&dh);
        let xdg_activation_state = XdgActivationState::new::<Self>(&dh);
        let fractional_scale_manager_state = FractionalScaleManagerState::new::<Self>(&dh);
        let pointer_constraints_state = PointerConstraintsState::new::<Self>(&dh);
        // Text-input tworzymy tylko jako global (nie implementujemy jeszcze
        // input-method), więc nie potrzebujemy przechowywać jego stanu.
        TextInputManagerState::new::<Self>(&dh);

        // Warstwy (panele/menus), viewporter oraz wskaźnik względny i gesty.
        let layer_shell_state = WlrLayerShellState::new::<Self>(&dh);
        ViewporterState::new::<Self>(&dh);
        RelativePointerManagerState::new::<Self>(&dh);
        PointerGesturesState::new::<Self>(&dh);

        // Screencopy: rejestrujemy global; logika przechwycenia w `screencopy.rs`.
        crate::screencopy::init(&dh);

        // Lista i sterowanie oknami dla paneli/pasków zadań.
        crate::foreign_toplevel::init(&dh);

        // Menu aplikacji w pasku SSD (własny protokół PaganDE).
        crate::app_menu::init(&dh);

        // XWayland: `xwayland_shell` (skojarzenie okna X11 z wl_surface) oraz grab
        // klawiatury X11. `XWaylandKeyboardGrabState` nie wymaga pola — trzyma go
        // display; tworzymy wyłącznie po to, by zarejestrować global.
        let xwayland_shell_state = XWaylandShellState::new::<Self>(&dh);
        XWaylandKeyboardGrabState::new::<Self>(&dh);

        // Seat: mysz + klawiatura. Klawiaturę tworzymy z domyślną mapą xkb.
        let mut seat_state = SeatState::new();
        let mut seat = seat_state.new_wl_seat(&dh, "pagan".to_string());
        let pointer = seat.add_pointer();
        seat.add_keyboard(XkbConfig::default(), 200, 25)
            .expect("nie udało się zainicjalizować klawiatury");

        // Kaskadowe rozmieszczanie startuje w punkcie (CASCADE_STEP, CASCADE_STEP).
        // PO CO nie liczymy tego z trybu wyjścia: tryb jest w jednostkach
        // FIZYCZNYCH, a `next_cascade` jest logiczny. Nowe okna i tak trafiają
        // na monitor pod kursorem przy mapowaniu (`map_window`).
        let next_cascade = Point::from((CASCADE_STEP, CASCADE_STEP));

        // Motyw kompozytora: wczytaj plik (utwórz szablon przy pierwszym starcie)
        // i obserwuj zmiany. Wspólne dla obu backendów, bo `init` jest wspólny.
        crate::theme::install(&handle);

        MyCompositor {
            space: Space::default(),
            pointer_location: Point::from((0.0, 0.0)),
            socket_name,
            running: Arc::new(AtomicBool::new(true)),
            display_handle: dh,
            handle,
            compositor_state,
            shm_state,
            output_manager_state,
            seat_state,
            xdg_shell_state,
            xdg_decoration_state,
            seat,
            pointer,
            popups: PopupManager::default(),
            cursor_status: CursorImageStatus::default_named(),
            data_device_state,
            primary_selection_state,
            xdg_activation_state,
            fractional_scale_manager_state,
            pointer_constraints_state,
            dnd_icon: None,
            minimized: Vec::new(),
            foreign_toplevel: ForeignToplevelState::new(),
            app_menu_managers: Vec::new(),
            snap_preview: None,
            dmabuf_state: DmabufState::new(),
            dmabuf_global: None,
            layer_shell_state,
            screencopy_queue: Vec::new(),
            xwayland_shell_state,
            xwm: None,
            xdisplay: None,
            snap_config: crate::snap::SnapConfig::from_env(),
            #[cfg(feature = "udev")]
            udev: None,
            next_cascade,
        }
    }

    /// Dołącza stan backendu tty-udev po jego zbudowaniu.
    ///
    /// PO CO osobno: `UdevState` potrzebuje `LoopHandle`, który powstaje dopiero
    /// tutaj, a jednocześnie musi zostać zarejestrowany w źródłach calloop.
    #[cfg(feature = "udev")]
    pub fn attach_udev(&mut self, udev: crate::udev::UdevState) {
        self.udev = Some(udev);
    }

    /// Rejestruje global `linux-dmabuf` z podanymi formatami.
    ///
    /// PO CO osobno od `init`: formaty pochodzą z renderera, a renderer powstaje
    /// dopiero po zbudowaniu stanu (w backencie). Metoda jest więc wspólna dla
    /// winit i udev i wołana raz, tuż po starcie backendu.
    pub fn enable_dmabuf(&mut self, formats: impl IntoIterator<Item = Format>) {
        if self.dmabuf_global.is_some() {
            return;
        }
        let global = self
            .dmabuf_state
            .create_global::<Self>(&self.display_handle, formats);
        self.dmabuf_global = Some(global);
        info!("global linux-dmabuf zarejestrowany");
    }

    /// Znajduje okno na podstawie powierzchni Waylanda.
    pub fn window_for_surface(&self, surface: &WlSurface) -> Option<PaganWindow> {
        self.space
            .elements()
            .find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false))
            .cloned()
    }

    /// Mapuje nowe okno w układzie kaskadowym, na wyjściu pod kursorem.
    ///
    /// Wariant publicznego API z Zadania 1 (`map_window`). Właściwa logika
    /// (wybór monitora + kaskada) żyje w `crate::output`.
    pub fn map_window(&mut self, window: PaganWindow) {
        // Kolejność: najpierw umieszczenie w przestrzeni (kaskada), potem fokus.
        self.place_new_window(window.clone());
        // Nowo otwarte okno OD RAZU przejmuje fokus klawiatury. Dzięki temu jest
        // od razu wizualnie aktywne (jaśniejszy pasek) i przyjmuje klawisze —
        // bez tego użytkownik musiałby najpierw kliknąć w nowe okno.
        let serial = SCOUNTER.next_serial();
        self.raise_and_focus(&window, serial);
    }

    /// Przesuwa okno o wektor (dx, dy) w pikselach logicznych.
    pub fn move_window(&mut self, window: &PaganWindow, dx: f64, dy: f64) {
        if let Some(loc) = self.space.element_location(window) {
            let new_loc = loc + Point::from((dx.round() as i32, dy.round() as i32));
            self.space.map_element(window.clone(), new_loc, false);
        }
    }

    /// Minimalizuje okno: chowa je z przestrzeni i odkłada na listę schowanych.
    ///
    /// PO CO `unmap_elem`, a nie przesunięcie za ekran: okno ma zniknąć zarówno
    /// z kompozycji, jak i z trafień myszy/fokusa. Okno poza `space` nie jest
    /// rysowane ani nie łapie kliknięć.
    pub fn minimize_window(&mut self, window: &PaganWindow) {
        // Powtórna minimalizacja nie może zdublować wpisu na liście.
        if self.minimized.iter().any(|entry| &entry.window == window) {
            return;
        }
        let Some(location) = self.space.element_location(window) else {
            // Okno nie jest zmapowane — nie ma czego chować.
            return;
        };

        self.space.unmap_elem(window);
        self.minimized.push(MinimizedWindow {
            window: window.clone(),
            location,
        });

        // Fokus nie może wskazywać na okno, którego nie ma już w przestrzeni.
        let serial = SCOUNTER.next_serial();
        self.focus_top_window(serial);
    }

    /// Przywraca ostatnio zminimalizowane okno (stos LIFO).
    ///
    /// PO CO LIFO: to naturalna kolejność dla użytkownika — ostatnio schowane
    /// wraca pierwsze, jak zdejmowanie ze stosu kartek.
    pub fn restore_last_minimized(&mut self) {
        let serial = SCOUNTER.next_serial();
        // Pomijamy okna, które klient zdążył zamknąć, gdy były schowane.
        while let Some(entry) = self.minimized.pop() {
            if !entry.window.alive() {
                continue;
            }
            self.space
                .map_element(entry.window.clone(), entry.location, true);
            self.raise_and_focus(&entry.window, serial);
            return;
        }
        tracing::info!("brak zminimalizowanych okien do przywrócenia");
    }

    /// Nadaje fokus oknu z wierzchu stosu (albo czyści fokus, gdy nic nie zostało).
    fn focus_top_window(&mut self, serial: Serial) {
        // `elements()` idzie od spodu do wierzchu, więc ostatni element = wierzch.
        // Kolekcjonujemy od razu do `Option`, żeby nie trzymać pożyczki `space`
        // podczas mutującego `raise_and_focus`.
        let top = self.space.elements().next_back().cloned();
        match top {
            Some(window) => self.raise_and_focus(&window, serial),
            None => {
                if let Some(keyboard) = self.seat.get_keyboard() {
                    keyboard.set_focus(self, Option::<KeyboardFocusTarget>::None, serial);
                }
            }
        }
    }

    /// Zamienia surowe okno Smithaya na nasze okno z dekoracją SSD (`PaganWindow`).
    ///
    /// PO CO: fokus klawiatury trzyma `Window` (tak wymaga Smithay), a operacje
    /// typu minimalizacja potrzebują wrappera `PaganWindow` obecnego w `space`.
    pub fn pagan_window_for(&self, window: &Window) -> Option<PaganWindow> {
        self.space
            .elements()
            .find(|candidate| &candidate.0 == window)
            .cloned()
    }

    /// Przywraca KONKRETNE okno z listy zminimalizowanych.
    ///
    /// PO CO osobno od `restore_last_minimized`: panel wie, który kafelek
    /// kliknięto, więc przywracamy wskazane okno, a nie ostatnie z brzegu.
    pub fn restore_window(&mut self, window: &PaganWindow) {
        let Some(position) = self
            .minimized
            .iter()
            .position(|entry| &entry.window == window)
        else {
            return;
        };
        let entry = self.minimized.remove(position);
        self.space
            .map_element(entry.window.clone(), entry.location, true);
        let serial = SCOUNTER.next_serial();
        self.raise_and_focus(&entry.window, serial);
    }

    /// Podnosi i aktywuje okno; jeśli było schowane — najpierw je przywraca.
    ///
    /// To wspólna akcja dla skrótu, kliknięcia w pasek zadań i protokołu
    /// `foreign-toplevel`.
    pub fn focus_window(&mut self, window: &PaganWindow) {
        if self.minimized.iter().any(|entry| &entry.window == window) {
            self.restore_window(window);
            return;
        }
        if !window.alive() {
            return;
        }
        let serial = SCOUNTER.next_serial();
        self.raise_and_focus(window, serial);
    }

    /// Prosi okno o zamknięcie (Wayland: `xdg_toplevel.close`, X11: `WM_DELETE_WINDOW`).
    pub fn close_window(&mut self, window: &PaganWindow) {
        if let Some(toplevel) = window.toplevel() {
            toplevel.send_close();
        } else if let Some(x11) = window.x11_surface() {
            let _ = x11.close();
        }
    }

    /// Maksymalizuje okno (w pływającym WM = obszar roboczy wyjścia pod oknem).
    pub fn maximize_window(&mut self, window: &PaganWindow) {
        if let Some(toplevel) = window.toplevel() {
            let Some(area) = self.output_work_area_for_window(window) else {
                return;
            };
            toplevel.with_pending_state(|state| {
                state.states.set(xdg_toplevel::State::Maximized);
                state.size = Some(area.size);
            });
            self.space.map_element(window.clone(), area.loc, true);
            toplevel.send_configure();
        } else if let Some(x11) = window.x11_surface() {
            // Dla X11 zmiana stanu jest prośbą do klienta; geometrią zajmuje się
            // `XwmHandler::maximize_request` (zapamiętuje starą geometrię).
            let _ = x11.set_maximized(true);
        }
    }

    /// Cofa maksymalizację.
    pub fn unmaximize_window(&mut self, window: &PaganWindow) {
        if let Some(toplevel) = window.toplevel() {
            toplevel.with_pending_state(|state| {
                state.states.unset(xdg_toplevel::State::Maximized);
                state.size = None;
            });
            toplevel.send_pending_configure();
        } else if let Some(x11) = window.x11_surface() {
            let _ = x11.set_maximized(false);
        }
    }

    /// Pełny ekran na wyjściu pod oknem.
    pub fn set_fullscreen_window(&mut self, window: &PaganWindow) {
        if let Some(toplevel) = window.toplevel() {
            let Some(area) = self.output_area_for_window(window) else {
                return;
            };
            toplevel.with_pending_state(|state| {
                state.states.set(xdg_toplevel::State::Fullscreen);
                state.size = Some(area.size);
            });
            self.space.map_element(window.clone(), area.loc, true);
            toplevel.send_configure();
        } else if let Some(x11) = window.x11_surface() {
            let _ = x11.set_fullscreen(true);
        }
    }

    /// Wychodzi z pełnego ekranu.
    pub fn unset_fullscreen_window(&mut self, window: &PaganWindow) {
        if let Some(toplevel) = window.toplevel() {
            toplevel.with_pending_state(|state| {
                state.states.unset(xdg_toplevel::State::Fullscreen);
                state.size = None;
            });
            toplevel.send_pending_configure();
        } else if let Some(x11) = window.x11_surface() {
            let _ = x11.set_fullscreen(false);
        }
    }

    /// Prostokąt wyjścia, na którym leży okno (a jeśli nie wiadomo — pierwszego).
    fn output_area_for_window(&self, window: &PaganWindow) -> Option<Rectangle<i32, Logical>> {
        let location = self.space.element_location(window);
        let under = location.and_then(|location| {
            self.space.outputs().find_map(|output| {
                let geometry = self.space.output_geometry(output)?;
                geometry.contains(location).then_some(geometry)
            })
        });
        under.or_else(|| {
            self.space
                .outputs()
                .next()
                .and_then(|output| self.space.output_geometry(output))
        })
    }

    /// Prostokąt OBSZARU ROBOCZEGO wyjścia, na którym leży okno (bez paneli).
    ///
    /// PO CO osobno od `output_area_for_window`: pełny ekran ma zasłaniać panel,
    /// a maksymalizacja — nie (okno zmaksymalizowane nadal widzi pasek systemowy).
    fn output_work_area_for_window(&self, window: &PaganWindow) -> Option<Rectangle<i32, Logical>> {
        let location = self.space.element_location(window);
        let under = location.and_then(|location| {
            self.space.outputs().find_map(|output| {
                let geometry = self.space.output_geometry(output)?;
                geometry
                    .contains(location)
                    .then(|| self.output_work_area(output))
                    .flatten()
            })
        });
        under.or_else(|| {
            self.space
                .outputs()
                .next()
                .and_then(|output| self.output_work_area(output))
        })
    }

    /// Prosi klienta o zmianę rozmiaru (poprzez nowy configure xdg-toplevel).
    ///
    /// PO CO przez configure, a nie bezpośrednio: w hybrydowym CSD to klient
    /// renderuje okno i ma ostatnie słowo co do rozmiaru bufora. My jedynie
    /// sugerujemy rozmiar, a klient potwierdza go commitem.
    pub fn resize_window(&mut self, window: &PaganWindow, new_size: Size<i32, Logical>) {
        if let Some(toplevel) = window.toplevel() {
            toplevel.with_pending_state(|state| {
                state.size = Some(new_size);
            });
            toplevel.send_configure();
        }
    }

    /// Podnosi okno na wierzch stosu (Z-order) i nadaje mu fokus klawiatury.
    ///
    /// PO CO w jednym miejscu: to jedna operacja semantyczna — "użytkownik
    /// kliknął okno". Wywołujemy ją przy każdym kliknięciu w powierzchnię.
    pub fn raise_and_focus(&mut self, window: &PaganWindow, serial: Serial) {
        self.space.raise_element(window, true);
        let keyboard = self.seat.get_keyboard().unwrap();
        keyboard.set_focus(
            self,
            Some(KeyboardFocusTarget::Window(window.0.clone())),
            serial,
        );
    }

    /// Wysyła początkowy configure do nowo zmapowanego toplevela (jeśli trzeba)
    /// i domyka stan resize po commicie klienta.
    fn ensure_initial_configure(&mut self, surface: &WlSurface) {
        let Some(window) = self.window_for_surface(surface) else {
            return;
        };

        if let Some(toplevel) = window.toplevel() {
            let initial_configure_sent = with_states(surface, |states| {
                states
                    .data_map
                    .get::<XdgToplevelSurfaceData>()
                    .map(|data| data.lock().unwrap().initial_configure_sent)
                    .unwrap_or(false)
            });
            if !initial_configure_sent {
                toplevel.send_configure();
            }
        }

        // Gdy klient zacommitował finalny rozmiar po resize, czyścimy stan.
        with_states(surface, |states| {
            if let Some(data) = states.data_map.get::<RefCell<SurfaceData>>() {
                let mut data = data.borrow_mut();
                if let ResizeState::WaitingForCommit(_) = data.resize_state {
                    data.resize_state = ResizeState::NotResizing;
                }
            }
        });
    }

    /// Obsługuje żądanie przeciągnięcia okna (`xdg_toplevel.move`).
    ///
    /// Uwaga: to jest standardowa ścieżka dla hybrydowego CSD — klient wysyła
    /// `move` z serialem kliknięcia, a my ustanawiamy grab.
    pub fn move_request_xdg(
        &mut self,
        surface: &ToplevelSurface,
        seat: &Seat<Self>,
        serial: Serial,
    ) {
        let Some(pointer) = seat.get_pointer() else {
            return;
        };
        if !pointer.has_grab(serial) {
            return;
        }
        let Some(start_data) = pointer.grab_start_data() else {
            return;
        };
        let Some(window) = self.window_for_surface(surface.wl_surface()) else {
            return;
        };
        // Ignorujemy żądania z innej powierzchni niż ta, która ma fokus.
        let same_client = start_data
            .focus
            .as_ref()
            .map(|(focus, _)| focus.same_client_as(&surface.wl_surface().id()))
            .unwrap_or(false);
        if !same_client {
            return;
        }

        // --- Doklejanie: pamiętamy „swobodny” rozmiar okna ---
        //
        // PO CO: gdy okno jest doklejone (kafel/ćwiartka) albo zmaksymalizowane,
        // chcemy, żeby po ponownym chwyceniu "wyskoczyło" do rozmiaru SPRZED
        // pierwszego doklejenia — a nie ciągnęło się w rozmiarze ekranu za kursorem.
        // Trzymamy go w `user_data` okna, więc przeżywa kolejne graby.
        let current_states = surface.current_state().states;
        // `ToplevelStateSet` nie ma `intersects` — sprawdzamy każdy stan osobno.
        let is_tiled = current_states.contains(xdg_toplevel::State::Maximized)
            || current_states.contains(xdg_toplevel::State::TiledLeft)
            || current_states.contains(xdg_toplevel::State::TiledRight)
            || current_states.contains(xdg_toplevel::State::TiledTop)
            || current_states.contains(xdg_toplevel::State::TiledBottom);

        let free_size_cell = window
            .user_data()
            .get_or_insert(|| Mutex::new(window.geometry().size));
        let free_size = *free_size_cell.lock().expect("rozmiar swobodny zatruty");

        let (initial_window_size, initial_window_location) = if is_tiled {
            // Wyskakujemy z kafla/maksymalizacji od razu ustawiając swobodny rozmiar.
            surface.with_pending_state(|state| {
                state.states.unset(xdg_toplevel::State::TiledLeft);
                state.states.unset(xdg_toplevel::State::TiledRight);
                state.states.unset(xdg_toplevel::State::TiledTop);
                state.states.unset(xdg_toplevel::State::TiledBottom);
                state.states.unset(xdg_toplevel::State::Maximized);
                state.size = Some(free_size);
            });
            surface.send_pending_configure();
            let pos = pointer.current_location();
            (free_size, Point::from((pos.x as i32, pos.y as i32)))
        } else {
            // Okno swobodne — zapamiętaj jego rozmiar jako punkt powrotu po snappie.
            let size = window.geometry().size;
            *free_size_cell.lock().expect("rozmiar swobodny zatruty") = size;
            let location = self
                .space
                .element_location(&window)
                .unwrap_or_else(|| Point::from((0, 0)));
            (size, location)
        };

        let grab = PointerMoveSurfaceGrab {
            start_data,
            window,
            initial_window_location,
            initial_window_size,
            current_snap: None,
        };
        pointer.set_grab(self, grab, serial, Focus::Clear);
    }

    /// Obsługuje żądanie zmiany rozmiaru okna (`xdg_toplevel.resize`).
    pub fn resize_request_xdg(
        &mut self,
        surface: &ToplevelSurface,
        seat: &Seat<Self>,
        serial: Serial,
        edges: xdg_toplevel::ResizeEdge,
    ) {
        let Some(pointer) = seat.get_pointer() else {
            return;
        };
        if !pointer.has_grab(serial) {
            return;
        }
        let Some(start_data) = pointer.grab_start_data() else {
            return;
        };
        let Some(window) = self.window_for_surface(surface.wl_surface()) else {
            return;
        };
        let same_client = start_data
            .focus
            .as_ref()
            .map(|(focus, _)| focus.same_client_as(&surface.wl_surface().id()))
            .unwrap_or(false);
        if !same_client {
            return;
        }

        let geometry = window.geometry();
        let location = self
            .space
            .element_location(&window)
            .unwrap_or_else(|| Point::from((0, 0)));
        let (initial_window_location, initial_window_size) = (location, geometry.size);

        with_states(surface.wl_surface(), move |states| {
            states
                .data_map
                .get::<RefCell<SurfaceData>>()
                .expect("SurfaceData brak — patrz new_toplevel")
                .borrow_mut()
                .resize_state = ResizeState::Resizing(crate::grabs::ResizeData {
                edges: edges.into(),
                initial_window_location,
                initial_window_size,
            });
        });

        let grab = PointerResizeSurfaceGrab {
            start_data,
            window,
            edges: edges.into(),
            initial_window_location,
            initial_window_size,
            last_window_size: initial_window_size,
        };
        pointer.set_grab(self, grab, serial, Focus::Clear);
    }
}

impl CompositorHandler for MyCompositor {
    fn compositor_state(&mut self) -> &mut CompositorState {
        &mut self.compositor_state
    }

    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
        // Klient XWayland ma WŁASNY typ danych (tworzy go Smithay przy `XWayland::spawn`)
        // i też potrzebuje stanu kompozytora — bez tego panikowalibyśmy przy pierwszym
        // bindowaniu `wl_output` przez XWayland (a to psuje całą sesję).
        if let Some(state) = client.get_data::<XWaylandClientData>() {
            return &state.compositor_state;
        }
        if let Some(state) = client.get_data::<ClientState>() {
            return &state.compositor_state;
        }
        panic!("nieznany typ danych klienta — patrz init()")
    }

    fn new_surface(&mut self, _surface: &WlSurface) {
        // Nic nie robimy: w tym szkieletie nie importujemy dmabufów ani nie
        // montujemy pre-commit hooków. SHM obsługiwany jest przez ShmState.
    }

    fn commit(&mut self, surface: &WlSurface) {
        // Przetwarza bufor powierzchni (wymagane przez Smithay).
        on_commit_buffer_handler::<Self>(surface);

        // Znajdujemy korzeń drzewa powierzchni (dla ewentualnych subsurface'ów).
        let mut root = surface.clone();
        while let Some(parent) = get_parent(&root) {
            root = parent;
        }

        if let Some(window) = self.window_for_surface(&root) {
            window.on_commit();
            if &root == surface {
                // Klient może poprosić o przesunięcie okna (buffer_delta) —
                // np. przy synchronizacji zawartości. Respektujemy to.
                let buffer_offset = with_states(surface, |states| {
                    states
                        .cached_state
                        .get::<SurfaceAttributes>()
                        .current()
                        .buffer_delta
                        .take()
                });
                if let Some(offset) = buffer_offset {
                    if let Some(location) = self.space.element_location(&window) {
                        self.space
                            .map_element(window.clone(), location + offset, false);
                    }
                }

                self.ensure_initial_configure(surface);
            }
        }

        // Jeśli commituje powierzchnia ikony przeciągania (DnD), aktualizujemy jej
        // przesunięcie. Klient może przesuwać ikonę względem kursora (`buffer_delta`),
        // np. gdy przeciąga się kolumnę tabeli — chcemy to odwzorować.
        if let Some(icon) = self.dnd_icon.as_mut() {
            if &icon.surface == surface {
                let buffer_delta = with_states(surface, |states| {
                    states
                        .cached_state
                        .get::<SurfaceAttributes>()
                        .current()
                        .buffer_delta
                        .take()
                })
                .unwrap_or_default();
                icon.offset += buffer_delta;
            }
        }

        // Warstwy (panele/paski): po commicie klienta przeliczamy strefy i — jeśli
        // to pierwszy commit — wysyłamy PIERWSZY `configure`. Smithay celowo nie
        // robi tego w `arrange()` (patrz komentarz w `layer.rs`), więc musi to
        // zrobić kompozytor. Bez tego panel nie wie, jak szeroki ma być.
        if let Some(output) = self
            .space
            .outputs()
            .find(|output| {
                layer_map_for_output(output)
                    .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
                    .is_some()
            })
            .cloned()
        {
            let initial_configure_sent = with_states(surface, |states| {
                states
                    .data_map
                    .get::<LayerSurfaceData>()
                    .map(|data| data.lock().unwrap().initial_configure_sent)
                    .unwrap_or(false)
            });

            let mut map = layer_map_for_output(&output);
            // Najpierw przelicz (uwzględnij rozmiar/anchory podane przez klienta),
            // potem wyślij pierwszy configure.
            map.arrange();
            if !initial_configure_sent {
                if let Some(layer) = map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) {
                    layer.layer_surface().send_configure();
                }
            }
        }

        self.popups.commit(surface);
    }
}

impl ShmHandler for MyCompositor {
    fn shm_state(&self) -> &ShmState {
        &self.shm_state
    }
}

impl OutputHandler for MyCompositor {}

impl BufferHandler for MyCompositor {
    fn buffer_destroyed(
        &mut self,
        _buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
    ) {
    }
}

impl SeatHandler for MyCompositor {
    type KeyboardFocus = KeyboardFocusTarget;
    type PointerFocus = PointerFocusTarget;
    type TouchFocus = PointerFocusTarget;

    fn seat_state(&mut self) -> &mut SeatState<Self> {
        &mut self.seat_state
    }

    /// Gdy zmienia się fokus klawiatury, przekazujemy to schowkowi i selekcji
    /// pierwotnej — bez tego kopiuj/wklej "nie wie", który klient jest aktywny.
    fn focus_changed(&mut self, seat: &Seat<Self>, target: Option<&KeyboardFocusTarget>) {
        let dh = &self.display_handle;
        let wl_surface = target
            .and_then(WaylandFocus::wl_surface)
            .map(|surface| surface.into_owned());
        let client = wl_surface
            .as_ref()
            .and_then(|surface| dh.get_client(surface.id()).ok());
        set_data_device_focus(dh, seat, client.clone());
        set_primary_focus(dh, seat, client);

        // Wyróżnij wizualnie okno AKTYWNE (firmowy pasek aktywny + akcent),
        // a pozostałe przygaś. Porównujemy po OKNIE, a nie po `wl_surface`:
        // okno X11 (XWayland) może nie mieć jeszcze powiązanej powierzchni
        // Waylanda w chwili nadawania fokusu, a mimo to jest aktywnym oknem.
        for window in self.space.elements() {
            let is_focused = matches!(
                target,
                Some(KeyboardFocusTarget::Window(focused)) if focused == &window.0
            );
            window.set_focused(is_focused);
        }
    }

    fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
        self.cursor_status = image;
    }
}

impl XdgDecorationHandler for MyCompositor {
    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
        // DECYZJA: wymuszamy tryb SERVER-SIDE. Od tego momentu to KOMPOZYTOR
        // rysuje pasek tytułu — spójnie dla aplikacji, które znają ten protokół
        // (nasz toolkit, Qt, GTK4).
        //
        // Klienci, którzy NIE tworzą tego obiektu (np. GTK3), rysują własne CSD —
        // dla nich `PaganWindow::wants_ssd` zwróci `false`, żeby nie nakładać
        // dwóch pasków (patrz `mark_ssd_negotiated`).
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(DecorationMode::ServerSide);
        });
        crate::window::mark_ssd_negotiated(&toplevel);
        // Negocjacja potrafi przyjść PO pierwszym umieszczeniu okna w kaskadzie
        // (klient tworzy obiekt dekoracji chwilę po pierwszym commicie). Wtedy
        // okno stoi już tak, jakby paska nie było — dosuwamy je, żeby nasz pasek
        // nie schował się pod panelem.
        self.reposition_for_ssd(&toplevel);
        tracing::debug!("dekoracja: klient negocjuje (new_decoration)");
    }

    fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
        // Ignorujemy preferencję klienta — dekoracje rysujemy my.
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(DecorationMode::ServerSide);
        });
        tracing::debug!("dekoracja: request_mode -> ServerSide");
    }

    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
        // Brak preferencji klienta = nadal nasz pasek serwerowy.
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(DecorationMode::ServerSide);
        });
    }
}

impl MyCompositor {
    /// Dosuwa już umieszczone okno tak, aby pasek SSD zmieścił się na ekranie.
    ///
    /// PO CO osobno od `place_new_window`: w momencie pierwszego mapowania okno
    /// Waylanda może jeszcze nie mieć wynegocjowanych dekoracji, więc ląduje
    /// wyżej. Gdy klient je zgłosi, korygujemy pozycję (bez zmiany rozmiaru).
    fn reposition_for_ssd(&mut self, toplevel: &ToplevelSurface) {
        let Some(window) = self.window_for_surface(toplevel.wl_surface()) else {
            return;
        };
        let Some(location) = self.space.element_location(&window) else {
            return;
        };
        let adjusted = self.adjust_for_ssd(&window, location);
        if adjusted != location {
            self.space.map_element(window, adjusted, false);
        }
    }
}

impl XdgShellHandler for MyCompositor {
    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
        &mut self.xdg_shell_state
    }

    fn new_toplevel(&mut self, surface: ToplevelSurface) {
        // Nie wysyłamy configure tutaj — Smithay wymaga, by początkowy
        // configure poszedł przy pierwszym commicie (patrz `ensure_initial_configure`).
        let window = PaganWindow::new(surface.clone());

        // Doklejamy `SurfaceData` do powierzchni — używa jej grab resize do
        // śledzenia stanu operacji.
        with_states(surface.wl_surface(), |states| {
            states
                .data_map
                .insert_if_missing(|| RefCell::new(SurfaceData::default()));
        });

        self.map_window(window);
    }

    fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
        if let Err(err) = self.popups.track_popup(PopupKind::Xdg(surface)) {
            warn!("Nie udało się śledzić popupu: {}", err);
        }
    }

    fn reposition_request(
        &mut self,
        surface: PopupSurface,
        positioner: PositionerState,
        token: u32,
    ) {
        surface.with_pending_state(|state| {
            state.geometry = positioner.get_geometry();
            state.positioner = positioner;
        });
        surface.send_repositioned(token);
    }

    fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) {
        if let Some(seat) = Seat::from_resource(&seat) {
            self.move_request_xdg(&surface, &seat, serial);
        }
    }

    fn resize_request(
        &mut self,
        surface: ToplevelSurface,
        seat: wl_seat::WlSeat,
        serial: Serial,
        edges: xdg_toplevel::ResizeEdge,
    ) {
        if let Some(seat) = Seat::from_resource(&seat) {
            self.resize_request_xdg(&surface, &seat, serial, edges);
        }
    }

    fn ack_configure(&mut self, surface: WlSurface, configure: Configure) {
        // Dokańczamy maszynę stanów resize, gdy klient potwierdził finalny configure.
        // `Configure` to enum; serial niesie wariant toplevela (popupy go nie potrzebują).
        if let Configure::Toplevel(configure) = configure {
            let waiting_serial = with_states(&surface, |states| {
                states
                    .data_map
                    .get::<RefCell<SurfaceData>>()
                    .and_then(|data| {
                        if let ResizeState::WaitingForFinalAck(_, serial) =
                            data.borrow().resize_state
                        {
                            Some(serial)
                        } else {
                            None
                        }
                    })
            });

            if let Some(serial) = waiting_serial {
                let is_resizing = with_states(&surface, |states| {
                    states
                        .data_map
                        .get::<XdgToplevelSurfaceData>()
                        .map(|data| {
                            data.lock()
                                .unwrap()
                                .current
                                .states
                                .contains(xdg_toplevel::State::Resizing)
                        })
                        .unwrap_or(false)
                });

                if configure.serial >= serial && is_resizing {
                    with_states(&surface, |states| {
                        if let Some(data) = states.data_map.get::<RefCell<SurfaceData>>() {
                            let mut data = data.borrow_mut();
                            if let ResizeState::WaitingForFinalAck(resize_data, _) =
                                data.resize_state
                            {
                                data.resize_state = ResizeState::WaitingForCommit(resize_data);
                            }
                        }
                    });
                }
            }
        }
    }

    fn fullscreen_request(
        &mut self,
        surface: ToplevelSurface,
        wl_output: Option<wl_output::WlOutput>,
    ) {
        // Uproszczenie MVP: pełny ekran = rozmiar wyjścia. Respektujemy protokół
        // i zawsze odpowiadamy configure.
        let output = wl_output
            .as_ref()
            .and_then(Output::from_resource)
            .or_else(|| self.space.outputs().next().cloned());

        if let Some(output) = output {
            if let Some(geometry) = self.space.output_geometry(&output) {
                surface.with_pending_state(|state| {
                    state.states.set(xdg_toplevel::State::Fullscreen);
                    state.size = Some(geometry.size);
                });
            }
        }
        surface.send_configure();
    }

    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
        surface.with_pending_state(|state| {
            state.states.unset(xdg_toplevel::State::Fullscreen);
            state.size = None;
        });
        surface.send_pending_configure();
    }

    fn maximize_request(&mut self, surface: ToplevelSurface) {
        // Maksymalizacja w menedżerze pływającym = obszar ROBOCZY wyjścia
        // (bez panelu), żeby pasek systemowy pozostał widoczny.
        let Some(window) = self.window_for_surface(surface.wl_surface()) else {
            surface.send_configure();
            return;
        };
        if let Some(area) = self.output_work_area_for_window(&window) {
            surface.with_pending_state(|state| {
                state.states.set(xdg_toplevel::State::Maximized);
                state.size = Some(area.size);
            });
            self.space.map_element(window, area.loc, true);
        }
        surface.send_configure();
    }

    fn unmaximize_request(&mut self, surface: ToplevelSurface) {
        surface.with_pending_state(|state| {
            state.states.unset(xdg_toplevel::State::Maximized);
            state.size = None;
        });
        surface.send_pending_configure();
    }

    fn minimize_request(&mut self, surface: ToplevelSurface) {
        // Klient (np. nasz toolkit po kliknięciu przycisku) prosi o schowanie okna.
        // Robimy to samo, co skrót `Ctrl+Alt+M` — jedno miejsce prawdy.
        if let Some(window) = self.window_for_surface(surface.wl_surface()) {
            self.minimize_window(&window);
        }
    }

    fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
        // Popup (np. menu) prosi o grab wskaźnika/klawiatury. Musimy go ustanowić,
        // bo inaczej kliknięcie poza menu nie zamknie go.
        let Some(seat) = Seat::from_resource(&seat) else {
            return;
        };
        let kind = PopupKind::Xdg(surface);

        let Some(root_surface) = find_popup_root_surface(&kind).ok() else {
            return;
        };
        let Some(root_window) = self.window_for_surface(&root_surface) else {
            return;
        };
        let root = KeyboardFocusTarget::Window(root_window.0.clone());

        let Ok(mut grab) = self.popups.grab_popup(root, kind, &seat, serial) else {
            return;
        };

        if let Some(keyboard) = seat.get_keyboard() {
            if keyboard.is_grabbed()
                && !(keyboard.has_grab(serial)
                    || keyboard.has_grab(grab.previous_serial().unwrap_or(serial)))
            {
                grab.ungrab(PopupUngrabStrategy::All);
                return;
            }
            keyboard.set_focus(self, grab.current_grab(), serial);
            keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
        }

        if let Some(pointer) = seat.get_pointer() {
            if pointer.is_grabbed()
                && !(pointer.has_grab(serial)
                    || pointer.has_grab(grab.previous_serial().unwrap_or(serial)))
            {
                grab.ungrab(PopupUngrabStrategy::All);
                return;
            }
            pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
        }
    }
}

// --- Delegacje globali do naszego stanu -------------------------------------
// Te makra "spinają" globalne obiekty Waylanda z implementacjami traitów.
delegate_compositor!(MyCompositor);
delegate_shm!(MyCompositor);
delegate_output!(MyCompositor);
delegate_seat!(MyCompositor);
delegate_xdg_shell!(MyCompositor);
delegate_xdg_decoration!(MyCompositor);

// --- Protokoły "biurkowe": schowek/DnD, aktywacja, skala, ograniczenia -------

impl DataDeviceHandler for MyCompositor {
    fn data_device_state(&self) -> &DataDeviceState {
        &self.data_device_state
    }
}

impl ClientDndGrabHandler for MyCompositor {
    fn started(
        &mut self,
        _source: Option<WlDataSource>,
        icon: Option<WlSurface>,
        _seat: Seat<Self>,
    ) {
        // Ikona DnD: trzymamy powierzchnię; przesunięcie na razie zerowe (rysowanie
        // ikony podłączymy razem z kursorem z motywu XCursor).
        self.dnd_icon = icon.map(|surface| DndIcon {
            surface,
            offset: Point::from((0, 0)),
        });
    }

    fn dropped(&mut self, _target: Option<WlSurface>, _validated: bool, _seat: Seat<Self>) {
        self.dnd_icon = None;
    }
}

impl ServerDndGrabHandler for MyCompositor {
    fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {
        // Serwerowe graby DnD (np. przeciąganie pliku z panelu) nie są obsługiwane.
        warn!("serwerowy grab DnD nie jest obsługiwany");
    }
}

// `SelectionHandler` jest częścią delegacji data-device — osobnego makra brak.
impl SelectionHandler for MyCompositor {
    type SelectionUserData = ();
}

delegate_data_device!(MyCompositor);

impl PrimarySelectionHandler for MyCompositor {
    fn primary_selection_state(&self) -> &PrimarySelectionState {
        &self.primary_selection_state
    }
}
delegate_primary_selection!(MyCompositor);

impl XdgActivationHandler for MyCompositor {
    fn activation_state(&mut self) -> &mut XdgActivationState {
        &mut self.xdg_activation_state
    }

    fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
        // Token uznajemy za ważny, jeśli pochodzi z ostatniego wejścia naszej klawiatury.
        if let Some((serial, seat)) = data.serial {
            let keyboard = self.seat.get_keyboard().unwrap();
            Seat::from_resource(&seat) == Some(self.seat.clone())
                && keyboard
                    .last_enter()
                    .map(|last_enter| serial.is_no_older_than(&last_enter))
                    .unwrap_or(false)
        } else {
            false
        }
    }

    fn request_activation(
        &mut self,
        _token: XdgActivationToken,
        token_data: XdgActivationTokenData,
        surface: WlSurface,
    ) {
        // Świeży token (<10 s) => spełniamy prośbę i podnosimy okno.
        if token_data.timestamp.elapsed().as_secs() < 10 {
            if let Some(window) = self.window_for_surface(&surface) {
                self.space.raise_element(&window, true);
            }
        }
    }
}
delegate_xdg_activation!(MyCompositor);

impl FractionalScaleHandler for MyCompositor {
    fn new_fractional_scale(&mut self, surface: WlSurface) {
        // Ustalamy preferowaną skalę na podstawie wyjścia, na którym leży okno,
        // a jeśli nie wiemy — pierwszego wyjścia. Bez tego klienci HiDPI dostawaliby 1.0.
        let mut root = surface.clone();
        while let Some(parent) = get_parent(&root) {
            root = parent;
        }

        let output = self.space.outputs().next().cloned().or_else(|| {
            self.window_for_surface(&root)
                .and_then(|window| self.space.outputs_for_element(&window).first().cloned())
        });

        if let Some(output) = output {
            let scale = output.current_scale().fractional_scale();
            with_states(&surface, |states| {
                with_fractional_scale(states, |fractional| {
                    fractional.set_preferred_scale(scale);
                });
            });
        }
    }
}
delegate_fractional_scale!(MyCompositor);

impl PointerConstraintsHandler for MyCompositor {
    fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
        // Aktywujemy ograniczenie tylko dla powierzchni, która ma fokus wskaźnika.
        let Some(current_focus) = pointer.current_focus() else {
            return;
        };
        if current_focus.wl_surface().as_deref() == Some(surface) {
            with_pointer_constraint(surface, pointer, |constraint| {
                if let Some(constraint) = constraint {
                    constraint.activate();
                }
            });
        }
    }

    fn cursor_position_hint(
        &mut self,
        surface: &WlSurface,
        pointer: &PointerHandle<Self>,
        location: Point<f64, Logical>,
    ) {
        let active = with_pointer_constraint(surface, pointer, |constraint| {
            constraint.is_some_and(|constraint| constraint.is_active())
        });
        if active {
            let origin = self
                .space
                .elements()
                .find_map(|window| {
                    (window.wl_surface().as_deref() == Some(surface)).then(|| window.geometry())
                })
                .unwrap_or_default()
                .loc
                .to_f64();
            pointer.set_location(origin + location);
        }
    }
}
delegate_pointer_constraints!(MyCompositor);

// Text-input: sam global (bez input-method) — wystarcza, by klienty nie zgłaszały błędu.
delegate_text_input_manager!(MyCompositor);

// --- dmabuf, warstwy, viewporter, wskaźnik względny, gesty -----------------

impl DmabufHandler for MyCompositor {
    fn dmabuf_state(&mut self) -> &mut DmabufState {
        &mut self.dmabuf_state
    }

    fn dmabuf_imported(
        &mut self,
        _global: &DmabufGlobal,
        // `dmabuf` nieużywane bez feature `udev` (tam sprawdzamy import na GPU).
        #[cfg_attr(not(feature = "udev"), allow(unused_variables))] dmabuf: Dmabuf,
        notifier: ImportNotifier,
    ) {
        // W backendzie udev mamy renderery per GPU — sprawdzamy import naprawdę.
        #[cfg(feature = "udev")]
        {
            if let Some(udev) = self.udev.as_mut() {
                let node = udev.primary_gpu;
                if let Ok(mut renderer) = udev.gpus.single_renderer(&node) {
                    if renderer.as_mut().import_dmabuf(&dmabuf, None).is_ok() {
                        let _ = notifier.successful::<Self>();
                    } else {
                        notifier.failed();
                    }
                    return;
                }
            }
        }

        // Backend winit: renderer żyje lokalnie w pętli renderowania, więc nie
        // mamy go tutaj. Akceptujemy optymistycznie — global tworzymy z formatów
        // TEGO SAMEGO renderera, więc import przy renderowaniu się powiedzie.
        // WERYFIKUJ: przy problemach z dmabuf na winit przenieść backend do stanu.
        let _ = notifier.successful::<Self>();
    }
}
delegate_dmabuf!(MyCompositor);

impl WlrLayerShellHandler for MyCompositor {
    fn shell_state(&mut self) -> &mut WlrLayerShellState {
        &mut self.layer_shell_state
    }

    fn new_layer_surface(
        &mut self,
        surface: WlrLayerSurface,
        wl_output: Option<wl_output::WlOutput>,
        _layer: WlrLayer,
        namespace: String,
    ) {
        // Klient może wskazać wyjście, ale często tego nie robi — wtedy pierwsze.
        let output = wl_output
            .as_ref()
            .and_then(Output::from_resource)
            .or_else(|| self.space.outputs().next().cloned());
        let Some(output) = output else { return };

        let mut map = layer_map_for_output(&output);
        if let Err(err) = map.map_layer(&DesktopLayerSurface::new(surface, namespace)) {
            warn!(?err, "nie udało się zmapować layer surface");
        }
        // Przelicz strefy (non-exclusive zone) po dodaniu warstwy.
        map.arrange();
    }

    fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
        if let Some((mut map, layer)) = self.space.outputs().find_map(|output| {
            let map = layer_map_for_output(output);
            let layer = map
                .layers()
                .find(|layer| layer.layer_surface() == &surface)
                .cloned();
            layer.map(|layer| (map, layer))
        }) {
            map.unmap_layer(&layer);
            map.arrange();
        }
    }

    fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
        if let Err(err) = self.popups.track_popup(PopupKind::from(popup)) {
            warn!(?err, "nie udało się śledzić popupu warstwy");
        }
    }
}
delegate_layer_shell!(MyCompositor);

delegate_viewporter!(MyCompositor);
delegate_relative_pointer!(MyCompositor);
delegate_pointer_gestures!(MyCompositor);