# PaganDE Środowisko graficzne Wayland budowane od zera w Rust: - **`pagan-compositor`** — kompozytor (serwer) na [Smithay 0.7](https://github.com/Smithay/smithay): floating window manager, renderer OpenGL (`glow`), **wymuszone dekoracje serwerowe (SSD)** z menu aplikacji w pasku, płaski firmowy styl, doklejanie okien (Aero Snap). - **`pagan-toolkit`** — własna biblioteka UI dla klientów: `wayland-client` + `memmap2` (SHM) + `cairo-rs`. Bez Qt/GTK/SCTK/iced. Antyaliasing, zaokrąglone rogi, wspólny pasek tytułu, lekka lokalizacja PL/EN. - **`smithay-0.7.0`** — vendored źródła Smithay (build offline, powtarzalny). ## Filozofia: spójne dekoracje serwerowe (SSD) Kompozytor rysuje pasek tytułu dla okien, które oddają mu dekoracje, i wymusza tryb serwerowy (`xdg-decoration` → `ServerSide`). Dotyczy to naszego toolkitu, Qt, GTK4 oraz WSZYSTKICH aplikacji X11 (X11 nie zna CSD). Wyjątkiem są klienty, które tego protokołu nie znają (GTK3) — one rysują własne dekoracje, więc nie dokładamy drugiego paska. Szczegóły i obejście dla GTK: [Dekoracje serwerowe (SSD)](#dekoracje-serwerowe-ssd-i-menu-aplikacji-w-pasku). Menu aplikacji jest jedynym elementem, którego kompozytor nie zna z góry — aplikacje mają różne menu. Rozwiązuje to **własny protokół** `pagan_app_menu_v1`: klient publikuje etykiety górnego poziomu, kompozytor rysuje je w pasku i odsyła kliknięcia. Szczegóły: [Dekoracje serwerowe (SSD)](#dekoracje-serwerowe-ssd-i-menu-aplikacji-w-pasku). Aplikacje bez tego protokołu dostają w pasku przycisk „burger” (zamiast etykiet). ## Wymagania - Rust. Wystarczy `rustc` z PaganOS — zawiera `cargo` (sprawdzone: `cargo 1.96.0`). `cairo-rs 0.22` wymaga świeżego stabilnego toolchainu (edition 2024), więc ≥ 1.85. - Biblioteki systemowe: EGL/GL, Wayland, xkbcommon, Cairo, glib (dla `cairo-rs`). ### PaganOS (`repo.paganlinux.eu`) ```bash # toolchain + biblioteki do OBU projektów (backend winit) i do toolkitu sudo pag install rustc pkgconf wayland wayland-protocols libxkbcommon cairo \ glib2 pixman mesa libglvnd egl-wayland fontconfig dejavu-ttf # backend PRODUKCYJNY (tty-udev) + testy narzędziami Waylanda sudo pag install xorg-libdrm libinput seatd systemd libdisplay-info \ xorg-libxcursor grim slurp # aplikacje X11 (opcjonalnie) — bez tego działają tylko klienty Wayland sudo pag install xorg-xwayland ``` Uwaga: `systemd` dostarcza `libudev`, a `seatd` — `libseat`; `mesa` — `libgbm`. Jeśli któraś nazwa nie istnieje w repo, sprawdź ją przez wyszukiwanie w `pag`. `grim`/`slurp` **nie instalują** cargo — to niezależne klienty Wayland (cargo jest w paczce `rustc`). ## Uruchomienie (development, backend winit) Terminal 1 — kompozytor: ```bash cd pagan-compositor cargo run -- --winit # wypisze m.in.: # WAYLAND_DISPLAY=wayland-1 ``` Terminal 2 — klient demo (skopiuj socket z terminala 1): ```bash cd pagan-toolkit WAYLAND_DISPLAY=wayland-1 PAGAN_LANG=pl cargo run --example demo ``` Powinno pojawić się okno 800×500 z ciemnym paskiem tytułu: „hamburger” (Menu) po lewej i trzy okrągłe przyciski (minimalizuj / maksymalizuj / zamknij) po prawej — **wszystkie w jednej linii** (`TITLEBAR_CONTROL_CENTER_Y = 20.0`), z zaokrąglonymi rogami. ## Doklejanie i kafelkowanie (Aero Snap) Kompozytor obsługuje „doklejanie” okien do krawędzi i rogów ekranu — **po stronie serwera**, więc działa dla każdej aplikacji (nie tylko naszego toolkitu). ```text ┌─────────┬─────────┐ krawędź lewa/prawa → połowa ekranu │ TL │ TR │ krawędź górna → pełny ekran ├─────────┼─────────┤ róg (2 krawędzie) → ćwiartka (siatka 2×2) │ BL │ BR │ krawędź dolna → bez doklejania └─────────┴─────────┘ ``` - Podczas przeciągania okna (za pasek tytułu) kursor wjeżdżający w strefę (domyślnie 12 px od krawędzi) natychmiast ustawia okno w docelowej geometrii; wyjazd ze strefy przywraca rozmiar sprzed doklejenia. - Dwa okna doklejone do przeciwległych połówek (lub rogów) same układają się w kafelki — to całe „stackowanie”, nie potrzeba osobnego menedżera kafelkowego. - Kolejne przeciągnięcie okna doklejonego **wyskakuje** do rozmiaru sprzed snapa (rozmiar „swobodny” pamiętamy w `user_data` okna). - Resize myszką: złapanie okna w odległości 8 px od krawędzi/rogu uruchamia `xdg_toplevel.resize` (toolkit) → grab resize (kompozytor). Konfiguracja przez zmienne środowiskowe (czytane raz przy starcie): | Zmienna | Domyślnie | Znaczenie | |---|---|---| | `PAGAN_SNAP_THRESHOLD` | `12` | Odległość od krawędzi włączająca doklejanie (px) | | `PAGAN_SNAP_BOTTOM` | `0` | `1` włącza doklejanie do dolnej krawędzi (dolna połowa) | Logika geometrii to czysta funkcja w `pagan-compositor/src/snap.rs` z testami: `cargo test --bins`. ## Motyw kompozytora (kolory) — natychmiastowe przeładowanie Kompozytor sam rysuje tylko tło pulpitu, pasek/menu SSD, „ducha” doklejania i awaryjny kursor — dlatego motyw steruje nimi. Konfiguracja: `~/.config/pagan/theme.conf` (lub `PAGAN_THEME`), tworzona z komentowanym szablonem przy pierwszym starcie: ```conf background = #0e0f14 # tło pulpitu cursor_fill = #f2f2f7 # awaryjny kursor (gdy brak motywu XCursor) cursor_border = #14141a # Dekoracje serwerowe (SSD) — płaski, firmowy pasek tytułu ssd_bar = #1c202b # pasek okna AKTYWNEGO ssd_unfocused = #14161c # pasek okna NIEAKTYWNEGO ssd_border = #0d0e13 # 1 px linia pod paskiem ssd_text = #d6dae4 # tytuł + etykiety menu ssd_text_unfocused = #868b96 ssd_icon_close = #e06c6c # glif „zamknij” ssd_radius = 12 ssd_font_px = 13 ssd_menu_min_width = 560 # węższe okno zwija menu do burgera ``` **Zmiany działają od razu** — kompozytor obserwuje plik przez `inotify` i wczytuje go od nowa po każdym zapisie (bez restartu). `Ctrl+Alt+R` wymusza przeładowanie ręcznie. Kolory to `#RRGGBB` / `#RRGGBBAA`. Implementacja: `src/theme.rs` (7 testów jednostkowych). ### Dekoracje serwerowe (SSD) i menu aplikacji w pasku Kompozytor **wymusza** tryb dekoracji serwerowych (`xdg-decoration` → `ServerSide`) dla okien najwyższego poziomu, które ten protokół znają (nasz toolkit, Qt, GTK4). Pasek jest płaski (bez gradientów), z zaokrąglonymi górnymi rogami. Dwa świadome wyjątki: * **Klienty CSD (np. GTK3).** GTK3 na Waylandzie ZAWSZE rysuje własne dekoracje i nie tworzy obiektu `zxdg_toplevel_decoration_v1`. Nie da się na nim wymusić naszego paska, więc kompozytor NIE dokłada drugiego — inaczej oba paski nakładałyby się na siebie (wyglądało to jak „przesunięta dekoracja”). Aby GTK dostało NASZ pasek, launcher uruchamia je przez XWayland (patrz niżej). * **Okna `override-redirect`** (menu, podpowiedzi X11) to elementy aplikacji, a nie samodzielne okna — pasek nad nimi wyglądałby jak przyklejony do menu. Wysokości panelu nie zgadujemy: kompozytor czyta **obszar roboczy** wyjścia (`LayerMap::non_exclusive_zone`), więc kaskada nowych okien i maksymalizacja lądują POD panelem, a nie za nim. W pasku, w **jednym rzędzie**, leżą: menu aplikacji (po lewej), tytuł okna (wyśrodkowany, o ile się mieści) oraz przyciski minimalizuj/maksymalizuj/zamknij (po prawej). Okno nieaktywne jest przygaszone i pozbawione tytułu z akcentem — dzięki temu od razu widać, gdzie trafi klawiatura. #### Własny protokół: `pagan_app_menu_v1` Każda aplikacja ma **inne** menu, więc kompozytor nie może znać jego zawartości. Rozdzielamy odpowiedzialność: * **klient** publikuje same TYTUŁY górnego poziomu (`set_labels`), * **kompozytor** rysuje je w firmowym pasku i odsyła zdarzenie `clicked(index, x)` (z pozycją etykiety, żeby klient wiedział, gdzie rozwinąć), * **klient** rysuje ROZWINIĘCIE we własnej powierzchni (`OpenMenu`) i wykonuje akcję wybranej pozycji (`WindowAction::MenuItem`). Pełny łańcuch działania (z logu demo): ``` otwarto menu z paska tytułu index=0 wybrano pozycję menu aplikacji menu_index=0 item_index=3 label="Zamknij" Zamykanie okna na życzenie użytkownika. ``` Opis protokołu: [`protocols/pagan-app-menu-v1.xml`](protocols/pagan-app-menu-v1.xml). Kod generuje `wayland-scanner` po obu stronach (te same etykiety na serwerze w `pagan-compositor/src/app_menu.rs` i u klienta w `pagan-toolkit/src/app_menu.rs`). Użycie w aplikacji: ```rust let mut menu = pagan_toolkit::AppMenu::new(); menu.add("Plik", &["Nowe okno", "Otwórz…", "Zapisz", "Zamknij"]) .add("Edycja", &["Wytnij", "Kopiuj", "Wklej"]); pagan_toolkit::publish_app_menu(&mut state, &menu)?; ``` Aplikacje bez tego protokołu dostają w pasku przycisk „burger”. Gdy okno jest za wąskie na etykiety (`ssd_menu_min_width`), etykiety zwijają się do burgera — a jego **rozwinięcie to menu aplikacji** (wiersze = etykiety, klik odsyłamy klientowi). Dopiero gdy klient nie ma menu (np. terminale X11), burger rozwija awaryjne menu okna (minimalizuj/maksymalizuj/zamknij). Tekst na pasku (tytuł i etykiety menu) rysuje kompozytor: rasteryzuje go Cairo i wysyła na GPU jako teksturę (`pagan-compositor/src/text.rs`, cache per napis). #### Ikony przycisków (wektorowo) Ikony minimalizuj/maksymalizuj/zamknij oraz „burger” są rysowane **wektorowo** Cairo (`pagan-compositor/src/icons.rs`) — prawdziwe ścieżki z zaokrąglonymi końcami (`LineCap::Round`), a nie aproksymacja prostokątami. Świadomie NIE ładujemy plików `.svg`: renderowanie SVG wymaga `librsvg`, które ciągnie stos GTK/GDK — wbrew założeniu „bez gotowych frameworków UI”. Kształty są te same, co miałyby w SVG, a rozmiar i kolor steruje motyw (`ssd_icon_px`, `ssd_text`). #### Launcher do testów (panel) Panel (`pagan-panel`) ma dwa przyciski obok siebie: * **Pagan** — uruchamia terminal (jak dotychczas), * **Demo** — uruchamia naszą aplikację testową (`pagan-toolkit/examples/demo`), żeby jednym kliknięciem obejrzeć dekoracje SSD i menu aplikacji. Ścieżkę do demo można nadpisać zmienną `PAGAN_DEMO_CMD`. Bez niej panel szuka `pagan-demo` w `PATH`, a potem w układzie deweloperskim repozytorium. **Uruchamianie w NASZEJ sesji (ważne).** Panel nie startuje programów „jak leci”: w sesji XFCE ustawione jest `XDG_SESSION_TYPE=x11` i `DISPLAY=:0.0`, więc GUI domyślnie wybiera X11 i ląduje na pulpicie XFCE — poza naszym kompozytorem (a wtedy okno i jego dekoracja nie mają ze sobą nic wspólnego). Dlatego launcher: * ustawia `XDG_SESSION_TYPE=wayland`, * dla **GTK** wymusza `GDK_BACKEND=x11` (czyli NASZ XWayland). Powód: GTK3 nie zna `xdg-decoration` na Waylandzie, więc tylko na X11 dostaje nasz pasek SSD; nadpisz przez `PAGAN_GDK_BACKEND=wayland`, jeśli wolisz natywny Wayland, * dla **Qt** wymusza `QT_QPA_PLATFORM=wayland` (Qt zna `xdg-decoration` i sam rezygnuje z CSD, więc dostaje nasz pasek bez XWayland), * podaje aplikacjom `DISPLAY` **naszego** XWayland, który kompozytor publikuje w `$XDG_RUNTIME_DIR/pagan/xwayland-display`. Dzięki temu i aplikacje Wayland, i X11 trafiają do naszego kompozytora i dostają spójną dekorację SSD. ### Testy interakcji bez machania myszką (dev) Na backendzie `winit` kompozytor jest zwykłym oknem X11, więc zdarzenia myszy można wstrzykiwać przez XTest (wymaga `libXtst`): ```bash DISPLAY=:1 python3 tools/x11_mouse.py drag X1 Y1 X2 Y2 # przeciągnij DISPLAY=:1 python3 tools/x11_mouse.py click X Y # kliknij DISPLAY=:1 python3 tools/x11_probe.py list # okna X + geometria DISPLAY=:1 python3 tools/x11_probe.py find 1280 800 # origin okna klienta ``` ## Co jest zrobione (Zadania 1–3) | Zadanie | Status | Gdzie | |---|---|---| | 1. Szkielet kompozytora (Smithay, winit, floating, move/resize, Z-order, xdg-decoration=server) | ✅ winit + ✅ udev | `pagan-compositor/src/` | | 1b. Doklejanie do krawędzi/rogów (Aero Snap) + „wyskakiwanie” ze snapa | ✅ (`snap.rs`, 10 testów) | `pagan-compositor/src/snap.rs`, `grabs.rs` | | 2. Szkielet toolkitu (Wayland, SHM/memmap2, Cairo, CSD, hit-testing) | ✅ | `pagan-toolkit/src/` | | 3. Shadery GLSL (zaokrąglone prostokąty: pasek, menu, duch snapu) | ✅ | `pagan-compositor/src/shaders/`, `window.rs` | | 4. Wymuszone dekoracje serwerowe (SSD) + menu aplikacji w pasku | ✅ | `pagan-compositor/src/window.rs`, `text.rs`, `app_menu.rs` | | 5. Własny protokół `pagan_app_menu_v1` (menu per aplikacja) | ✅ | `protocols/`, `pagan-compositor/src/app_menu.rs`, `pagan-toolkit/src/app_menu.rs` | | 6. Wektorowe ikony paska + launcher aplikacji testowej w panelu | ✅ | `pagan-compositor/src/icons.rs`, `pagan-panel/src/main.rs` | ## Ograniczenia i następne kroki - **Protokoły**: schowek/DnD, primary-selection, xdg-activation, fractional-scale, pointer-constraints, text-input, wlr-layer-shell, linux-dmabuf, viewporter, relative-pointer, pointer-gestures, **wlr-screencopy napisany od zera** (`src/screencopy.rs`, bufory SHM — test: `grim`) oraz **XWayland** (`src/xwayland.rs`; bez binarki `Xwayland` kompozytor działa dalej, tylko loguje WARN). - **Monitory**: liczba, rozmiary, układ poziomy bez przerw, hotplug, ratowanie okien, nowe okna na monitorze pod kursorem, skala/transformacja (globalnie przez `PAGAN_SCALE`/`PAGAN_TRANSFORM` oraz per monitor w `~/.config/pagan/outputs.conf`), ręczny układ przez `PAGAN_OUTPUTS`. - **Skróty**: `Ctrl+Alt+Backspace` (wyjście), `Ctrl+Alt+←/→` (fokus), `Ctrl+Alt+Delete` (zamknij), `Ctrl+Alt+M` (minimalizuj), `Ctrl+Alt+U` (przywróć), `Ctrl+Alt+R` (przeładuj motyw). - **Backend tty-udev**: kompiluje się (`--features udev`, `--tty-udev`), ale **nie był uruchamiany na sprzęcie** (komentarze `// WERYFIKUJ:`). - **Bez cienia pod oknami** (świadomie): okna są płaskie, oddziela je od tła tylko zaokrąglony pasek i 1 px linia. `rounded_corners.frag` rysuje zaokrąglone prostokąty (pasek SSD, tło menu, „duch” doklejania). - **Kursor** z motywu XCursor (`XCURSOR_THEME`/`XCURSOR_SIZE`), z zapasowym prostokątem. **Ikona DnD** rysowana pod kursorem. Minimalizacja chowa okno z `Space`. - **Brak animacji** (świadomie — najpierw działający system). ## Status weryfikacji (uruchomione i sprawdzone) Całość **skompilowana i uruchomiona** na prawdziwym sprzęcie (NVIDIA + Mesa, EGL 1.5): | Co | Komenda | Wynik | |---|---|---| | Build kompozytora (winit) | `cargo build` | ✅ 0 błędów | | Build kompozytora (udev) | `cargo build --features udev` | ✅ 0 błędów | | Testy jednostkowe | `cargo test --bins` | ✅ 43/43 (snap + monitory + motyw + SSD) | | Build toolkitu | `cargo build --examples` | ✅ 0 błędów | | Kompozytor | `./target/debug/pagan-compositor --winit` | ✅ socket `wayland-1`, output 1280×800 | | Klient + CSD | `WAYLAND_DISPLAY=wayland-1 demo` | ✅ okno, klik/hover/maximize/move | | Bez cienia | analiza pikseli klatki (ring wokół okna) | ✅ brak halo — treść styka się z pulpitem | | Zrzut ekranu | `WAYLAND_DISPLAY=wayland-1 grim shot.png` | ✅ (nasz `wlr-screencopy` od zera) | | Kursor z motywu | `grim -c shot.png` | ✅ widoczny kursor XCursor | | Motyw — tło | zmiana `background` w `theme.conf` | ✅ nowy kolor w ~1 s (inotify) | | Motyw — pasek | zmiana `ssd_bar` w `theme.conf` | ✅ nowy kolor w ~1 s (inotify) | | XWayland | log startowy bez binarki `Xwayland` | ⚠️ miękka degradacja (WARN) — brak binarki w środowisku testowym | Zmierzona geometria z `grim`: menu i trzy przyciski mają środek na **tej samej osi Y**, odstęp dokładnie 32 px, rogi przezroczyste (promień 12 px). > `grim` bez `-c` **nie rysuje kursora** — do weryfikacji kursora używaj `grim -c`. ### Dwie pułapki API, które kosztowały najwięcej czasu (na przyszłość) 1. **Kolejność elementów renderowania**: Smithay oczekuje listy **od przodu do tyłu** (indeks 0 = na wierzchu; `render_output` rysuje od końca). Belka paska musi być **na końcu** listy (pod tekstem i glifami), a **kursor i ikona DnD na samym początku** — inaczej kursor chowa się POD oknem. `space::render_output` w Smithayu robi dokładnie tak: najpierw elementy niestandardowe, potem zawartość przestrzeni. 2. **`PixelShaderElement`**: `varying v_coords` jest w **UV [0..1]**, nie w pikselach — trzeba pomnożyć przez `uniform vec2 size`. Dodatkowo GLSL ES wymaga jawnego `precision mediump float;` (Smithay dokleja tylko `#version 100`).