PaganDE
Środowisko graficzne Wayland budowane od zera w Rust:
pagan-compositor— kompozytor (serwer) na Smithay 0.7: 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).
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).
Aplikacje bez tego protokołu dostają w pasku przycisk „burger” (zamiast etykiet).
Wymagania
- Rust. Wystarczy
rustcz PaganOS — zawieracargo(sprawdzone:cargo 1.96.0).cairo-rs 0.22wymaga ś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)
# 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:
cd pagan-compositor
cargo run -- --winit
# wypisze m.in.:
# WAYLAND_DISPLAY=wayland-1
Terminal 2 — klient demo (skopiuj socket z terminala 1):
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).
┌─────────┬─────────┐ 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_dataokna). - 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:
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.
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:
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 znaxdg-decorationna Waylandzie, więc tylko na X11 dostaje nasz pasek SSD; nadpisz przezPAGAN_GDK_BACKEND=wayland, jeśli wolisz natywny Wayland, - dla Qt wymusza
QT_QPA_PLATFORM=wayland(Qt znaxdg-decorationi sam rezygnuje z CSD, więc dostaje nasz pasek bez XWayland), - podaje aplikacjom
DISPLAYnaszego 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):
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 binarkiXwaylandkompozytor 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_TRANSFORMoraz per monitor w~/.config/pagan/outputs.conf), ręczny układ przezPAGAN_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.fragrysuje 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 zSpace. - 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).
grimbez-cnie rysuje kursora — do weryfikacji kursora używajgrim -c.
Dwie pułapki API, które kosztowały najwięcej czasu (na przyszłość)
- Kolejność elementów renderowania: Smithay oczekuje listy od przodu do tyłu
(indeks 0 = na wierzchu;
render_outputrysuje 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_outputw Smithayu robi dokładnie tak: najpierw elementy niestandardowe, potem zawartość przestrzeni. PixelShaderElement:varying v_coordsjest w UV [0..1], nie w pikselach — trzeba pomnożyć przezuniform vec2 size. Dodatkowo GLSL ES wymaga jawnegoprecision mediump float;(Smithay dokleja tylko#version 100).