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

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

1115 lines Raw ← Back
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
//! XWM — menedżer okien X11 dla XWayland.
//!
//! Selene jest kompozytorem, więc rolę menedżera okien X11 musi pełnić sam: przejmuje
//! `SubstructureRedirect` na oknie głównym X, zarządza oknami X11 i tłumaczy ich stan na
//! model Luny (tytuł, typ okna, zamykanie, maksymalizacja, geometria).
//!
//! Dlaczego osobny wątek: X11 ma własny protokół i własne gniazdo, a pętla protokołu
//! Waylanda nie może się na nim blokować. Kompozytor rozmawia z XWM przez kanał
//! ([`XwmCommand`]), a odczytuje stan przez [`Xwm::info_for_serial`].
//!
//! Powiązanie okno X11 ↔ powierzchnia Wayland: Xwayland wysyła do okna X11 komunikat
//! `WL_SURFACE_SERIAL` z serialem, który wcześniej ustawił na powierzchni
//! (`xwayland_surface_v1.set_serial`). Dlatego trzymamy mapę `serial → okno X11`.

use std::collections::HashMap;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;

use wayland_server::Resource;

use x11rb::connection::Connection;
use x11rb::protocol::composite::{ConnectionExt as CompositeExt, Redirect};
use x11rb::protocol::xproto::{
    Atom, AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
    ConnectionExt, EventMask, InputFocus, PropMode, Window,
};
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::x11_utils::Serialize;

use crate::compositor::SurfaceData;
use crate::state::State;
use crate::sync::LockExt;

/// Rodzaj okna X11 (z `_NET_WM_WINDOW_TYPE`), o tyle, o ile wpływa na wygląd.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum X11Kind {
    #[default]
    Normal,
    Dialog,
    /// Menu, podpowiedź, listy rozwijane — bez dekoracji, nad innymi oknami.
    Transient,
}

/// Stan okna X11, którego nie da się odczytać z Waylanda.
#[derive(Debug, Clone, Default)]
pub struct X11Info {
    pub title: String,
    pub override_redirect: bool,
    pub kind: X11Kind,
    /// Klient wyraźnie poprosił o brak ramki (`_MOTIF_WM_HINTS` bez dekoracji).
    /// Tak robią aplikacje z własnymi dekoracjami (CSD) — nie dokładamy wtedy naszego paska.
    pub client_decorations: bool,
    /// Pozycja i rozmiar okna w przestrzeni roota (dla menu/podpowiedzi).
    /// Menu X11 (override-redirect) ma współrzędne absolutne — musimy je znać,
    /// żeby narysować je tam, gdzie klient je umieścił, a nie w kaskadzie.
    pub x: i32,
    pub y: i32,
    pub width: i32,
    pub height: i32,
}

impl X11Info {
    /// Czy okno ma dostać nasze dekoracje (menu i podpowiedzi — nie).
    pub fn decorated(&self) -> bool {
        !self.override_redirect && self.kind != X11Kind::Transient && !self.client_decorations
    }
}

/// Zmiana stanu okna X11, o której kompozytor musi się dowiedzieć (XWM to osobny wątek).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X11Change {
    Fullscreen(bool),
    Maximized(bool),
    /// Klient prosi o zminimalizowanie/przywrócenie (`WM_CHANGE_STATE` / IconicState).
    Minimize(bool),
    /// Interaktywne przesunięcie od pozycji wskaźnika (`_NET_WM_MOVERESIZE`, kierunek 8).
    Move {
        x: i32,
        y: i32,
    },
    /// Interaktywna zmiana rozmiaru (bitmaska jak w `seat`: 1=góra, 2=dół, 4=lewo, 8=prawo).
    Resize {
        edges: u8,
        x: i32,
        y: i32,
    },
}

/// Polecenia z kompozytora do wątku XWM.
#[derive(Debug, Clone)]
pub enum XwmCommand {
    Close(Window),
    SetGeometry {
        window: Window,
        x: i32,
        y: i32,
        w: u32,
        h: u32,
    },
    SetActive(Option<Window>),
    SetMaximized {
        window: Window,
        on: bool,
    },
    /// Informuje klienta X11, ile miejsca zajmują nasze dekoracje.
    SetFrameExtents {
        window: Window,
        top: u32,
    },
    /// Prosi klienta o wejście/wyjście z pełnego ekranu (EWMH `_NET_WM_STATE_FULLSCREEN`).
    SetFullscreen {
        window: Window,
        on: bool,
    },
}

/// Współdzielony stan XWM (czytany przez kompozytor).
#[derive(Default)]
pub struct XwmState {
    /// `seriał powierzchni Wayland → okno X11` (z komunikatu `WL_SURFACE_SERIAL`).
    by_serial: HashMap<u64, Window>,
    /// `okno X11 → informacje` (tytuł, typ, override-redirect).
    windows: HashMap<Window, X11Info>,
    /// Okna zarządzane (na liście klientów), w kolejności pojawiania się.
    managed: Vec<Window>,
    /// Zmiany stanu zgłoszone przez klientów X11 (np. `_NET_WM_STATE_FULLSCREEN`).
    changes: Vec<(Window, X11Change)>,
    /// Ostatnio znany stan EWMH okna: `(fullscreen, maximized)`.
    ewmh: HashMap<Window, (bool, bool)>,
    /// Czy udało się zostać menedżerem okien X11.
    pub is_wm: bool,
}

