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

PaganDE/pagan-toolkit/src/connection.rs main

532 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
//! Warstwa `wayland-client`: połączenie, registry, role xdg-shell, dekoracje i pętla zdarzeń.
//!
//! Moduł świadomie NIE korzysta z helperów SCTK (`XdgShell`, `XdgDecorationManagerV1`)
//! — w `wayland-protocols` ich nie ma, więc trzymamy stan samodzielnie w [`CompositorState`].
//!
//! Podział odpowiedzialności:
//! * tutaj: `wl_registry`, `wl_compositor`, `wl_shm`, `wl_surface`, `xdg_*`, dekoracje,
//! * w [`crate::input`]: `wl_seat` i `wl_pointer`.

use std::fmt;

use wayland_client::globals::{registry_queue_init, BindError, GlobalError, GlobalListContents};
use wayland_client::protocol::{
    wl_buffer, wl_compositor, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool,
    wl_surface::WlSurface,
};
use wayland_client::{ConnectError, Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum};
use wayland_protocols::xdg::decoration::zv1::client::{
    zxdg_decoration_manager_v1::ZxdgDecorationManagerV1,
    zxdg_toplevel_decoration_v1::{self, Mode, ZxdgToplevelDecorationV1},
};
use wayland_protocols::xdg::shell::client::{
    xdg_surface::{self, XdgSurface},
    xdg_toplevel::{self, XdgToplevel},
    xdg_wm_base::{self, XdgWmBase},
};

use crate::app_menu::{encode_labels, AppMenu, PaganAppMenuManagerV1, PaganAppMenuV1};
use crate::i18n::Locale;
use crate::shm::{ShmBuffer, ShmError};
use crate::window::{MyToolkitWindow, WindowAction};

/// Błędy warstwy połączenia i konfiguracji okna.
#[derive(Debug)]
pub enum ToolkitError {
    /// Nie udało się połączyć z kompozytorem (np. brak `WAYLAND_DISPLAY`).
    Connect(ConnectError),
    /// Błąd inicjalizacji listy globali.
    Global(GlobalError),
    /// Nie udało się zbindować wymaganego globala (brak lub zła wersja).
    Bind(BindError),
    /// Błąd alokacji pamięci współdzielonej.
    Shm(ShmError),
    /// Błąd Cairo podczas renderowania.
    Cairo(cairo::Error),
    /// Błąd protokołu Wayland (opakowany w tekst, by nie zależeć od konkretnego typu backendu).
    Wayland(String),
    /// Brak globala wymaganego do dalszej pracy.
    MissingGlobal(&'static str),
    /// Kompozytor nie dostarczył poprawnej konfiguracji powierzchni.
    Configuration(&'static str),
}

impl fmt::Display for ToolkitError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ToolkitError::Connect(err) => write!(f, "nie udało się połączyć z Waylandem: {err}"),
            ToolkitError::Global(err) => write!(f, "błąd inicjalizacji globali: {err}"),
            ToolkitError::Bind(err) => write!(f, "nie udało się zbindować globala: {err}"),
            ToolkitError::Shm(err) => write!(f, "błąd pamięci współdzielonej: {err}"),
            ToolkitError::Cairo(err) => write!(f, "błąd Cairo: {err}"),
            ToolkitError::Wayland(msg) => write!(f, "błąd protokołu Wayland: {msg}"),
            ToolkitError::MissingGlobal(name) => write!(f, "brak wymaganego globala: {name}"),
            ToolkitError::Configuration(msg) => write!(f, "błąd konfiguracji: {msg}"),
        }
    }
}

impl std::error::Error for ToolkitError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ToolkitError::Connect(err) => Some(err),
            ToolkitError::Global(err) => Some(err),
            ToolkitError::Bind(err) => Some(err),
            ToolkitError::Shm(err) => Some(err),
            ToolkitError::Cairo(err) => Some(err),
            _ => None,
        }
    }
}

impl From<ConnectError> for ToolkitError {
    fn from(value: ConnectError) -> Self {
        ToolkitError::Connect(value)
    }
}

impl From<GlobalError> for ToolkitError {
    fn from(value: GlobalError) -> Self {
        ToolkitError::Global(value)
    }
}

impl From<BindError> for ToolkitError {
    fn from(value: BindError) -> Self {
        ToolkitError::Bind(value)
    }
}

impl From<ShmError> for ToolkitError {
    fn from(value: ShmError) -> Self {
        ToolkitError::Shm(value)
    }
}

