//! 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 for ToolkitError { fn from(value: ConnectError) -> Self { ToolkitError::Connect(value) } } impl From for ToolkitError { fn from(value: GlobalError) -> Self { ToolkitError::Global(value) } } impl From for ToolkitError { fn from(value: BindError) -> Self { ToolkitError::Bind(value) } } impl From for ToolkitError { fn from(value: ShmError) -> Self { ToolkitError::Shm(value) } } impl From 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_shm`. pub shm: Option, /// `xdg_wm_base`. pub xdg_wm_base: Option, /// `zxdg_decoration_manager_v1` (opcjonalny — nie każdy kompozytor go ma). pub decoration_manager: Option, /// `wl_seat`. pub seat: Option, /// `wl_pointer` utworzony po zdarzeniu `wl_seat.capabilities`. pub pointer: Option, // --- role powierzchni --- /// `wl_surface`. pub surface: Option, /// `xdg_surface`. pub xdg_surface: Option, /// `xdg_toplevel`. pub toplevel: Option, /// `zxdg_toplevel_decoration_v1`. pub decoration: Option, // --- menu aplikacji (własny protokół PaganDE) --- /// `pagan_app_menu_manager_v1` (opcjonalny — tylko kompozytory PaganDE). pub app_menu_manager: Option, /// `pagan_app_menu_v1` dla naszego okna. pub app_menu: Option, /// 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, // --- 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, /// 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, /// Tryb dekoracji wynegocjowany z kompozytorem. pub decoration_mode: Option, /// 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, ) { // 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 for CompositorState { fn event( _state: &mut Self, _proxy: &wl_registry::WlRegistry, _event: wl_registry::Event, _data: &GlobalListContents, _conn: &Connection, _qh: &QueueHandle, ) { // `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 for CompositorState { fn event( _state: &mut Self, _proxy: &wl_shm::WlShm, event: wl_shm::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for CompositorState { fn event( _state: &mut Self, proxy: &XdgWmBase, event: xdg_wm_base::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { proxy.pong(serial); } } } // `xdg_surface.configure` zamyka sekwencję konfiguracji i wymaga `ack_configure`. impl Dispatch for CompositorState { fn event( state: &mut Self, proxy: &XdgSurface, event: xdg_surface::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for CompositorState { fn event( state: &mut Self, _proxy: &XdgToplevel, event: xdg_toplevel::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for CompositorState { fn event( state: &mut Self, _proxy: &ZxdgToplevelDecorationV1, event: zxdg_toplevel_decoration_v1::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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, locale: Locale, ) -> Result<(EventQueue, CompositorState), ToolkitError> { let connection = Connection::connect_to_env()?; let (globals, mut event_queue) = registry_queue_init::(&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 = 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 = 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, ) -> 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, ) -> 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(()) }