impl XwmState {
    /// Informacje o oknie powiązanym z serialem powierzchni Wayland.
    pub fn info_for_serial(&self, serial: u64) -> Option<X11Info> {
        let win = *self.by_serial.get(&serial)?;
        self.windows.get(&win).cloned()
    }

    /// Okno X11 powiązane z serialem.
    pub fn window_for_serial(&self, serial: u64) -> Option<Window> {
        self.by_serial.get(&serial).copied()
    }

    /// Serial powierzchni Wayland odpowiadający oknu X11.
    pub fn serial_for_window(&self, window: Window) -> Option<u64> {
        self.by_serial
            .iter()
            .find(|(_, w)| **w == window)
            .map(|(serial, _)| *serial)
    }

    /// Zmiany stanu oczekujące na odebranie przez kompozytor.
    pub fn take_changes(&mut self) -> Vec<(Window, X11Change)> {
        std::mem::take(&mut self.changes)
    }
}

/// Uchwyt XWM: kanał poleceń + współdzielony stan.
#[derive(Clone)]
pub struct Xwm {
    tx: Sender<XwmCommand>,
    state: Arc<Mutex<XwmState>>,
}

/// Globalny uchwyt XWM.
///
/// Jest dokładnie jeden kompozytor w procesie, a XWM powstaje raz przy starcie — dzięki
/// temu kompozytor nie musi przekazywać go przez cały model okien.
static GLOBAL: OnceLock<Arc<Xwm>> = OnceLock::new();

/// Rejestruje działający XWM jako globalny.
pub fn set_global(xwm: Xwm) {
    let _ = GLOBAL.set(Arc::new(xwm));
}

/// Globalny XWM (jeśli X11 działa).
pub fn global() -> Option<&'static Arc<Xwm>> {
    GLOBAL.get()
}

impl Xwm {
    pub fn info_for_serial(&self, serial: u64) -> Option<X11Info> {
        self.state.lock_or_recover().info_for_serial(serial)
    }

    pub fn window_for_serial(&self, serial: u64) -> Option<Window> {
        self.state.lock_or_recover().window_for_serial(serial)
    }

    pub fn serial_for_window(&self, window: Window) -> Option<u64> {
        self.state.lock_or_recover().serial_for_window(window)
    }

    pub fn is_wm(&self) -> bool {
        self.state.lock_or_recover().is_wm
    }

    fn send(&self, cmd: XwmCommand) {
        // Kanał jest jedynym sposobem rozmowy z wątkiem X11; jego zamknięcie (X11 padł)
        // nie może przewrócić kompozytora.
        let _ = self.tx.send(cmd);
    }

    pub fn close(&self, window: Window) {
        self.send(XwmCommand::Close(window));
    }

    pub fn set_geometry(&self, window: Window, x: i32, y: i32, w: u32, h: u32) {
        self.send(XwmCommand::SetGeometry { window, x, y, w, h });
    }

    pub fn set_active(&self, window: Option<Window>) {
        self.send(XwmCommand::SetActive(window));
    }

    pub fn set_maximized(&self, window: Window, on: bool) {
        self.send(XwmCommand::SetMaximized { window, on });
    }

    pub fn set_frame_extents(&self, window: Window, top: u32) {
        self.send(XwmCommand::SetFrameExtents { window, top });
    }

    pub fn set_fullscreen(&self, window: Window, on: bool) {
        self.send(XwmCommand::SetFullscreen { window, on });
    }

    /// Odbiera zmiany stanu zgłoszone przez klientów X11 (woła kompozytor).
    pub fn take_changes(&self) -> Vec<(Window, X11Change)> {
        self.state.lock_or_recover().take_changes()
    }
}

/// Wysokość naszego paska tytułu — ile miejsca oddajemy klientom X11 w `_NET_FRAME_EXTENTS`.
/// Musi być równa [`crate::state::TITLEBAR`], bo dokładnie tyle miejsca zabiera nasz pasek.
pub const FRAME_TOP: u32 = crate::state::TITLEBAR as u32;

/// Uruchamia XWM. **Nie blokuje kompozytora.**
///
/// Xwayland potrzebuje chwili, zanim zacznie przyjmować połączenia, dlatego łączenie
/// ponawiamy w wątku; gdy się uda, XWM rejestruje się jako globalny ([`global`]).
/// Kompozytor nie musi więc czekać i od razu obsługuje klientów Waylanda.
pub fn spawn(display: u32) {
    let (tx, rx) = std::sync::mpsc::channel();
    let state: Arc<Mutex<XwmState>> = Arc::new(Mutex::new(XwmState::default()));
    let st = Arc::clone(&state);

    std::thread::spawn(move || {
        // 1) Połączenie z X11 — z ponowieniami (Xwayland wstaje asynchronicznie).
        let Some((conn, screen)) = connect_retry(display, Duration::from_secs(8)) else {
            return;
        };
        // Dopiero teraz X11 naprawdę działa — sesja może dostać `DISPLAY`
        // (wcześniej gniazdo bywało martwe i aplikacje nie mogły się połączyć).
        crate::xwayland::write_display_marker(display);
        let root = conn.setup().roots[screen].root;
        let Ok(mut atoms) = Atoms::new(&conn) else {
            return;
        };
        let is_wm = become_wm(&conn, root, &mut atoms).unwrap_or(false);
        st.lock_or_recover().is_wm = is_wm;
        if std::env::var("SELENE_STATS").is_ok() {
            eprintln!(
                "selene/xwm: X11 gotowy (ekran :{display}), menedżer okien: {}",
                if is_wm { "tak" } else { "nie" }
            );
        }
        // 2) Rejestracja globalna — kompozytor czyta `global()` przy każdej operacji,
        //    więc podłączenie „w locie” jest bezpieczne.
        set_global(Xwm {
            tx: tx.clone(),
            state: Arc::clone(&st),
        });
        run(conn, root, atoms, rx, st);
    });
}