impl From<cairo::Error> for ToolkitError {
    fn from(value: cairo::Error) -> Self {
        ToolkitError::Cairo(value)
    }
}

/// Cały stan aplikacji — to on implementuje `Dispatch` dla wszystkich użytych interfejsów.
///
/// Trzyma obiekty protokołu razem z modelem okna i buforem SHM. NIE trzyma
/// `EventQueue` — kolejka jest osobnym bytem w funkcji sterującej u użytkownika,
/// co pozwala wołać `event_queue.blocking_dispatch(&mut state)` bez konfliktu pożyczeń.
pub struct CompositorState {
    /// Połączenie (klon tani w utrzymaniu; przydatne m.in. do wymuszonego flushu).
    pub connection: Connection,
    /// Model i logika okna.
    pub window: MyToolkitWindow,
    /// Język interfejsu (do logów i etykiet).
    pub locale: Locale,

    // --- globala z registry ---
    /// `wl_compositor`.
    pub compositor: Option<wl_compositor::WlCompositor>,
    /// `wl_shm`.
    pub shm: Option<wl_shm::WlShm>,
    /// `xdg_wm_base`.
    pub xdg_wm_base: Option<XdgWmBase>,
    /// `zxdg_decoration_manager_v1` (opcjonalny — nie każdy kompozytor go ma).
    pub decoration_manager: Option<ZxdgDecorationManagerV1>,
    /// `wl_seat`.
    pub seat: Option<wl_seat::WlSeat>,
    /// `wl_pointer` utworzony po zdarzeniu `wl_seat.capabilities`.
    pub pointer: Option<wl_pointer::WlPointer>,

    // --- role powierzchni ---
    /// `wl_surface`.
    pub surface: Option<WlSurface>,
    /// `xdg_surface`.
    pub xdg_surface: Option<XdgSurface>,
    /// `xdg_toplevel`.
    pub toplevel: Option<XdgToplevel>,
    /// `zxdg_toplevel_decoration_v1`.
    pub decoration: Option<ZxdgToplevelDecorationV1>,

    // --- menu aplikacji (własny protokół PaganDE) ---
    /// `pagan_app_menu_manager_v1` (opcjonalny — tylko kompozytory PaganDE).
    pub app_menu_manager: Option<PaganAppMenuManagerV1>,
    /// `pagan_app_menu_v1` dla naszego okna.
    pub app_menu: Option<PaganAppMenuV1>,
    /// Lokalna definicja menu (tytuły + pozycje) — z niej bierzemy rozwinięcie.
    pub app_menu_def: AppMenu,
    /// Indeks tytułu i pozycja X etykiety klikniętej w pasku (do odebrania w pętli).
    pub pending_menu_click: Option<(usize, i32)>,

    // --- bufor ---
    /// Aktualnie używany bufor SHM (tworzony leniwie, dopasowany do rozmiaru okna).
    pub shm_buffer: Option<ShmBuffer>,

    // --- stan logiczny ---
    /// Czy dostaliśmy już pierwszy `xdg_surface.configure`.
    pub configured: bool,
    /// Flaga pętli głównej; `false` kończy aplikację.
    pub running: bool,
    /// Czy trzeba przerysować i wysłać nową ramkę.
    pub needs_redraw: bool,
    /// Akcja wyprodukowana przez wejście, do odebrania przez aplikację.
    pub pending_action: Option<WindowAction>,
    /// Rozmiar zasugerowany przez `xdg_toplevel.configure` (zastosujemy w `xdg_surface.configure`).
    pub pending_toplevel_size: Option<(i32, i32)>,
    /// Ostatnia znana pozycja kursora w współrzędnych powierzchni.
    pub last_pointer: (f64, f64),
    /// Serial ostatniego zdarzenia `button-press` — wymagany przez `move`/`resize`.
    pub pointer_serial: Option<u32>,
    /// Tryb dekoracji wynegocjowany z kompozytorem.
    pub decoration_mode: Option<Mode>,
    /// Czy okno jest aktualnie zmaksymalizowane (stan po stronie klienta).
    pub maximized: bool,
    /// Czy menu jest otwarte (demo loguje ten stan).
    pub menu_open: bool,
}

