🔒 Repository is read-only – file editing is disabled.
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(())
}