/// Łączy się z X11, ponawiając próbę aż do `timeout`.
fn connect_retry(display: u32, timeout: Duration) -> Option<(RustConnection, usize)> {
    let name = format!(":{display}");
    let deadline = std::time::Instant::now() + timeout;
    loop {
        match x11rb::connect(Some(&name)) {
            Ok(v) => return Some(v),
            Err(e) => {
                if std::time::Instant::now() >= deadline {
                    if std::env::var("SELENE_STATS").is_ok() {
                        eprintln!("selene/xwm: brak połączenia z X11: {e}");
                    }
                    return None;
                }
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
}

/// Pętla wątku X11: polecenia z kompozytora + zdarzenia z X11.
fn run(
    conn: RustConnection,
    root: Window,
    atoms: Atoms,
    rx: Receiver<XwmCommand>,
    state: Arc<Mutex<XwmState>>,
) {
    loop {
        // 1) Polecenia od kompozytora.
        while let Ok(cmd) = rx.try_recv() {
            apply_command(&conn, &root, &atoms, &state, cmd);
        }
        // 2) Zdarzenia X11.
        let mut handled = false;
        while let Ok(Some(event)) = conn.poll_for_event() {
            handled = true;
            handle_event(&conn, root, &atoms, &state, event);
        }
        let _ = conn.flush();
        if !handled {
            std::thread::sleep(Duration::from_millis(4));
        }
    }
}

/// Przejmuje rolę menedżera okien X11 (`SubstructureRedirect`).
fn become_wm(conn: &RustConnection, root: Window, atoms: &mut Atoms) -> Option<bool> {
    let aux = ChangeWindowAttributesAux::new()
        .event_mask(EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY);
    // Gdy inny menedżer okien już działa, X zwraca `Access` — nie walczymy z nim.
    let ok = conn
        .change_window_attributes(root, &aux)
        .ok()
        .and_then(|c| c.check().ok())
        .is_some();
    if !ok {
        return Some(false);
    }
    // `_NET_SUPPORTING_WM_CHECK`: dowód dla klientów, że mamy EWMH.
    let wm_window = conn.generate_id().ok()?;
    conn.create_window(
        x11rb::COPY_FROM_PARENT as u8,
        wm_window,
        root,
        0,
        0,
        1,
        1,
        0,
        x11rb::protocol::xproto::WindowClass::INPUT_OUTPUT,
        x11rb::COPY_FROM_PARENT as u32,
        &Default::default(),
    )
    .ok()?;
    set_prop32(
        conn,
        root,
        atoms.net_supporting_wm_check,
        AtomEnum::WINDOW.into(),
        &[wm_window],
    );
    set_prop32(
        conn,
        wm_window,
        atoms.net_supporting_wm_check,
        AtomEnum::WINDOW.into(),
        &[wm_window],
    );
    set_prop_str(conn, wm_window, AtomEnum::WM_NAME.into(), "Luna");
    // Lista obsługiwanych rozszerzeń EWMH, których naprawdę używamy.
    let supported: Vec<Atom> = vec![
        atoms.net_wm_state,
        atoms.net_wm_state_maximized_vert,
        atoms.net_wm_state_maximized_horz,
        atoms.net_wm_state_fullscreen,
        atoms.net_close_window,
        atoms.net_active_window,
        atoms.net_client_list,
        atoms.net_wm_name,
        atoms.net_wm_window_type,
        atoms.net_frame_extents,
    ];
    set_prop32(
        conn,
        root,
        atoms.net_supported,
        AtomEnum::ATOM.into(),
        &supported,
    );
    // Composite: w trybie rootless Xwayland tworzy powierzchnię Wayland dla okna dopiero
    // wtedy, gdy okno jest przekierowane (`redirectDraw == RedirectDrawManual`). Bez tego
    // klient X11 działa w tle, ale kompozytor nie dostaje żadnej powierzchni do rysowania.
    // `RedirectSubwindows` obejmuje zarówno okna już istniejące, jak i wszystkie przyszłe
    // (X serwera przekierowuje je w hooku `CreateWindow`), więc wystarczy jedno wywołanie.
    if !enable_composite(conn, root) {
        if std::env::var("SELENE_STATS").is_ok() {
            eprintln!("selene/xwm: Composite niedostępny — okna X11 nie będą widoczne");
        }
    }
    let _ = conn.flush();
    Some(true)
}

/// Włącza przekierowanie Composite dla okien potomnych roota.
fn enable_composite(conn: &RustConnection, root: Window) -> bool {
    let version = conn
        .composite_query_version(0, 4)
        .ok()
        .and_then(|c| c.reply().ok());
    let Some(version) = version else {
        return false;
    };
    let ok = conn
        .composite_redirect_subwindows(root, Redirect::MANUAL)
        .ok()
        .and_then(|c| c.check().ok())
        .is_some();
    if std::env::var("SELENE_STATS").is_ok() {
        eprintln!(
            "selene/xwm: Composite {}.{} → przekierowanie okien: {}",
            version.major_version,
            version.minor_version,
            if ok { "tak" } else { "nie" }
        );
    }
    ok
}

fn handle_event(
    conn: &RustConnection,
    root: Window,
    atoms: &Atoms,
    state: &Arc<Mutex<XwmState>>,
    event: Event,
) {
    match event {
        Event::CreateNotify(e) => {
            // Zmiany własności (`_NET_WM_NAME`, `_NET_WM_WINDOW_TYPE`, `_MOTIF_WM_HINTS`)
            // trafiają **do okna**, nie do roota — musimy wybrać maskę na samym oknie.
            // Każdy klient ma własną maskę, więc nie nadpisujemy wyboru aplikacji.
            if e.parent == root {
                let aux = ChangeWindowAttributesAux::new()
                    .event_mask(EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY);
                let _ = conn.change_window_attributes(e.window, &aux);
                // Zapamiętaj pozycję od razu — menu może się pojawić zanim przyjdzie
                // `WL_SURFACE_SERIAL`, a wtedy chcemy je postawić we właściwym miejscu.
                {
                    let mut st = state.lock_or_recover();
                    let info = st.windows.entry(e.window).or_default();
                    info.x = i32::from(e.x);
                    info.y = i32::from(e.y);
                    info.width = i32::from(e.width);
                    info.height = i32::from(e.height);
                }
            }
        }
        Event::ConfigureNotify(e) => {
            let mut st = state.lock_or_recover();
            let info = st.windows.entry(e.window).or_default();
            info.x = i32::from(e.x);
            info.y = i32::from(e.y);
            info.width = i32::from(e.width);
            info.height = i32::from(e.height);
        }
        Event::MapRequest(e) => {
            let over = is_override_redirect(conn, e.window);
            let info = read_info(conn, atoms, e.window);
            if std::env::var("SELENE_STATS").is_ok() {
                eprintln!(
                    "selene/xwm: MapRequest 0x{:x} '{}' over={} csd={}",
                    e.window, info.title, over, info.client_decorations
                );
            }
            {
                let mut info = info;
                info.override_redirect = over;
                insert_info(state, e.window, info);
            }
            if !over {
                let mut st = state.lock_or_recover();
                if !st.managed.contains(&e.window) {
                    st.managed.push(e.window);
                }
            }
            // Menu i podpowiedzi mapujemy od razu; reszta i tak trafia do naszego modelu
            // przez `commit` powierzchni Wayland — mapujemy zawsze, żeby klient żył.
            let mapped = conn.map_window(e.window);
            let _ = conn.flush();
            if std::env::var("SELENE_STATS").is_ok() {
                let state = conn
                    .get_window_attributes(e.window)
                    .ok()
                    .and_then(|c| c.reply().ok())
                    .map(|a| a.map_state);
                eprintln!(
                    "selene/xwm: map_window 0x{:x} -> ok={} state={:?}",
                    e.window,
                    mapped.is_ok(),
                    state
                );
            }
            publish_client_list(conn, root, atoms, state);
        }
        Event::ConfigureRequest(e) => {
            // Klient prosi o geometrię — spełniamy (nie mamy własnej polityki kafelkowej).
            let aux = ConfigureWindowAux::new()
                .x(i32::from(e.x))
                .y(i32::from(e.y))
                .width(u32::from(e.width))
                .height(u32::from(e.height));
            let _ = conn.configure_window(e.window, &aux);
            let _ = conn.flush();
        }
        Event::PropertyNotify(e) => {
            if e.atom == atoms.net_wm_name
                || e.atom == AtomEnum::WM_NAME.into()
                || e.atom == atoms.net_wm_window_type
                || e.atom == atoms.motif_wm_hints
            {
                let info = read_info(conn, atoms, e.window);
                if std::env::var("SELENE_STATS").is_ok() && e.atom == atoms.motif_wm_hints {
                    eprintln!(
                        "selene/xwm: 0x{:x} dekoracje klienta: {}",
                        e.window, info.client_decorations
                    );
                }
                insert_info(state, e.window, info);
            }
        }
        Event::ClientMessage(e) => {
            if e.type_ == atoms.wl_surface_serial {
                // Powiązanie okno X11 ↔ powierzchnia Wayland (patrz opis modułu).
                let serial = serial_from_client_message(&e.data);
                if serial != 0 {
                    let info = read_info(conn, atoms, e.window);
                    if std::env::var("SELENE_STATS").is_ok() {
                        eprintln!(
                            "selene/xwm: WL_SURFACE_SERIAL 0x{:x} '{}' serial={}",
                            e.window, info.title, serial
                        );
                    }
                    let mut st = state.lock_or_recover();
                    st.by_serial.insert(serial, e.window);
                    drop(st);
                    insert_info(state, e.window, info);
                }
            } else if e.type_ == atoms.net_close_window {
                request_close(conn, e.window);
            } else if e.type_ == atoms.net_wm_state {
                // Klient prosi o zmianę stanu (pełny ekran, maksymalizacja).
                // `data = [action, atom1, atom2, source, 0]`; akcja 0 = usuń, 1 = dodaj,
                // 2 = przełącz. Samo ustawienie robi kompozytor — my tylko przekazujemy
                // życzenie (XWM nie zna modelu okien Waylanda).
                let data = e.data.as_data32();
                let action = data[0];
                let mut st = state.lock_or_recover();
                let mut flags = *st.ewmh.get(&e.window).unwrap_or(&(false, false));
                let mut pending: Vec<X11Change> = Vec::new();
                for state_atom in [data[1], data[2]] {
                    if state_atom == 0 {
                        continue;
                    }
                    let (fullscreen, current) = if state_atom == atoms.net_wm_state_fullscreen {
                        (true, flags.0)
                    } else if state_atom == atoms.net_wm_state_maximized_vert
                        || state_atom == atoms.net_wm_state_maximized_horz
                    {
                        (false, flags.1)
                    } else {
                        continue;
                    };
                    let on = match action {
                        0 => false,
                        1 => true,
                        2 => !current,
                        _ => continue,
                    };
                    let change = if fullscreen {
                        flags.0 = on;
                        X11Change::Fullscreen(on)
                    } else {
                        flags.1 = on;
                        X11Change::Maximized(on)
                    };
                    pending.push(change);
                }
                st.ewmh.insert(e.window, flags);
                st.changes
                    .extend(pending.into_iter().map(|c| (e.window, c)));
            } else if e.type_ == atoms.wm_change_state {
                // `WM_CHANGE_STATE`: 1 = NormalState, 3 = IconicState (minimalizacja).
                let data = e.data.as_data32();
                let iconic = data[0] == 3;
                state
                    .lock_or_recover()
                    .changes
                    .push((e.window, X11Change::Minimize(iconic)));
            } else if e.type_ == atoms.net_wm_moveresize {
                // `data = [x_root, y_root, direction, button, source]`.
                let data = e.data.as_data32();
                let (x, y, direction) = (data[0] as i32, data[1] as i32, data[2]);
                let change = match direction {
                    // 8 = Move, 0..=7 = krawędzie/narożniki, 11 = Cancel.
                    8 => Some(X11Change::Move { x, y }),
                    0..=7 => Some(X11Change::Resize {
                        edges: move_resize_edges(direction),
                        x,
                        y,
                    }),
                    _ => None,
                };
                if let Some(change) = change {
                    state.lock_or_recover().changes.push((e.window, change));
                }
            }
        }
        Event::UnmapNotify(e) => {
            let mut st = state.lock_or_recover();
            st.managed.retain(|w| *w != e.window);
            drop(st);
            publish_client_list(conn, root, atoms, state);
        }
        Event::DestroyNotify(e) => {
            let mut st = state.lock_or_recover();
            st.windows.remove(&e.window);
            st.managed.retain(|w| *w != e.window);
            st.by_serial.retain(|_, w| *w != e.window);
            st.ewmh.remove(&e.window);
        }
        _ => {}
    }
}

fn apply_command(
    conn: &RustConnection,
    root: &Window,
    atoms: &Atoms,
    state: &Arc<Mutex<XwmState>>,
    cmd: XwmCommand,
) {
    match cmd {
        XwmCommand::Close(win) => request_close(conn, win),
        XwmCommand::SetGeometry { window, x, y, w, h } => {
            let aux = ConfigureWindowAux::new().x(x).y(y).width(w).height(h);
            let _ = conn.configure_window(window, &aux);
        }
        XwmCommand::SetActive(window) => {
            let list: Vec<u32> = window.into_iter().collect();
            set_prop32(
                conn,
                *root,
                atoms.net_active_window,
                AtomEnum::WINDOW.into(),
                &list,
            );
            if let Some(w) = window {
                let _ = conn.set_input_focus(InputFocus::POINTER_ROOT, w, x11rb::CURRENT_TIME);
            }
        }
        XwmCommand::SetMaximized { window, on } => {
            // Prosimy klienta o zmianę stanu (EWMH) — tak działają wszystkie pulpity.
            let action = if on { 1u32 } else { 0u32 }; // _NET_WM_STATE_ADD / _REMOVE
            for state_atom in [
                atoms.net_wm_state_maximized_vert,
                atoms.net_wm_state_maximized_horz,
            ] {
                send_wm_state(conn, atoms, window, action, state_atom);
            }
        }
        XwmCommand::SetFrameExtents { window, top } => {
            set_prop32(
                conn,
                window,
                atoms.net_frame_extents,
                AtomEnum::CARDINAL.into(),
                &[0, 0, top, 0],
            );
        }
        XwmCommand::SetFullscreen { window, on } => {
            let action = if on { 1u32 } else { 0u32 }; // _NET_WM_STATE_ADD / _REMOVE
            send_wm_state(conn, atoms, window, action, atoms.net_wm_state_fullscreen);
            state
                .lock_or_recover()
                .ewmh
                .entry(window)
                .or_insert((false, false))
                .0 = on;
        }
    }
    let _ = conn.flush();
    let _ = state;
}

/// Odczytuje serial z komunikatu `WL_SURFACE_SERIAL` (dwa razy 32 bity: `lo`, `hi`).
/// Bitmaska krawędzi dla kierunku `_NET_WM_MOVERESIZE` (1=góra, 2=dół, 4=lewo, 8=prawo).
fn move_resize_edges(direction: u32) -> u8 {
    match direction {
        0 => 1 | 4, // lewy-górny
        1 => 1,     // góra
        2 => 1 | 8, // prawy-górny
        3 => 8,     // prawo
        4 => 2 | 8, // prawy-dolny
        5 => 2,     // dół
        6 => 2 | 4, // lewy-dolny
        7 => 4,     // lewo
        _ => 0,
    }
}

fn serial_from_client_message(data: &x11rb::protocol::xproto::ClientMessageData) -> u64 {
    let raw = data.serialize();
    if raw.len() < 8 {
        return 0;
    }
    let lo = u32::from_ne_bytes([raw[0], raw[1], raw[2], raw[3]]);
    let hi = u32::from_ne_bytes([raw[4], raw[5], raw[6], raw[7]]);
    (u64::from(hi) << 32) | u64::from(lo)
}

/// Prosi klienta o zamknięcie okna (`WM_DELETE_WINDOW`), a gdy nie umie — ubija połączenie.
fn request_close(conn: &RustConnection, window: Window) {
    // `WM_PROTOCOLS` to lista protokołów obsługiwanych przez klienta; zamykanie
    // odbywa się przez wysłanie `ClientMessage` typu `WM_PROTOCOLS` z `WM_DELETE_WINDOW`
    // w pierwszym słowie. Wcześniej wysyłaliśmy typ = pierwszy atom z listy i dane
    // `[16, 0, …]`, co żaden klient nie rozpoznawał — „X” na pasku nic nie robiło.
    let Ok(wm_protocols) = atom(conn, "WM_PROTOCOLS") else {
        return;
    };
    let Ok(wm_delete) = atom(conn, "WM_DELETE_WINDOW") else {
        return;
    };
    let protocols = get_atoms_prop(conn, window, wm_protocols);
    if protocols.contains(&wm_delete) {
        send_client_message(conn, window, wm_protocols, &[wm_delete, 0, 0, 0, 0]);
    }
}

fn send_wm_state(conn: &RustConnection, atoms: &Atoms, window: Window, action: u32, state: Atom) {
    send_client_message(conn, window, atoms.net_wm_state, &[action, state, 0, 1, 0]);
}

/// Wysyła `ClientMessage` (`WM_PROTOCOLS`-style) do okna.
fn send_client_message(conn: &RustConnection, window: Window, type_: Atom, data: &[u32; 5]) {
    let event = ClientMessageEvent::new(32, window, type_, *data);
    let _ = conn.send_event(false, window, EventMask::NO_EVENT, event);
    let _ = conn.flush();
}

/// Ustawia listę okien klientów (`_NET_CLIENT_LIST`) — używają jej panele i aplikacje.
fn publish_client_list(
    conn: &RustConnection,
    root: Window,
    atoms: &Atoms,
    state: &Arc<Mutex<XwmState>>,
) {
    let list = state.lock_or_recover().managed.clone();
    set_prop32(
        conn,
        root,
        atoms.net_client_list,
        AtomEnum::WINDOW.into(),
        &list,
    );
    let _ = conn.flush();
}

/// Czy okno ma `override_redirect` (menu, podpowiedź, okna „niezarządzane”).
fn is_override_redirect(conn: &RustConnection, window: Window) -> bool {
    conn.get_window_attributes(window)
        .ok()
        .and_then(|c| c.reply().ok())
        .map(|a| a.override_redirect)
        .unwrap_or(false)
}

/// Zapisuje informacje o oknie, **zachowując geometrię** z `CreateNotify`/
/// `ConfigureNotify` (odczyt własności jej nie zawiera).
fn insert_info(state: &Arc<Mutex<XwmState>>, window: Window, mut info: X11Info) {
    let mut st = state.lock_or_recover();
    if let Some(prev) = st.windows.get(&window) {
        info.x = prev.x;
        info.y = prev.y;
        info.width = prev.width;
        info.height = prev.height;
    }
    st.windows.insert(window, info);
}

/// Czyta tytuł i typ okna X11.
fn read_info(conn: &RustConnection, atoms: &Atoms, window: Window) -> X11Info {
    let title = read_title(conn, atoms, window);
    let kind = read_kind(conn, atoms, window);
    X11Info {
        title,
        override_redirect: is_override_redirect(conn, window),
        kind,
        client_decorations: has_client_decorations(conn, atoms, window),
        ..X11Info::default()
    }
}

/// Czy klient chce sam rysować dekoracje (`_MOTIF_WM_HINTS` z wyłączonymi dekoracjami).
///
/// To standardowy sposób, w jaki GTK/Qt/Chrome mówią menedżerowi okien „nie dodawaj ramki”.
/// Flagi: bit 2 (`1 << 1`) mówi, że pole `decorations` jest istotne; `decorations == 0`
/// oznacza „bez ramki” (dekoracje po stronie klienta).
fn has_client_decorations(conn: &RustConnection, atoms: &Atoms, window: Window) -> bool {
    let hints = get_atoms_prop(conn, window, atoms.motif_wm_hints);
    hints.len() >= 3 && hints[0] & 0b10 != 0 && hints[2] == 0
}

/// Tytuł: najpierw `_NET_WM_NAME` (UTF-8), potem klasyczny `WM_NAME`.
fn read_title(conn: &RustConnection, atoms: &Atoms, window: Window) -> String {
    if let Some(utf8) = get_prop8(conn, window, atoms.net_wm_name) {
        if let Ok(s) = String::from_utf8(utf8) {
            let s = s.trim_end_matches('\0').trim();
            if !s.is_empty() {
                return s.to_owned();
            }
        }
    }
    get_prop8(conn, window, AtomEnum::WM_NAME.into())
        .and_then(|b| String::from_utf8(b).ok())
        .map(|s| s.trim_end_matches('\0').trim().to_owned())
        .unwrap_or_default()
}

/// Typ okna: menu/podpowiedzi nie dostają dekoracji.
fn read_kind(conn: &RustConnection, atoms: &Atoms, window: Window) -> X11Kind {
    let types = get_atoms_prop(conn, window, atoms.net_wm_window_type);
    for t in types {
        if t == atoms.net_wm_window_type_menu
            || t == atoms.net_wm_window_type_dropdown
            || t == atoms.net_wm_window_type_tooltip
            || t == atoms.net_wm_window_type_popup
            || t == atoms.net_wm_window_type_notification
        {
            return X11Kind::Transient;
        }
        if t == atoms.net_wm_window_type_dialog
            || t == atoms.net_wm_window_type_utility
            || t == atoms.net_wm_window_type_splash
        {
            return X11Kind::Dialog;
        }
    }
    X11Kind::Normal
}

/// Wartość własności jako lista 32-bitowa.
fn get_atoms_prop(conn: &RustConnection, window: Window, property: Atom) -> Vec<u32> {
    conn.get_property(false, window, property, AtomEnum::ANY, 0, 1024)
        .ok()
        .and_then(|c| c.reply().ok())
        .and_then(|r| r.value32().map(|it| it.collect()))
        .unwrap_or_default()
}

/// Wartość własności jako bajty (tekst UTF-8 / Latin-1).
fn get_prop8(conn: &RustConnection, window: Window, property: Atom) -> Option<Vec<u8>> {
    let reply = conn
        .get_property(false, window, property, AtomEnum::ANY, 0, 1024)
        .ok()?
        .reply()
        .ok()?;
    if reply.value.is_empty() {
        return None;
    }
    Some(reply.value)
}

fn set_prop32(conn: &RustConnection, window: Window, property: Atom, type_: Atom, data: &[u32]) {
    // x11rb 0.14 nie ma `change_property32` — serializujemy liczby sami.
    let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
    let _ = conn.change_property(
        PropMode::REPLACE,
        window,
        property,
        type_,
        32,
        data.len() as u32,
        &bytes,
    );
}

fn set_prop_str(conn: &RustConnection, window: Window, property: Atom, text: &str) {
    let _ = conn.change_property(
        PropMode::REPLACE,
        window,
        property,
        AtomEnum::STRING,
        8,
        text.len() as u32,
        text.as_bytes(),
    );
}

/// Internowane atomy, których używamy (internowanie to jedno zapytanie do X).
struct Atoms {
    net_supported: Atom,
    net_supporting_wm_check: Atom,
    net_client_list: Atom,
    net_active_window: Atom,
    net_close_window: Atom,
    net_wm_state: Atom,
    net_wm_state_maximized_vert: Atom,
    net_wm_state_maximized_horz: Atom,
    net_wm_state_fullscreen: Atom,
    motif_wm_hints: Atom,
    net_wm_name: Atom,
    net_wm_window_type: Atom,
    net_wm_window_type_dialog: Atom,
    net_wm_window_type_utility: Atom,
    net_wm_window_type_splash: Atom,
    net_wm_window_type_menu: Atom,
    net_wm_window_type_dropdown: Atom,
    net_wm_window_type_tooltip: Atom,
    net_wm_window_type_popup: Atom,
    net_wm_window_type_notification: Atom,
    net_frame_extents: Atom,
    wl_surface_serial: Atom,
    wm_change_state: Atom,
    net_wm_moveresize: Atom,
}

impl Atoms {
    fn new(conn: &RustConnection) -> Result<Self, x11rb::errors::ReplyError> {
        Ok(Self {
            net_supported: atom(conn, "_NET_SUPPORTED")?,
            net_supporting_wm_check: atom(conn, "_NET_SUPPORTING_WM_CHECK")?,
            net_client_list: atom(conn, "_NET_CLIENT_LIST")?,
            net_active_window: atom(conn, "_NET_ACTIVE_WINDOW")?,
            net_close_window: atom(conn, "_NET_CLOSE_WINDOW")?,
            net_wm_state: atom(conn, "_NET_WM_STATE")?,
            net_wm_state_maximized_vert: atom(conn, "_NET_WM_STATE_MAXIMIZED_VERT")?,
            net_wm_state_maximized_horz: atom(conn, "_NET_WM_STATE_MAXIMIZED_HORZ")?,
            net_wm_state_fullscreen: atom(conn, "_NET_WM_STATE_FULLSCREEN")?,
            motif_wm_hints: atom(conn, "_MOTIF_WM_HINTS")?,
            net_wm_name: atom(conn, "_NET_WM_NAME")?,
            net_wm_window_type: atom(conn, "_NET_WM_WINDOW_TYPE")?,
            net_wm_window_type_dialog: atom(conn, "_NET_WM_WINDOW_TYPE_DIALOG")?,
            net_wm_window_type_utility: atom(conn, "_NET_WM_WINDOW_TYPE_UTILITY")?,
            net_wm_window_type_splash: atom(conn, "_NET_WM_WINDOW_TYPE_SPLASH")?,
            net_wm_window_type_menu: atom(conn, "_NET_WM_WINDOW_TYPE_MENU")?,
            net_wm_window_type_dropdown: atom(conn, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU")?,
            net_wm_window_type_tooltip: atom(conn, "_NET_WM_WINDOW_TYPE_TOOLTIP")?,
            net_wm_window_type_popup: atom(conn, "_NET_WM_WINDOW_TYPE_POPUP_MENU")?,
            net_wm_window_type_notification: atom(conn, "_NET_WM_WINDOW_TYPE_NOTIFICATION")?,
            net_frame_extents: atom(conn, "_NET_FRAME_EXTENTS")?,
            wl_surface_serial: atom(conn, "WL_SURFACE_SERIAL")?,
            wm_change_state: atom(conn, "WM_CHANGE_STATE")?,
            net_wm_moveresize: atom(conn, "_NET_WM_MOVERESIZE")?,
        })
    }
}

fn atom(conn: &RustConnection, name: &str) -> Result<Atom, x11rb::errors::ReplyError> {
    conn.intern_atom(false, name.as_bytes())?
        .reply()
        .map(|r| r.atom)
}

/// Synchronizuje model kompozytora z XWM (wołane raz na iterację pętli głównej).
///
/// XWM działa w osobnym wątku i nie ma dostępu do obiektów Waylanda, więc tytuły oraz
/// życzenia stanu (pełny ekran, maksymalizacja) od klientów X11 odbieramy tutaj.
pub fn sync(state: &mut State) {
    sync_window_info(state);
    let Some(xwm) = global() else {
        return;
    };
    for (window, change) in xwm.take_changes() {
        let Some(serial) = xwm.serial_for_window(window) else {
            continue;
        };
        let Some(slot) = slot_for_serial(state, serial) else {
            continue;
        };
        match change {
            X11Change::Fullscreen(on) => crate::seat::set_fullscreen(state, slot, on),
            X11Change::Maximized(on) => crate::seat::set_maximized(state, slot, on),
            X11Change::Minimize(on) => crate::seat::set_minimized(state, slot, on),
            X11Change::Move { x, y } => crate::seat::interactive_move_at(
                state,
                slot,
                libluna_core::Point::new(x as f64, y as f64),
            ),
            X11Change::Resize { edges, x, y } => crate::seat::interactive_resize_at(
                state,
                slot,
                edges,
                libluna_core::Point::new(x as f64, y as f64),
            ),
        }
    }
}

/// Aktualizuje tytuł i tryb dekoracji okien X11.
///
/// Xwayland wysyła `WL_SURFACE_SERIAL` **po** pierwszym `commit`, więc informacje z XWM
/// docierają chwilę po utworzeniu okna. Bez tego kroku tytuł zostałby pusty, a klient
/// z własnymi dekoracjami (CSD) dostałby od nas drugi pasek tytułu.
fn sync_window_info(state: &mut State) {
    let Some(xwm) = global() else {
        return;
    };
    let updates: Vec<(usize, X11Info)> = state
        .slots
        .iter()
        .enumerate()
        .filter_map(|(slot, surface)| {
            let data = surface.data::<SurfaceData>()?;
            if !*data.xwayland.lock_or_recover() {
                return None;
            }
            let serial = *data.x11_serial.lock_or_recover();
            if serial == 0 {
                return None;
            }
            let info = xwm.info_for_serial(serial)?;
            let is_popup = info.override_redirect || info.kind == X11Kind::Transient;
            let changed = {
                let windows = state.windows.lock_or_recover();
                let w = windows.get(slot)?;
                let moved =
                    is_popup && info.width > 0 && (w.x != info.x as f64 || w.y != info.y as f64);
                w.title != info.title || w.ssd != info.decorated() || w.popup != is_popup || moved
            };
            changed.then_some((slot, info))
        })
        .collect();
    for (slot, info) in updates {
        let is_popup = info.override_redirect || info.kind == X11Kind::Transient;
        if std::env::var("SELENE_STATS").is_ok() {
            eprintln!(
                "selene/win: X11 zaktualizowane '{}' ssd={} popup={}",
                info.title,
                info.decorated(),
                is_popup
            );
        }
        {
            let mut windows = state.windows.lock_or_recover();
            if let Some(w) = windows.get_mut(slot) {
                w.title = info.title.clone();
                w.ssd = info.decorated();
                w.popup = is_popup;
                // Menu X11 ma współrzędne absolutne — ustawiamy je dokładnie tam,
                // gdzie klient je otworzył (klik w menu wtedy trafia w menu).
                if is_popup && info.width > 0 && info.height > 0 {
                    w.x = info.x as f64;
                    w.y = info.y as f64;
                }
            }
        }
        if let Some(surface) = state.slots.get(slot).cloned() {
            crate::foreign_toplevel::broadcast_title(state, &surface, &info.title);
        }
    }
}

/// Slot okna odpowiadający serialowi powierzchni X11.
fn slot_for_serial(state: &State, serial: u64) -> Option<usize> {
    state.slots.iter().position(|surface| {
        surface
            .data::<SurfaceData>()
            .map(|d| *d.xwayland.lock_or_recover() && *d.x11_serial.lock_or_recover() == serial)
            .unwrap_or(false)
    })
}

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

    #[test]
    fn moveresize_directions_map_to_edges() {
        assert_eq!(move_resize_edges(0), 1 | 4); // lewy-górny
        assert_eq!(move_resize_edges(1), 1); // góra
        assert_eq!(move_resize_edges(3), 8); // prawo
        assert_eq!(move_resize_edges(4), 2 | 8); // prawy-dolny
        assert_eq!(move_resize_edges(5), 2); // dół
        assert_eq!(move_resize_edges(7), 4); // lewo
        assert_eq!(move_resize_edges(8), 0); // „move” nie jest zmianą rozmiaru
        assert_eq!(move_resize_edges(99), 0);
    }

    #[test]
    fn normal_windows_are_decorated_but_menus_are_not() {
        let normal = X11Info {
            kind: X11Kind::Normal,
            ..Default::default()
        };
        assert!(normal.decorated());
        let menu = X11Info {
            kind: X11Kind::Transient,
            ..Default::default()
        };
        assert!(!menu.decorated());
        // Override-redirect (np. podpowiedź bez typu) też bez dekoracji.
        let over = X11Info {
            override_redirect: true,
            ..Default::default()
        };
        assert!(!over.decorated());
        // Klient z własnymi dekoracjami (CSD) nie dostaje drugiego paska.
        let csd = X11Info {
            client_decorations: true,
            ..Default::default()
        };
        assert!(!csd.decorated());
    }
}