/// Makro pomocnicze: implementuje `Dispatch` ignorujący zdarzenia danego interfejsu.
///
/// Uwaga: nie używamy `delegate_noop!` z `wayland-client`, bo ten opiera się na
/// dodatkowym typie delegata i generycznym `UserData`; jawne `impl` jest tu
/// prostsze i odporniejsze na zmiany API.
macro_rules! noop_dispatch {
    ($iface:ty) => {
        impl Dispatch<$iface, ()> for CompositorState {
            fn event(
                _state: &mut Self,
                _proxy: &$iface,
                _event: <$iface as Proxy>::Event,
                _data: &(),
                _conn: &Connection,
                _qh: &QueueHandle<Self>,
            ) {
                // Zdarzenia tego interfejsu nie wpływają ani na wygląd, ani na logikę okna.
            }
        }
    };
}

// `wl_registry` ma specjalne user-data (`GlobalListContents`) — dlatego własna implementacja.
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for CompositorState {
    fn event(
        _state: &mut Self,
        _proxy: &wl_registry::WlRegistry,
        _event: wl_registry::Event,
        _data: &GlobalListContents,
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        // `GlobalListContents` jest aktualizowany automatycznie przez `wayland-client`.
        // Dynamiczne zmiany globali (np. hotplug seat) świadomie ignorujemy w wersji 0.1.
    }
}

noop_dispatch!(wl_compositor::WlCompositor);
noop_dispatch!(WlSurface);
noop_dispatch!(wl_shm_pool::WlShmPool);
noop_dispatch!(wl_buffer::WlBuffer);
noop_dispatch!(ZxdgDecorationManagerV1);

// `wl_shm` ma zdarzenie `format`; logujemy je, bo to jedyny ciekawy sygnał wsparcia.
impl Dispatch<wl_shm::WlShm, ()> for CompositorState {
    fn event(
        _state: &mut Self,
        _proxy: &wl_shm::WlShm,
        event: wl_shm::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let wl_shm::Event::Format { format } = event {
            tracing::debug!(?format, "kompozytor oferuje format pamięci współdzielonej");
        }
    }
}

// `xdg_wm_base` wymaga ODPOWIEDZI na `ping` — inaczej kompozytor uzna nas za zawieszonych.
impl Dispatch<XdgWmBase, ()> for CompositorState {
    fn event(
        _state: &mut Self,
        proxy: &XdgWmBase,
        event: xdg_wm_base::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let xdg_wm_base::Event::Ping { serial } = event {
            proxy.pong(serial);
        }
    }
}

// `xdg_surface.configure` zamyka sekwencję konfiguracji i wymaga `ack_configure`.
impl Dispatch<XdgSurface, ()> for CompositorState {
    fn event(
        state: &mut Self,
        proxy: &XdgSurface,
        event: xdg_surface::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let xdg_surface::Event::Configure { serial } = event {
            // Ack MUSI trafić do kompozytora przed commitem nowej zawartości.
            proxy.ack_configure(serial);

            // Dopiero tutaj stosujemy rozmiar zasugerowany przez xdg_toplevel.configure.
            if let Some((width, height)) = state.pending_toplevel_size.take() {
                if width > 0 && height > 0 {
                    state.window.resize(width, height);
                }
            }

            state.configured = true;
            state.needs_redraw = true;
        }
    }
}

impl Dispatch<XdgToplevel, ()> for CompositorState {
    fn event(
        state: &mut Self,
        _proxy: &XdgToplevel,
        event: xdg_toplevel::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            xdg_toplevel::Event::Configure { width, height, .. } => {
                // Zero oznacza „klient decyduje" — zachowujemy wtedy dotychczasowy rozmiar.
                state.pending_toplevel_size = Some((width, height));
            }
            xdg_toplevel::Event::Close => {
                // Prośba użytkownika o zamknięcie okna.
                state.running = false;
            }
            // `ConfigureBounds` i `WmCapabilities` na razie nas nie ograniczają.
            _ => {}
        }
    }
}

impl Dispatch<ZxdgToplevelDecorationV1, ()> for CompositorState {
    fn event(
        state: &mut Self,
        _proxy: &ZxdgToplevelDecorationV1,
        event: zxdg_toplevel_decoration_v1::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event {
            if let WEnum::Value(mode) = mode {
                state.decoration_mode = Some(mode);
                // Kompozytor PaganDE wymusza tryb serwerowy. Wtedy klient musi
                // zrezygnować z własnego paska tytułu, inaczej powstałyby dwa paski.
                let ssd = mode == Mode::ServerSide;
                if state.window.ssd != ssd {
                    state.window.set_ssd(ssd);
                    state.needs_redraw = true;
                }
                tracing::info!(?mode, "wynegocjowano tryb dekoracji okna");
            }
        }
    }
}

/// Tworzy połączenie, binduje globala, buduje role xdg-shell i czeka na pierwszy configure.
///
/// Zwraca rozdzielone: `EventQueue` (do pętli) i `state` (do `Dispatch`). Ten podział
/// jest celowy — patrz komentarz przy [`CompositorState`].
///
/// Po powrocie okno NIE jest jeszcze widoczne: trzeba zawołać [`render_and_commit`],
/// żeby podłączyć pierwszy bufor i zmapować powierzchnię.
pub fn create_window(
    width: i32,
    height: i32,
    title: impl Into<String>,
    locale: Locale,
) -> Result<(EventQueue<CompositorState>, CompositorState), ToolkitError> {
    let connection = Connection::connect_to_env()?;
    let (globals, mut event_queue) = registry_queue_init::<CompositorState>(&connection)?;
    let qh = event_queue.handle();

    // Wersje bindowania dobieramy konserwatywnie: mieszczą się w wersjach interfejsów,
    // a jednocześnie dają wszystko, czego potrzebujemy (damage_buffer wymaga compositora v4).
    let compositor: wl_compositor::WlCompositor = globals.bind(&qh, 1..=4, ())?;
    let shm: wl_shm::WlShm = globals.bind(&qh, 1..=1, ())?;
    let xdg_wm_base: XdgWmBase = globals.bind(&qh, 1..=2, ())?;
    let seat: wl_seat::WlSeat = globals.bind(&qh, 1..=5, ())?;
    // Dekoracje są opcjonalne — brak globala nie jest błędem.
    let decoration_manager: Option<ZxdgDecorationManagerV1> = globals.bind(&qh, 1..=1, ()).ok();
    // Menu aplikacji (własny protokół PaganDE) też jest opcjonalne — na obcym
    // kompozytorze po prostu nie mamy menu w pasku tytułu.
    let app_menu_manager: Option<PaganAppMenuManagerV1> = globals.bind(&qh, 1..=1, ()).ok();

    // Kolejność jest wymuszona protokołem: surface → xdg_surface → rola (xdg_toplevel).
    let surface = compositor.create_surface(&qh, ());
    let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &qh, ());
    let toplevel = xdg_surface.get_toplevel(&qh, ());

    let title = title.into();
    toplevel.set_title(title.clone());
    toplevel.set_app_id("org.pagan.toolkit.demo".to_string());
    toplevel.set_min_size(120, 80);

    // Prosimy o dekoracje serwerowe (SSD) — w PaganDE pasek tytułu rysuje
    // kompozytor, żeby wszystkie okna wyglądały spójnie. Kompozytor i tak wymusza
    // ten tryb, ale jawne `set_mode` jest zgodne z protokołem i działa też tam,
    // gdzie wybór należy do klienta.
    let decoration = decoration_manager.as_ref().map(|manager| {
        let decoration = manager.get_toplevel_decoration(&toplevel, &qh, ());
        decoration.set_mode(Mode::ServerSide);
        decoration
    });
    if decoration.is_none() {
        tracing::info!("kompozytor nie oferuje zxdg_decoration_manager_v1 — pozostajemy przy CSD");
    }

    // Menu aplikacji: obiekt tworzymy od razu, ale etykiety opublikuje aplikacja
    // dopiero przez [`publish_app_menu`] (bo tylko ona wie, jakie ma menu).
    let app_menu = app_menu_manager
        .as_ref()
        .map(|manager| manager.get_app_menu(&surface, &qh, ()));
    if app_menu.is_none() {
        tracing::info!("kompozytor nie oferuje pagan_app_menu_manager_v1 — brak menu w pasku SSD");
    }

    let mut state = CompositorState {
        connection: connection.clone(),
        window: MyToolkitWindow::with_locale(width, height, title, locale),
        locale,
        compositor: Some(compositor),
        shm: Some(shm),
        xdg_wm_base: Some(xdg_wm_base),
        decoration_manager,
        seat: Some(seat),
        pointer: None,
        surface: Some(surface.clone()),
        xdg_surface: Some(xdg_surface),
        toplevel: Some(toplevel),
        decoration,
        app_menu_manager,
        app_menu,
        app_menu_def: AppMenu::new(),
        pending_menu_click: None,
        shm_buffer: None,
        configured: false,
        running: true,
        needs_redraw: false,
        pending_action: None,
        pending_toplevel_size: None,
        last_pointer: (0.0, 0.0),
        pointer_serial: None,
        decoration_mode: None,
        maximized: false,
        menu_open: false,
    };

    // Pierwszy commit BEZ bufora — dopiero po configure wolno podłączyć zawartość.
    surface.commit();

    // Czekamy, aż kompozytor prześle konfigurację (i ewentualnie zdarzenia seat/pointer).
    event_queue
        .roundtrip(&mut state)
        .map_err(|err| ToolkitError::Wayland(err.to_string()))?;

    if !state.configured {
        return Err(ToolkitError::Configuration(
            "kompozytor nie przysłał xdg_surface.configure",
        ));
    }

    // Tryb dekoracji mógł przyjść w trakcie roundtripu — zastosuj go, zanim
    // aplikacja narysuje pierwszą ramkę (inaczej błysnąłby zbędny pasek CSD).
    if state.decoration_mode == Some(Mode::ServerSide) {
        state.window.set_ssd(true);
    }

    Ok((event_queue, state))
}

/// Dopasowuje bufor SHM do bieżącego rozmiaru okna (tworzy go leniwie lub realokuje).
fn ensure_shm_buffer(
    state: &mut CompositorState,
    qh: &QueueHandle<CompositorState>,
) -> Result<(), ToolkitError> {
    let needs_allocation = match &state.shm_buffer {
        Some(buffer) => !buffer.matches(state.window.width, state.window.height),
        None => true,
    };

    if needs_allocation {
        let shm = state
            .shm
            .clone()
            .ok_or(ToolkitError::MissingGlobal("wl_shm"))?;
        let buffer = ShmBuffer::new(&shm, qh, state.window.width, state.window.height)?;
        tracing::debug!(
            width = state.window.width,
            height = state.window.height,
            "zaalokowano nowy bufor SHM"
        );
        state.shm_buffer = Some(buffer);
    }

    Ok(())
}

/// Renderuje okno do bufora SHM i wysyła nową ramkę (`attach` + `damage` + `commit`).
///
/// To jedyna funkcja, którą aplikacja musi wołać po zmianie stanu wizualnego.
pub fn render_and_commit(
    state: &mut CompositorState,
    qh: &QueueHandle<CompositorState>,
) -> Result<(), ToolkitError> {
    ensure_shm_buffer(state, qh)?;

    let surface = match state.surface.clone() {
        Some(surface) => surface,
        None => return Ok(()),
    };

    // Pożyczamy rozłączne pola tej samej struktury: `window` (niemutowalnie) i
    // `shm_buffer` (mutowalnie) — borrow checker to akceptuje.
    let window = &state.window;
    let shm_buffer = match state.shm_buffer.as_mut() {
        Some(buffer) => buffer,
        None => return Ok(()),
    };

    window.render_to_buffer(&mut shm_buffer.mmap);

    surface.attach(Some(&shm_buffer.buffer), 0, 0);
    surface.damage_buffer(0, 0, window.width, window.height);
    surface.set_buffer_scale(1);
    surface.commit();

    Ok(())
}

/// Publikuje menu aplikacji w pasku tytułu (własny protokół PaganDE).
///
/// PO CO przekazujemy CAŁE menu: kompozytorowi wysyłamy tylko tytuły górnego
/// poziomu, a pozycje zachowujemy lokalnie — to one tworzą rozwinięcie pokazywane
/// po kliknięciu etykiety.
///
/// Brak globala `pagan_app_menu_manager_v1` nie jest błędem — na obcym
/// kompozytorze po prostu nie ma gdzie pokazać menu.
pub fn publish_app_menu(state: &mut CompositorState, menu: &AppMenu) -> Result<(), ToolkitError> {
    let titles = menu.titles();
    if let Some(app_menu) = state.app_menu.as_ref() {
        let refs: Vec<&str> = titles.iter().map(String::as_str).collect();
        app_menu.set_labels(encode_labels(&refs));
        // Wypychamy od razu, żeby pasek zdążył się zaktualizować przed pierwszą klatką.
        state
            .connection
            .flush()
            .map_err(|err| ToolkitError::Wayland(err.to_string()))?;
    }
    state.app_menu_def = menu.clone();
    Ok(())
}