← PaganDE

Commit 4d9c960

19
plików
+1795
dodanych
-176
usuniętych
@@ -34,6 +34,9 @@ sudo pag install rustc pkgconf wayland wayland-protocols libxkbcommon cairo \
34 34 # backend PRODUKCYJNY (tty-udev) + testy narzędziami Waylanda
35 35 sudo pag install xorg-libdrm libinput seatd systemd libdisplay-info \
36 36 xorg-libxcursor grim slurp
37 +
38 +# aplikacje X11 (opcjonalnie) — bez tego działają tylko klienty Wayland
39 +sudo pag install xorg-xwayland
37 40 ```
38 41
39 42 Uwaga: `systemd` dostarcza `libudev`, a `seatd` — `libseat`; `mesa` — `libgbm`.
@@ -78,8 +81,8 @@ serwera**, więc działa dla każdej aplikacji (nie tylko naszego toolkitu).
78 81 ```
79 82
80 83 - Podczas przeciągania okna (za pasek tytułu) kursor wjeżdżający w strefę
81 - (12 px od krawędzi) natychmiast ustawia okno w docelowej geometrii; wyjazd ze
82 - strefy przywraca rozmiar sprzed doklejenia.
84 + (domyślnie 12 px od krawędzi) natychmiast ustawia okno w docelowej geometrii;
85 + wyjazd ze strefy przywraca rozmiar sprzed doklejenia.
83 86 - Dwa okna doklejone do przeciwległych połówek (lub rogów) same układają się
84 87 w kafelki — to całe „stackowanie”, nie potrzeba osobnego menedżera kafelkowego.
85 88 - Kolejne przeciągnięcie okna doklejonego **wyskakuje** do rozmiaru sprzed snapa
@@ -87,8 +90,15 @@ serwera**, więc działa dla każdej aplikacji (nie tylko naszego toolkitu).
87 90 - Resize myszką: złapanie okna w odległości 8 px od krawędzi/rogu uruchamia
88 91 `xdg_toplevel.resize` (toolkit) → grab resize (kompozytor).
89 92
93 +Konfiguracja przez zmienne środowiskowe (czytane raz przy starcie):
94 +
95 +| Zmienna | Domyślnie | Znaczenie |
96 +|---|---|---|
97 +| `PAGAN_SNAP_THRESHOLD` | `12` | Odległość od krawędzi włączająca doklejanie (px) |
98 +| `PAGAN_SNAP_BOTTOM` | `0` | `1` włącza doklejanie do dolnej krawędzi (dolna połowa) |
99 +
90 100 Logika geometrii to czysta funkcja w `pagan-compositor/src/snap.rs` z testami:
91 -`cargo test --bin pagan-compositor` (8 testów).
101 +`cargo test --bin pagan-compositor` (10 testów).
92 102
93 103 ### Testy interakcji bez machania myszką (dev)
94 104
@@ -98,6 +108,8 @@ można wstrzykiwać przez XTest (wymaga `libXtst`):
98 108 ```bash
99 109 DISPLAY=:1 python3 tools/x11_mouse.py drag X1 Y1 X2 Y2 # przeciągnij
100 110 DISPLAY=:1 python3 tools/x11_mouse.py click X Y # kliknij
111 +DISPLAY=:1 python3 tools/x11_probe.py list # okna X + geometria
112 +DISPLAY=:1 python3 tools/x11_probe.py find 1280 800 # origin okna klienta
101 113 ```
102 114
103 115 ## Co jest zrobione (Zadania 1–3)
@@ -105,7 +117,7 @@ DISPLAY=:1 python3 tools/x11_mouse.py click X Y # kliknij
105 117 | Zadanie | Status | Gdzie |
106 118 |---|---|---|
107 119 | 1. Szkielet kompozytora (Smithay, winit, floating, move/resize, Z-order, xdg-decoration=client) | ✅ winit + ✅ udev | `pagan-compositor/src/` |
108 -| 1b. Doklejanie do krawędzi/rogów (Aero Snap) + „wyskakiwanie” ze snapa | ✅ (`snap.rs`, 8 testów) | `pagan-compositor/src/snap.rs`, `grabs.rs` |
120 +| 1b. Doklejanie do krawędzi/rogów (Aero Snap) + „wyskakiwanie” ze snapa | ✅ (`snap.rs`, 10 testów) | `pagan-compositor/src/snap.rs`, `grabs.rs` |
109 121 | 2. Szkielet toolkitu (Wayland, SHM/memmap2, Cairo, CSD, hit-testing) | ✅ | `pagan-toolkit/src/` |
110 122 | 3. Shadery GLSL (zaokrąglenia + cień) | ✅ **cień na GPU** (pixel shader) | `pagan-compositor/src/shaders/`, `window.rs` |
111 123
@@ -113,16 +125,21 @@ DISPLAY=:1 python3 tools/x11_mouse.py click X Y # kliknij
113 125
114 126 - **Protokoły**: schowek/DnD, primary-selection, xdg-activation, fractional-scale,
115 127 pointer-constraints, text-input, wlr-layer-shell, linux-dmabuf, viewporter,
116 - relative-pointer, pointer-gestures oraz **wlr-screencopy napisany od zera**
117 - (`src/screencopy.rs`, bufory SHM — test: `grim`). Brakuje **XWayland**.
128 + relative-pointer, pointer-gestures, **wlr-screencopy napisany od zera**
129 + (`src/screencopy.rs`, bufory SHM — test: `grim`) oraz **XWayland**
130 + (`src/xwayland.rs`; bez binarki `Xwayland` kompozytor działa dalej, tylko loguje WARN).
118 131 - **Monitory**: liczba, rozmiary, układ poziomy bez przerw, hotplug, ratowanie okien,
119 - nowe okna na monitorze pod kursorem, skala/transformacja przez `PAGAN_SCALE`/
120 - `PAGAN_TRANSFORM`, ręczny układ przez `PAGAN_OUTPUTS`.
121 -- **Skróty**: `Ctrl+Alt+Backspace` (wyjście), `Ctrl+Alt+←/→` (fokus), `Ctrl+Alt+Delete` (zamknij).
132 + nowe okna na monitorze pod kursorem, skala/transformacja (globalnie przez
133 + `PAGAN_SCALE`/`PAGAN_TRANSFORM` oraz per monitor w `~/.config/pagan/outputs.conf`),
134 + ręczny układ przez `PAGAN_OUTPUTS`.
135 +- **Skróty**: `Ctrl+Alt+Backspace` (wyjście), `Ctrl+Alt+←/→` (fokus),
136 + `Ctrl+Alt+Delete` (zamknij), `Ctrl+Alt+M` (minimalizuj), `Ctrl+Alt+U` (przywróć).
122 137 - **Backend tty-udev**: kompiluje się (`--features udev`, `--tty-udev`), ale **nie był
123 138 uruchamiany na sprzęcie** (komentarze `// WERYFIKUJ:`).
124 139 - **Cień** działa na GPU własnym shaderem (`shadow.frag`). `rounded_corners.frag` jest
125 140 gotową maską rogów na przyszły tryb SSD; przy CSD jest zbędna (klient rysuje rogi).
141 +- **Kursor** z motywu XCursor (`XCURSOR_THEME`/`XCURSOR_SIZE`), z zapasowym prostokątem.
142 + **Ikona DnD** rysowana pod kursorem. Minimalizacja chowa okno z `Space`.
126 143 - **Brak animacji** (świadomie — najpierw działający system).
127 144
128 145 ## Status weryfikacji (uruchomione i sprawdzone)
@@ -133,20 +150,27 @@ Całość **skompilowana i uruchomiona** na prawdziwym sprzęcie (NVIDIA + Mesa,
133 150 |---|---|---|
134 151 | Build kompozytora (winit) | `cargo build` | ✅ 0 błędów |
135 152 | Build kompozytora (udev) | `cargo build --features udev` | ✅ 0 błędów |
153 +| Testy jednostkowe | `cargo test --bin pagan-compositor` | ✅ 15/15 (snap + konfiguracja monitorów) |
136 154 | Build toolkitu | `cargo build --examples` | ✅ 0 błędów |
137 155 | Kompozytor | `./target/debug/pagan-compositor --winit` | ✅ socket `wayland-1`, output 1280×800 |
138 156 | Klient + CSD | `WAYLAND_DISPLAY=wayland-1 demo` | ✅ okno, klik/hover/maximize/move |
139 -| Cień (shader) | podgląd klatki | ✅ miękki cień POD oknem |
157 +| Cień (shader) | analiza pikseli klatki | ✅ miękki cień POD oknem |
140 158 | Zrzut ekranu | `WAYLAND_DISPLAY=wayland-1 grim shot.png` | ✅ (nasz `wlr-screencopy` od zera) |
159 +| Kursor z motywu | `grim -c shot.png` | ✅ widoczny kursor XCursor |
160 +| XWayland | log startowy bez binarki `Xwayland` | ⚠️ miękka degradacja (WARN) — brak binarki w środowisku testowym |
141 161
142 162 Zmierzona geometria z `grim`: menu i trzy przyciski mają środek na **tej samej
143 163 osi Y**, odstęp dokładnie 32 px, rogi przezroczyste (promień 12 px).
144 164
165 +> `grim` bez `-c` **nie rysuje kursora** — do weryfikacji kursora używaj `grim -c`.
166 +
145 167 ### Dwie pułapki API, które kosztowały najwięcej czasu (na przyszłość)
146 168
147 169 1. **Kolejność elementów renderowania**: Smithay oczekuje listy **od przodu do tyłu**
148 170 (indeks 0 = na wierzchu; `render_output` rysuje od końca). Cień musi być **na końcu**
149 - listy, inaczej zasłania okno.
171 + listy (pod oknem), a **kursor i ikona DnD na samym początku** — inaczej kursor
172 + chowa się POD oknem. `space::render_output` w Smithayu robi dokładnie tak:
173 + najpierw elementy niestandardowe, potem zawartość przestrzeni.
150 174 2. **`PixelShaderElement`**: `varying v_coords` jest w **UV [0..1]**, nie w pikselach —
151 175 trzeba pomnożyć przez `uniform vec2 size`. Dodatkowo GLSL ES wymaga jawnego
152 176 `precision mediump float;` (Smithay dokleja tylko `#version 100`).
@@ -294,6 +294,12 @@ dependencies = [
294 294 "libc",
295 295 ]
296 296
297 +[[package]]
298 +name = "core_detect"
299 +version = "1.0.0"
300 +source = "registry+https://github.com/rust-lang/crates.io-index"
301 +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
302 +
297 303 [[package]]
298 304 name = "cpufeatures"
299 305 version = "0.2.17"
@@ -402,6 +408,21 @@ dependencies = [
402 408 "linux-raw-sys 0.9.4",
403 409 ]
404 410
411 +[[package]]
412 +name = "encoding_rs"
413 +version = "0.8.41"
414 +source = "registry+https://github.com/rust-lang/crates.io-index"
415 +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"
416 +dependencies = [
417 + "cfg-if",
418 + "core_detect",
419 + "multiversion",
420 + "multiversion_no_op",
421 + "rustversion",
422 + "scopeguard",
423 + "simdutf8",
424 +]
425 +
405 426 [[package]]
406 427 name = "equivalent"
407 428 version = "1.0.2"
@@ -823,6 +844,33 @@ dependencies = [
823 844 "libc",
824 845 ]
825 846
847 +[[package]]
848 +name = "multiversion"
849 +version = "0.9.0"
850 +source = "registry+https://github.com/rust-lang/crates.io-index"
851 +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"
852 +dependencies = [
853 + "multiversion-macros",
854 +]
855 +
856 +[[package]]
857 +name = "multiversion-macros"
858 +version = "0.9.0"
859 +source = "registry+https://github.com/rust-lang/crates.io-index"
860 +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"
861 +dependencies = [
862 + "proc-macro2",
863 + "quote",
864 + "rustversion",
865 + "syn 3.0.5",
866 +]
867 +
868 +[[package]]
869 +name = "multiversion_no_op"
870 +version = "1.0.0"
871 +source = "registry+https://github.com/rust-lang/crates.io-index"
872 +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
873 +
826 874 [[package]]
827 875 name = "ndk"
828 876 version = "0.9.0"
@@ -1123,6 +1171,7 @@ dependencies = [
1123 1171 "tracing",
1124 1172 "tracing-subscriber",
1125 1173 "wayland-protocols-wlr",
1174 + "xcursor",
1126 1175 ]
1127 1176
1128 1177 [[package]]
@@ -1385,6 +1434,12 @@ version = "1.0.1"
1385 1434 source = "registry+https://github.com/rust-lang/crates.io-index"
1386 1435 checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
1387 1436
1437 +[[package]]
1438 +name = "scopeguard"
1439 +version = "1.2.0"
1440 +source = "registry+https://github.com/rust-lang/crates.io-index"
1441 +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1442 +
1388 1443 [[package]]
1389 1444 name = "semver"
1390 1445 version = "1.0.28"
@@ -1499,6 +1554,7 @@ dependencies = [
1499 1554 "drm",
1500 1555 "drm-ffi",
1501 1556 "drm-fourcc",
1557 + "encoding_rs",
1502 1558 "errno",
1503 1559 "gbm",
1504 1560 "gl_generator",
@@ -1512,6 +1568,7 @@ dependencies = [
1512 1568 "profiling",
1513 1569 "rand",
1514 1570 "rustix 1.1.4",
1571 + "scopeguard",
1515 1572 "sha2",
1516 1573 "smallvec",
1517 1574 "tempfile",
@@ -1526,6 +1583,7 @@ dependencies = [
1526 1583 "wayland-protocols-wlr",
1527 1584 "wayland-server",
1528 1585 "winit",
1586 + "x11rb",
1529 1587 "xkbcommon",
1530 1588 ]
1531 1589
@@ -25,10 +25,14 @@ smithay = { path = "../smithay-0.7.0", default-features = false, features = [
25 25 "renderer_gl",
26 26 "renderer_glow",
27 27 "wayland_frontend",
28 + # XWayland: uruchamianie aplikacji X11 (wymaga binarki `Xwayland` w PATH).
29 + "xwayland",
28 30 ] }
29 31 anyhow = "1"
30 32 tracing = "0.1"
31 33 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
34 +# Ładowanie kursora z motywu XCursor (System: libXcursor).
35 +xcursor = "0.3"
32 36 # Definicje protokołu wlr-screencopy (serwer) — implementujemy jego logikę sami.
33 37 wayland-protocols-wlr = { version = "0.3", features = ["server"] }
34 38 # Wymagane tylko przez backend tty-udev (feature `udev`):
@@ -10,10 +10,11 @@ zbudowany na [Smithay 0.7](https://github.com/Smithay/smithay).
10 10 - Dekoracje: **client-side** (wymuszamy `zxdg_toplevel_decoration_v1` w trybie
11 11 `client`), kompozytor dokłada tylko cień.
12 12
13 -> ⚠️ **Status weryfikacji:** kod nie został skompilowany w środowisku, w którym
14 -> powstał (brak toolchainu Rust). Napisany jest ściśle według wzorców z
15 -> `smithay-0.7.0/anvil`, ale pierwszy `cargo build` może wymagać drobnych
16 -> korekt. Miejsca szczególnie warte uwagi opisano na końcu.
13 +> ✅ **Status weryfikacji:** kompiluje się (`cargo build`, `cargo build --features udev`)
14 +> i jest uruchamiany na backendzie `winit`. Realny test wizualny potwierdził: pasek
15 +> tytułu 40 px z menu i przyciskami na wspólnej osi Y, rogi 12 px, cień GPU,
16 +> kursor z motywu XCursor (`grim -c`). Backend `tty-udev` kompiluje się, ale nie
17 +> był uruchamiany na sprzęcie z DRM — patrz `// WERYFIKUJ:`.
17 18
18 19 ## Wymagania
19 20
@@ -60,12 +61,18 @@ Logi: `RUST_LOG=pagan_compositor=debug cargo run -- --winit`.
60 61 src/
61 62 ├── main.rs # wejście, logowanie, wybór backendu
62 63 ├── winit.rs # backend dev: okno + pętla główna + render
64 +├── udev.rs # backend produkcyjny: DRM/KMS + libinput + libseat
63 65 ├── state.rs # MyCompositor: stan + handlery protokołów, floating WM
66 +├── output.rs # monitory: układ, hotplug, konfiguracja, ratowanie okien
64 67 ├── window.rs # PaganWindow: okno w Space + cień (per-okno, Z-order-poprawny)
65 68 ├── focus.rs # KeyboardFocusTarget / PointerFocusTarget
66 69 ├── grabs.rs # move / resize (standardowe xdg_toplevel.move/resize)
67 -├── input.rs # mysz, klawiatura, Z-order, surface_under
68 -├── render.rs # elementy renderowania, kursor, OutputDamageTracker
70 +├── input.rs # mysz, klawiatura, Z-order, surface_under, skróty
71 +├── snap.rs # strefy doklejania okien (krawędzie/rogi) + testy
72 +├── cursor.rs # kursor z motywu XCursor (cache ikon, animacja klatek)
73 +├── xwayland.rs # XWayland: X11Wm, schowek X11, obsługa okien X11
74 +├── screencopy.rs# wlr-screencopy napisany od zera
75 +├── render.rs # elementy renderowania, kursor, DnD, OutputDamageTracker
69 76 └── shaders/ # GLSL: rounded_corners.frag, shadow.frag (Zadanie 3)
70 77 ```
71 78
@@ -77,9 +84,9 @@ Zaimplementowane globalne: `wl_compositor`/`wl_subcompositor`, `wl_shm`,
77 84 `primary_selection`, `xdg_activation`, `fractional_scale`, `pointer_constraints`,
78 85 `text_input_manager`, `wlr-layer-shell` (**panele/tła/nakładki**), `linux-dmabuf`
79 86 (**bufory GPU**), `wp_viewporter`, `wp_relative_pointer`, `wp_pointer_gestures`,
80 -**`wlr-screencopy`** (zrzuty ekranu — **napisany od zera**, patrz niżej).
81 -
82 -Nie ma: **XWayland** (klienci X11).
87 +**`wlr-screencopy`** (zrzuty ekranu — **napisany od zera**, patrz niżej),
88 +**XWayland** (`xwayland_shell` + `X11Wm`; gdy brak binarki `Xwayland`, kompozytor
89 +loguje ostrzeżenie i działa dalej).
83 90
84 91 ## Screencopy (zrzuty ekranu) — napisane od zera
85 92
@@ -114,13 +121,33 @@ innego niż o pozycji (0,0) wymaga jeszcze korekty przesunięcia — patrz `// A
114 121 | `PAGAN_SCALE=1.5` | skala (HiDPI) wszystkich wyjść |
115 122 | `PAGAN_TRANSFORM=90\|180\|270\|flipped\|normal` | obrót wyjścia |
116 123 | `PAGAN_OUTPUTS="DP-1:0,0;HDMI-A-1:1920,0"` | ręczny układ monitorów (pozycje logiczne) |
124 +| `PAGAN_CONFIG=/ścieżka/outputs.conf` | wskazanie pliku konfiguracji monitorów |
117 125 | `PAGAN_DRM_DEVICE=/dev/dri/card1` | wybór GPU dla backendu udev |
126 +| `PAGAN_SNAP_THRESHOLD=12` | próg doklejania okna do krawędzi (px) |
127 +| `PAGAN_SNAP_BOTTOM=1` | włącz doklejanie także do dolnej krawędzi |
118 128 | `PAGAN_LANG` | (toolkit) język UI |
119 129
120 -Domyślnie (bez `PAGAN_OUTPUTS`) wyjścia układane są poziomo od (0,0), bez przerw.
130 +Domyślnie (bez konfiguracji) wyjścia układane są poziomo od (0,0), bez przerw.
121 131 `PAGAN_SCALE`/`PAGAN_TRANSFORM` nadpisują ustawienia backendu tylko gdy podane —
122 132 winit wymaga np. `Flipped180` do poprawnego rysowania.
123 133
134 +### Plik konfiguracji monitorów
135 +
136 +Oprócz zmiennych środowiskowych układ można zapisać trwale w pliku
137 +`$XDG_CONFIG_HOME/pagan/outputs.conf` (domyślnie `~/.config/pagan/outputs.conf`).
138 +Jeden monitor = jeden wiersz `NAZWA X Y [SKALA] [OBROT]`:
139 +
140 +```conf
141 +# nazwa x y skala obrot
142 +DP-1 0 0 1.0 normal
143 +HDMI-A-1 2560 0 1.5 90
144 +```
145 +
146 +Pola `SKALA` i `OBROT` są opcjonalne; `#` zaczyna komentarz. Nazwy monitorów
147 +zobaczysz w logu startowym (`układ wyjść zaktualizowany ... DP-1=...`).
148 +Kolejność nadpisywania: **plik → `PAGAN_OUTPUTS`/`PAGAN_SCALE`/`PAGAN_TRANSFORM`**
149 +(zmienne wygrywają, więc łatwo je przetestować bez ruszania pliku).
150 +
124 151 ## Instalacja zależności (Manjaro/Arch)
125 152
126 153 ```bash
@@ -134,7 +161,7 @@ sudo pacman -S --needed mesa wayland libxkbcommon cairo pkgconf
134 161 sudo pacman -S --needed libdrm libgbm libinput libseat libxkbcommon \
135 162 mesa vulkan-icd-loader
136 163
137 -# Klienci X11 (opcjonalnie, pod przyszły XWayland):
164 +# Klienci X11 (XWayland) — opcjonalnie, ale bez tego aplikacje X11 nie działają:
138 165 sudo pacman -S --needed xorg-xwayland
139 166
140 167 # Narzędzia do TESTU screencopy (NIE są częścią Rusta — nie instalują cargo):
@@ -157,6 +184,8 @@ Przechwytywane zanim trafią do klienta (patrz `input.rs::compositor_shortcut`):
157 184 | `Ctrl+Alt+Backspace` | zakończ kompozytor |
158 185 | `Ctrl+Alt+Left` / `Right` | fokus na poprzednie / następne okno |
159 186 | `Ctrl+Alt+Delete` | zamknij okno z fokusem (`xdg_toplevel.close`) |
187 +| `Ctrl+Alt+M` | zminimalizuj okno z fokusem |
188 +| `Ctrl+Alt+U` | przywróć ostatnio zminimalizowane okno |
160 189
161 190 ## Hybrydowe CSD — jak to działa
162 191
@@ -234,9 +263,9 @@ Kompozytor obsługuje wiele wyjść tak, jak wypada:
234 263 Stan wyjść logujemy po każdej zmianie:
235 264 `układ wyjść zaktualizowany count=2 outputs=DP-1=2560x1440+0+0, HDMI-A-1=1920x1080+2560+0`.
236 265
237 -Czego jeszcze nie ma: **skalowania HiDPI / transformacji** (wyjścia działają w
238 -skali 1 i `Transform::Normal`) oraz konfiguracji układu przez użytkownika
239 -(obecnie zawsze lewa→prawa). To naturalne kolejne kroki.
266 +Czego jeszcze nie ma: **przeliczania układu przy zmianie skali w locie**.
267 +Skalowanie (HiDPI) i obrót są już wspierane — globalnie (`PAGAN_SCALE`,
268 +`PAGAN_TRANSFORM`) oraz **per monitor** w `outputs.conf`.
240 269
241 270 ## Backend tty-udev (produkcja)
242 271
@@ -264,11 +293,15 @@ uruchomieniu na prawdziwym TTY.
264 293
265 294 - Backend `udev` jest niezweryfikowany na sprzęcie (patrz sekcja wyżej i `// WERYFIKUJ:`).
266 295 - Obsługa subsurface'ów w `commit` jest uproszczona (bierzemy root drzewa).
267 -- Brak **XWayland** (klienci X11). `wlr-screencopy` napisaliśmy od zera
268 - (`src/screencopy.rs`) — działa z `grim` na backendzie winit.
269 -- `fractional_scale` + `PAGAN_SCALE` dają podstawę HiDPI, ale nie ma jeszcze
270 - przeliczania układu przy zmianie skali w locie ani per-output scale z konfigu.
271 -- Ikona **DnD** jest śledzona, ale jeszcze nie rysowana.
272 -- Kursor rysowany jest jako prosty prostokąt (brak motywu XCursor).
296 +- **XWayland** zaimplementowany; wymaga obecności binarki `Xwayland`
297 + (`sudo pag install xorg-xwayland` w PaganOS, `sudo pacman -S xorg-xwayland`
298 + w Arch/Manjaro). Bez niej kompozytor działa, ale bez aplikacji X11.
299 +- `wlr-screencopy` napisaliśmy od zera (`src/screencopy.rs`) — działa z `grim`
300 + na backendzie winit (kursor: `grim -c`).
301 +- **Minimalizacja** chowa okno z `Space` i przywraca przez `Ctrl+Alt+U`.
302 + Docelowo trafi na pasek zadań / `wlr-foreign-toplevel`.
303 +- **Ikona DnD** jest już rysowana pod kursorem (`render.rs::output_elements`).
304 +- Kursor pochodzi z **motywu XCursor** (`XCURSOR_THEME`/`XCURSOR_SIZE`); gdy motywu
305 + brak, rysowany jest czytelny prostokąt zapasowy.
273 306 - `pointer_gestures`/`relative_pointer` są zarejestrowane; pełne przekazywanie
274 307 zdarzeń gestów zależy od backendu (udev przekazuje ruch względny).
275 308 new file mode 100644
@@ -0,0 +1,159 @@
1 +//! Kursor z motywu XCursor (zamiast rysowanego prostokąta).
2 +//!
3 +//! PO CO: kompozytor desktopowy powinien pokazywać właściwy kursor (strzałka,
4 +//! „łapka”, I-beam, kursor zmiany rozmiaru) z motywu systemowego. Wcześniej
5 +//! rysowaliśmy zastępczy prostokąt — działał, ale wyglądał obco.
6 +//!
7 +//! Źródło motywu to `XCURSOR_THEME` / `XCURSOR_SIZE` (standardowe zmienne
8 +//! środowiska), a same pliki czytamy przez crate `xcursor` (biblioteka
9 +//! `libXcursor`). Gdy motywu brak, `buffer()` zwraca `None` — kompozytor
10 +//! wraca wtedy do prostokąta, więc brak motywu nie psuje sesji.
11 +
12 +use std::{
13 + collections::HashMap,
14 + io::Read,
15 + sync::{Mutex, OnceLock},
16 + time::Duration,
17 +};
18 +
19 +use smithay::{
20 + backend::{
21 + allocator::Fourcc,
22 + renderer::element::memory::MemoryRenderBuffer,
23 + },
24 + input::pointer::CursorIcon,
25 + utils::Transform,
26 +};
27 +use xcursor::{
28 + parser::{parse_xcursor, Image},
29 + CursorTheme,
30 +};
31 +
32 +/// Motyw kursora + cache wczytanych ikon.
33 +///
34 +/// PO CO cache: parsowanie pliku ikony przy KAŻDEJ klatce byłoby marnotrawstwem
35 +/// (setki razy na sekundę). Wczytujemy raz na nazwę ikony.
36 +pub struct Theme {
37 + theme: CursorTheme,
38 + /// Nominalny rozmiar kursora (z `XCURSOR_SIZE`).
39 + size: u32,
40 + cache: Mutex<HashMap<String, Option<Vec<Image>>>>,
41 +}
42 +
43 +impl Theme {
44 + fn load() -> Self {
45 + let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".to_string());
46 + let size = std::env::var("XCURSOR_SIZE")
47 + .ok()
48 + .and_then(|value| value.trim().parse::<u32>().ok())
49 + .filter(|value| *value > 0)
50 + .unwrap_or(24);
51 +
52 + tracing::info!(theme = %name, size, "ładowanie motywu kursora");
53 + Self {
54 + theme: CursorTheme::load(&name),
55 + size,
56 + cache: Mutex::new(HashMap::new()),
57 + }
58 + }
59 +
60 + /// Wczytuje (z cache) ikony o danej nazwie, np. `default`, `text`, `pointer`.
61 + fn icons(&self, name: &str) -> Option<Vec<Image>> {
62 + if let Some(cached) = self.cache.lock().unwrap().get(name) {
63 + return cached.clone();
64 + }
65 +
66 + let loaded = self.load_icon(name);
67 + if loaded.is_none() {
68 + tracing::debug!(icon = name, "brak ikony w motywie kursora");
69 + }
70 + self.cache
71 + .lock()
72 + .unwrap()
73 + .insert(name.to_string(), loaded.clone());
74 + loaded
75 + }
76 +
77 + fn load_icon(&self, name: &str) -> Option<Vec<Image>> {
78 + let path = self.theme.load_icon(name)?;
79 + let mut data = Vec::new();
80 + std::fs::File::open(path).ok()?.read_to_end(&mut data).ok()?;
81 + parse_xcursor(&data)
82 + }
83 +
84 + /// Wybiera konkretną klatkę obrazu dla ikony (z awaryjnym `default`).
85 + pub fn image(&self, icon: CursorIcon, scale: u32, time: Duration) -> Option<Image> {
86 + let name = icon.name();
87 + // `or_else` na `default`: część ikon (np. egzotyczne kształty resize)
88 + // bywa nieobecna w motywie — lepiej pokazać strzałkę niż nic.
89 + let icons = self.icons(name).or_else(|| {
90 + if name == "default" {
91 + None
92 + } else {
93 + self.icons("default")
94 + }
95 + })?;
96 +
97 + frame(time.as_millis() as u32, self.size * scale.max(1), &icons)
98 + }
99 +
100 + /// Buduje bufor do narysowania kursora + punkt „hotspot” (czubek strzałki).
101 + ///
102 + /// Zwraca `None`, gdy nie ma motywu — wołający użyje wtedy prostokąta.
103 + pub fn buffer(
104 + &self,
105 + icon: CursorIcon,
106 + scale: u32,
107 + time: Duration,
108 + ) -> Option<(MemoryRenderBuffer, (i32, i32))> {
109 + let image = self.image(icon, scale, time)?;
110 + let buffer = MemoryRenderBuffer::from_slice(
111 + &image.pixels_rgba,
112 + Fourcc::Argb8888,
113 + (image.width as i32, image.height as i32),
114 + 1,
115 + Transform::Normal,
116 + None,
117 + );
118 + Some((buffer, (image.xhot as i32, image.yhot as i32)))
119 + }
120 +}
121 +
122 +/// Ikony najbliższe zadanemu rozmiarowi nominalnemu (XCursor trzyma kilka wariantów).
123 +fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
124 + let nearest = images
125 + .iter()
126 + .min_by_key(|image| (size as i32 - image.size as i32).abs());
127 + let (w, h) = nearest
128 + .map(|image| (image.width, image.height))
129 + .unwrap_or((0, 0));
130 + images
131 + .iter()
132 + .filter(move |image| image.width == w && image.height == h)
133 +}
134 +
135 +/// Wybiera klatkę animacji kursora dla danego czasu.
136 +fn frame(millis: u32, size: u32, images: &[Image]) -> Option<Image> {
137 + let total: u32 = nearest_images(size, images).map(|image| image.delay).sum();
138 + if total == 0 {
139 + return nearest_images(size, images).next().cloned();
140 + }
141 +
142 + let mut remaining = millis % total;
143 + for image in nearest_images(size, images) {
144 + if remaining < image.delay {
145 + return Some(image.clone());
146 + }
147 + remaining -= image.delay;
148 + }
149 + nearest_images(size, images).next().cloned()
150 +}
151 +
152 +/// Globalny motyw kursora — ładowany leniwie, dokładnie raz.
153 +///
154 +/// PO CO globalnie: motyw jest jeden dla całego kompozytora, a trzymanie go
155 +/// w stanie backendu zmuszałoby do plątania typów zależnych od backendu.
156 +pub fn theme() -> &'static Theme {
157 + static THEME: OnceLock<Theme> = OnceLock::new();
158 + THEME.get_or_init(Theme::load)
159 +}
@@ -115,6 +115,14 @@ impl From<KeyboardFocusTarget> for PointerFocusTarget {
115 115 WindowSurface::Wayland(surface) => {
116 116 PointerFocusTarget::WlSurface(surface.wl_surface().clone())
117 117 }
118 + // Okno X11 ma powiązaną `wl_surface` (XWayland tworzy ją przy
119 + // asocjacji, zanim okno zostanie zarządzane), więc konwersja jest
120 + // zawsze wykonalna. `expect` jak `unreachable!()` w anvilu.
121 + WindowSurface::X11(surface) => PointerFocusTarget::WlSurface(
122 + surface
123 + .wl_surface()
124 + .expect("okno X11 bez powiązanej wl_surface"),
125 + ),
118 126 },
119 127 KeyboardFocusTarget::Popup(popup) => PointerFocusTarget::Popup(popup),
120 128 }
@@ -147,6 +155,7 @@ impl WaylandFocus for KeyboardFocusTarget {
147 155 match self {
148 156 KeyboardFocusTarget::Window(w) => match w.underlying_surface() {
149 157 WindowSurface::Wayland(surface) => Some(Cow::Borrowed(surface.wl_surface())),
158 + WindowSurface::X11(surface) => surface.wl_surface().map(Cow::Owned),
150 159 },
151 160 KeyboardFocusTarget::Popup(p) => Some(Cow::Borrowed(p.wl_surface())),
152 161 }
@@ -350,6 +359,11 @@ impl KeyboardTarget<MyCompositor> for KeyboardFocusTarget {
350 359 WindowSurface::Wayland(w) => {
351 360 KeyboardTarget::enter(w.wl_surface(), seat, data, keys, serial)
352 361 }
362 + WindowSurface::X11(surface) => {
363 + if let Some(wl) = surface.wl_surface() {
364 + KeyboardTarget::enter(&wl, seat, data, keys, serial)
365 + }
366 + }
353 367 },
354 368 KeyboardFocusTarget::Popup(p) => {
355 369 KeyboardTarget::enter(p.wl_surface(), seat, data, keys, serial)
@@ -362,6 +376,11 @@ impl KeyboardTarget<MyCompositor> for KeyboardFocusTarget {
362 376 WindowSurface::Wayland(w) => {
363 377 KeyboardTarget::leave(w.wl_surface(), seat, data, serial)
364 378 }
379 + WindowSurface::X11(surface) => {
380 + if let Some(wl) = surface.wl_surface() {
381 + KeyboardTarget::leave(&wl, seat, data, serial)
382 + }
383 + }
365 384 },
366 385 KeyboardFocusTarget::Popup(p) => {
367 386 KeyboardTarget::leave(p.wl_surface(), seat, data, serial)
@@ -382,6 +401,11 @@ impl KeyboardTarget<MyCompositor> for KeyboardFocusTarget {
382 401 WindowSurface::Wayland(w) => {
383 402 KeyboardTarget::key(w.wl_surface(), seat, data, key, state, serial, time)
384 403 }
404 + WindowSurface::X11(surface) => {
405 + if let Some(wl) = surface.wl_surface() {
406 + KeyboardTarget::key(&wl, seat, data, key, state, serial, time)
407 + }
408 + }
385 409 },
386 410 KeyboardFocusTarget::Popup(p) => {
387 411 KeyboardTarget::key(p.wl_surface(), seat, data, key, state, serial, time)
@@ -400,6 +424,11 @@ impl KeyboardTarget<MyCompositor> for KeyboardFocusTarget {
400 424 WindowSurface::Wayland(w) => {
401 425 KeyboardTarget::modifiers(w.wl_surface(), seat, data, modifiers, serial)
402 426 }
427 + WindowSurface::X11(surface) => {
428 + if let Some(wl) = surface.wl_surface() {
429 + KeyboardTarget::modifiers(&wl, seat, data, modifiers, serial)
430 + }
431 + }
403 432 },
404 433 KeyboardFocusTarget::Popup(p) => {
405 434 KeyboardTarget::modifiers(p.wl_surface(), seat, data, modifiers, serial)
@@ -66,6 +66,27 @@ impl From<ResizeEdge> for xdg_toplevel::ResizeEdge {
66 66 }
67 67 }
68 68
69 +/// Konwersja z krawędzi X11 (przychodzi z `XwmHandler::resize_request`).
70 +///
71 +/// PO CO: okna X11 proszą o resize tą samą krawędzią, ale Smithay opisuje ją
72 +/// osobnym enumem bez wariantu `None` — mapujemy wprost na nasze stałe, więc
73 +/// jeden grab obsługuje oba światy.
74 +impl From<smithay::xwayland::xwm::ResizeEdge> for ResizeEdge {
75 + fn from(x: smithay::xwayland::xwm::ResizeEdge) -> Self {
76 + use smithay::xwayland::xwm::ResizeEdge as X;
77 + match x {
78 + X::Top => ResizeEdge::TOP,
79 + X::Bottom => ResizeEdge::BOTTOM,
80 + X::Left => ResizeEdge::LEFT,
81 + X::Right => ResizeEdge::RIGHT,
82 + X::TopLeft => ResizeEdge::TOP_LEFT,
83 + X::BottomLeft => ResizeEdge::BOTTOM_LEFT,
84 + X::TopRight => ResizeEdge::TOP_RIGHT,
85 + X::BottomRight => ResizeEdge::BOTTOM_RIGHT,
86 + }
87 + }
88 +}
89 +
69 90 /// Informacje potrzebne do prowadzenia operacji zmiany rozmiaru.
70 91 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
71 92 pub struct ResizeData {
@@ -119,19 +140,23 @@ impl PointerMoveSurfaceGrab {
119 140 data.space.output_geometry(&output)
120 141 }
121 142
122 - /// Wysyła klientowi nowy rozmiar + stan „tiled”/„maximized” przez `configure`.
143 + /// Wysyła klientowi nową geometrię + stan „tiled”/„maximized” przez `configure`.
123 144 ///
124 145 /// PO CO nie wystarczy `map_element`: pozycję zmieniamy sami, ale ROZMIAR musi
125 146 /// zatwierdzić klient — to on rysuje bufor w swoim procesie. Stany `Tiled*`
126 147 /// mówią klientowi, że okno jest częścią kafla; `None` = przywrócenie (czyścimy
127 148 /// wszystkie stany, bo okno wraca do swobodnego trybu).
128 - fn send_geometry(&self, size: Size<i32, Logical>, zone: Option<SnapZone>) {
149 + ///
150 + /// X11 nie ma stanów xdg — okno X11 dostaje pozycję i rozmiar jednym
151 + /// `X11Surface::configure(Rectangle)`, dlatego bierzemy pełny prostokąt.
152 + fn send_geometry(&self, geometry: Rectangle<i32, Logical>, zone: Option<SnapZone>) {
129 153 // (lewo, prawo, góra, dół) — które krawędzie kafla zaznaczyć.
130 154 // Dla `Top` używamy `Maximized` (pełny ekran to nie kafel, a maksymalizacja).
131 155 let (l, r, t, b, maximized) = match zone {
132 156 Some(SnapZone::Left) => (true, false, true, true, false),
133 157 Some(SnapZone::Right) => (false, true, true, true, false),
134 158 Some(SnapZone::Top) => (false, false, false, false, true),
159 + Some(SnapZone::Bottom) => (false, false, false, true, false),
135 160 Some(SnapZone::TopLeft) => (true, false, true, false, false),
136 161 Some(SnapZone::TopRight) => (false, true, true, false, false),
137 162 Some(SnapZone::BottomLeft) => (true, false, false, true, false),
@@ -142,7 +167,7 @@ impl PointerMoveSurfaceGrab {
142 167 match self.window.underlying_surface() {
143 168 WindowSurface::Wayland(xdg) => {
144 169 xdg.with_pending_state(|state| {
145 - state.size = Some(size);
170 + state.size = Some(geometry.size);
146 171 let s = &mut state.states;
147 172 // Najpierw czyścimy wszystko, potem ustawiamy to, co trzeba —
148 173 // dzięki temu przejście np. „lewa połowa → ćwiartka” nie zostawia
@@ -170,6 +195,9 @@ impl PointerMoveSurfaceGrab {
170 195 });
171 196 xdg.send_pending_configure();
172 197 }
198 + WindowSurface::X11(x11) => {
199 + let _ = x11.configure(geometry);
200 + }
173 201 }
174 202 }
175 203 }
@@ -197,7 +225,8 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
197 225
198 226 // Czy kursor wjechał w strefę doklejania na którymś z wyjść?
199 227 let target = Self::output_under(data, event.location).and_then(|geometry| {
200 - snap_zone(event.location, geometry).map(|zone| (zone, snap_geometry(zone, geometry)))
228 + snap_zone(event.location, geometry, data.snap_config)
229 + .map(|zone| (zone, snap_geometry(zone, geometry)))
201 230 });
202 231
203 232 match target {
@@ -205,8 +234,8 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
205 234 // Wchodzimy w strefę LUB przechodzimy między strefami (np. róg→krawędź).
206 235 if self.current_snap != Some(zone) {
207 236 self.current_snap = Some(zone);
208 - // Rozmiar idzie do klienta, pozycję ustawiamy sami.
209 - self.send_geometry(geometry.size, Some(zone));
237 + // Geometrię (pozycję i rozmiar) przekazujemy klientowi.
238 + self.send_geometry(geometry, Some(zone));
210 239 data.space
211 240 .map_element(self.window.clone(), geometry.loc, false);
212 241 }
@@ -215,7 +244,10 @@ impl PointerGrab<MyCompositor> for PointerMoveSurfaceGrab {
215 244 // Wyjście ze strefy: przywróć rozmiar sprzed doklejenia i podążaj
216 245 // z powrotem za kursorem.
217 246 if self.current_snap.take().is_some() {
218 - self.send_geometry(self.initial_window_size, None);
247 + self.send_geometry(
248 + Rectangle::new(new_location.to_i32_round(), self.initial_window_size),
249 + None,
250 + );
219 251 }
220 252 data.space
221 253 .map_element(self.window.clone(), new_location.to_i32_round(), false);
@@ -350,19 +382,33 @@ impl PointerResizeSurfaceGrab {
350 382 /// "Resizing", wyślij finalny configure i przesuń okno, gdy zmienialiśmy
351 383 /// lewą/górną krawędź (bo rosnąc w lewo, okno musi "uciekać" w lewo).
352 384 fn finish_resize(&self, data: &mut MyCompositor, serial: Serial) {
353 - let xdg = match self.window.underlying_surface() {
354 - WindowSurface::Wayland(xdg) => xdg,
355 - };
356 -
357 - xdg.with_pending_state(|state| {
358 - state.states.unset(xdg_toplevel::State::Resizing);
359 - state.size = Some(self.last_window_size);
360 - });
361 - xdg.send_pending_configure();
385 + // Zakończenie różni się zależnie od typu okna:
386 + // * Wayland (xdg) — zdejmujemy stan „Resizing” i wysyłamy finalny configure;
387 + // klient potwierdzi go asynchronicznie (maszyna stanów niżej).
388 + // * X11 — jedno `configure` ustawia pozycję i rozmiar; nie ma ack-a.
389 + match self.window.underlying_surface() {
390 + WindowSurface::Wayland(xdg) => {
391 + xdg.with_pending_state(|state| {
392 + state.states.unset(xdg_toplevel::State::Resizing);
393 + state.size = Some(self.last_window_size);
394 + });
395 + xdg.send_pending_configure();
396 + }
397 + WindowSurface::X11(x11) => {
398 + let location = data
399 + .space
400 + .element_location(&self.window)
401 + .unwrap_or_else(|| Point::from((0, 0)));
402 + let _ = x11.configure(Rectangle::new(location, self.last_window_size));
403 + }
404 + }
362 405
363 406 if self.edges.intersects(ResizeEdge::TOP_LEFT) {
364 407 let geometry = self.window.geometry();
365 - let mut location = data.space.element_location(&self.window).unwrap();
408 + let mut location = data
409 + .space
410 + .element_location(&self.window)
411 + .unwrap_or_else(|| Point::from((0, 0)));
366 412
367 413 if self.edges.intersects(ResizeEdge::LEFT) {
368 414 location.x =
@@ -373,20 +419,26 @@ impl PointerResizeSurfaceGrab {
373 419 self.initial_window_location.y + (self.initial_window_size.h - geometry.size.h);
374 420 }
375 421 data.space.map_element(self.window.clone(), location, false);
422 + if let Some(x11) = self.window.x11_surface() {
423 + let _ = x11.configure(Rectangle::new(location, self.last_window_size));
424 + }
376 425 }
377 426
378 - if let Some(surface) = self.window.wl_surface() {
379 - with_states(&surface, |states| {
380 - let mut surface_data = states
381 - .data_map
382 - .get::<RefCell<SurfaceData>>()
383 - .expect("SurfaceData must be present; see ensure_initial_configure")
384 - .borrow_mut();
385 - if let ResizeState::Resizing(resize_data) = surface_data.resize_state {
386 - surface_data.resize_state =
387 - ResizeState::WaitingForFinalAck(resize_data, serial);
388 - }
389 - });
427 + // Tylko Wayland prowadzi maszynę stanów resize w `SurfaceData` okna.
428 + if let WindowSurface::Wayland(_) = self.window.underlying_surface() {
429 + if let Some(surface) = self.window.wl_surface() {
430 + with_states(&surface, |states| {
431 + let mut surface_data = states
432 + .data_map
433 + .get::<RefCell<SurfaceData>>()
434 + .expect("SurfaceData must be present; see ensure_initial_configure")
435 + .borrow_mut();
436 + if let ResizeState::Resizing(resize_data) = surface_data.resize_state {
437 + surface_data.resize_state =
438 + ResizeState::WaitingForFinalAck(resize_data, serial);
439 + }
440 + });
441 + }
390 442 }
391 443 }
392 444 }
@@ -464,6 +516,13 @@ impl PointerGrab<MyCompositor> for PointerResizeSurfaceGrab {
464 516 });
465 517 xdg.send_pending_configure();
466 518 }
519 + WindowSurface::X11(x11) => {
520 + let location = data
521 + .space
522 + .element_location(&self.window)
523 + .unwrap_or_else(|| Point::from((0, 0)));
524 + let _ = x11.configure(Rectangle::new(location, self.last_window_size));
525 + }
467 526 }
468 527 }
469 528
@@ -350,6 +350,10 @@ enum CompositorShortcut {
350 350 FocusPrev,
351 351 /// Zamknij okno z fokusem.
352 352 CloseFocused,
353 + /// Schowaj okno z fokusem (minimalizacja).
354 + Minimize,
355 + /// Przywróć ostatnio schowane okno.
356 + Restore,
353 357 }
354 358
355 359 /// Rozpoznaje skrót globalny na podstawie modyfikatorów i klawisza.
@@ -370,6 +374,9 @@ fn compositor_shortcut(
370 374 Keysym::Left => Some(CompositorShortcut::FocusPrev),
371 375 Keysym::Right => Some(CompositorShortcut::FocusNext),
372 376 Keysym::Delete => Some(CompositorShortcut::CloseFocused),
377 + // CapsLock może zamienić literę na wielką — akceptujemy obie.
378 + Keysym::m | Keysym::M => Some(CompositorShortcut::Minimize),
379 + Keysym::u | Keysym::U => Some(CompositorShortcut::Restore),
373 380 _ => None,
374 381 }
375 382 }
@@ -413,6 +420,8 @@ impl MyCompositor {
413 420 CompositorShortcut::FocusNext => self.focus_adjacent(1),
414 421 CompositorShortcut::FocusPrev => self.focus_adjacent(-1),
415 422 CompositorShortcut::CloseFocused => self.close_focused(),
423 + CompositorShortcut::Minimize => self.minimize_focused(),
424 + CompositorShortcut::Restore => self.restore_last_minimized(),
416 425 }
417 426 }
418 427
@@ -455,4 +464,18 @@ impl MyCompositor {
455 464 }
456 465 }
457 466 }
467 +
468 + /// Schowaj okno z fokusem (Ctrl+Alt+M).
469 + fn minimize_focused(&mut self) {
470 + let current = self
471 + .seat
472 + .get_keyboard()
473 + .and_then(|keyboard| keyboard.current_focus());
474 + if let Some(KeyboardFocusTarget::Window(window)) = current {
475 + // Fokus trzyma surowe `Window` — mapujemy je na `PaganWindow` z `space`.
476 + if let Some(window) = self.pagan_window_for(&window) {
477 + self.minimize_window(&window);
478 + }
479 + }
480 + }
458 481 }
@@ -12,6 +12,7 @@
12 12 //! - `focus` – cele fokusa (mysz/klawiatura),
13 13 //! - `shaders` – wbudowane shadery GLSL (zaokrąglenia + cień).
14 14
15 +mod cursor;
15 16 mod focus;
16 17 mod grabs;
17 18 mod input;
@@ -23,6 +24,7 @@ mod snap;
23 24 mod state;
24 25 mod window;
25 26 mod winit;
27 +mod xwayland;
26 28
27 29 #[cfg(feature = "udev")]
28 30 mod udev;
@@ -11,9 +11,9 @@
11 11 //!
12 12 //! To jest dokładnie ta warstwa, którą mają "prawdziwe" kompozytory.
13 13
14 -use std::collections::HashMap;
14 +use std::{collections::HashMap, path::PathBuf};
15 15
16 -use tracing::info;
16 +use tracing::{info, warn};
17 17
18 18 use smithay::{
19 19 desktop::{layer_map_for_output, space::SpaceElement},
@@ -42,7 +42,14 @@ fn configured_scale() -> Option<f64> {
42 42 /// PO CO opcjonalna: backend winit wymaga `Flipped180` do poprawnego rysowania,
43 43 /// więc domyślnie nie nadpisujemy transformacji, jeśli użytkownik jej nie podał.
44 44 fn configured_transform() -> Option<Transform> {
45 - match std::env::var("PAGAN_TRANSFORM").ok()?.trim() {
45 + std::env::var("PAGAN_TRANSFORM")
46 + .ok()
47 + .and_then(|value| parse_transform(value.trim()))
48 +}
49 +
50 +/// Zamienia zapis tekstowy obrotu na `Transform` (używane przez env i plik).
51 +fn parse_transform(value: &str) -> Option<Transform> {
52 + match value {
46 53 "normal" => Some(Transform::Normal),
47 54 "90" => Some(Transform::_90),
48 55 "180" => Some(Transform::_180),
@@ -52,29 +59,108 @@ fn configured_transform() -> Option<Transform> {
52 59 }
53 60 }
54 61
55 -/// Ręczne pozycje monitorów z `PAGAN_OUTPUTS`, np. `DP-1:0,0;HDMI-A-1:2560,0`.
62 +/// Konfiguracja jednego monitora: pozycja, skala, obrót.
56 63 ///
57 -/// PO CO: domyślnie układamy wyjścia poziomo, ale użytkownik może chcieć np.
58 -/// monitor pionowy albo ułożony inaczej. Bez wpisu w zmiennej działa automat.
59 -fn configured_positions() -> HashMap<String, Point<i32, Logical>> {
60 - let mut positions = HashMap::new();
61 - let Ok(spec) = std::env::var("PAGAN_OUTPUTS") else {
62 - return positions;
63 - };
64 -
65 - for entry in spec.split(';') {
66 - let mut parts = entry.split(':');
67 - let (Some(name), Some(coords)) = (parts.next(), parts.next()) else {
68 - continue;
69 - };
70 - let mut xy = coords
71 - .split(',')
72 - .filter_map(|value| value.trim().parse::<i32>().ok());
73 - if let (Some(x), Some(y)) = (xy.next(), xy.next()) {
74 - positions.insert(name.trim().to_string(), Point::from((x, y)));
64 +/// PO CO osobny typ: te trzy ustawienia pochodzą z różnych źródeł (plik + env)
65 +/// i muszą się wzajemnie nadpisywać **per monitor**, a nie globalnie.
66 +#[derive(Debug, Clone, Default)]
67 +struct OutputConfig {
68 + position: Option<Point<i32, Logical>>,
69 + scale: Option<f64>,
70 + transform: Option<Transform>,
71 +}
72 +
73 +/// Ścieżka do pliku konfiguracji monitorów.
74 +///
75 +/// Kolejność: `PAGAN_CONFIG` (wskazanie wprost) → `$XDG_CONFIG_HOME/pagan/outputs.conf`
76 +/// → `~/.config/pagan/outputs.conf`. Brak pliku nie jest błędem — wtedy działa
77 +/// automatyczny układ poziomy plus ewentualne nadpisania ze zmiennych środowiskowych.
78 +fn config_path() -> Option<PathBuf> {
79 + if let Ok(path) = std::env::var("PAGAN_CONFIG") {
80 + return Some(PathBuf::from(path));
81 + }
82 + let base = std::env::var("XDG_CONFIG_HOME")
83 + .ok()
84 + .map(PathBuf::from)
85 + .or_else(|| {
86 + std::env::var("HOME")
87 + .ok()
88 + .map(|home| PathBuf::from(home).join(".config"))
89 + })?;
90 + Some(base.join("pagan").join("outputs.conf"))
91 +}
92 +
93 +/// Parsuje jeden wiersz pliku konfiguracji: `NAZWA X Y [SKALA] [OBROT]`.
94 +///
95 +/// Przykład: `DP-1 0 0 1.5 normal`. Pola `SKALA` i `OBROT` są opcjonalne.
96 +fn parse_output_line(line: &str) -> Option<(String, OutputConfig)> {
97 + let mut parts = line.split_whitespace();
98 + let name = parts.next()?.to_string();
99 + let x = parts.next()?.parse::<i32>().ok()?;
100 + let y = parts.next()?.parse::<i32>().ok()?;
101 + let scale = parts.next().and_then(|value| value.parse::<f64>().ok());
102 + let transform = parts.next().and_then(parse_transform);
103 + Some((
104 + name,
105 + OutputConfig {
106 + position: Some(Point::from((x, y))),
107 + scale,
108 + transform,
109 + },
110 + ))
111 +}
112 +
113 +/// Wczytuje konfigurację monitorów z pliku, a potem nakłada nadpisania z env.
114 +///
115 +/// PO CO taka kolejność: plik jest trwałym "domem" ustawień, a zmienne
116 +/// środowiskowe wygodnym jednorazowym nadpisaniem (np. do testów).
117 +fn load_output_configs() -> HashMap<String, OutputConfig> {
118 + let mut configs = HashMap::new();
119 +
120 + if let Some(path) = config_path() {
121 + match std::fs::read_to_string(&path) {
122 + Ok(text) => {
123 + for raw in text.lines() {
124 + // Komentarz zaczyna się od `#`; puste wiersze pomijamy.
125 + let line = raw.split('#').next().unwrap_or("").trim();
126 + if line.is_empty() {
127 + continue;
128 + }
129 + match parse_output_line(line) {
130 + Some((name, cfg)) => {
131 + configs.insert(name, cfg);
132 + }
133 + None => warn!(line, "nie umiem sparsować wiersza konfiguracji monitora"),
134 + }
135 + }
136 + info!(path = %path.display(), count = configs.len(), "wczytano konfigurację monitorów");
137 + }
138 + // Brak pliku to normalna sytuacja — nie straszymy użytkownika.
139 + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
140 + Err(err) => {
141 + warn!(path = %path.display(), %err, "nie udało się wczytać pliku konfiguracji monitorów")
142 + }
75 143 }
76 144 }
77 - positions
145 +
146 + // `PAGAN_OUTPUTS=DP-1:0,0;HDMI-A-1:2560,0` — pozycje nadpisują plik.
147 + if let Ok(spec) = std::env::var("PAGAN_OUTPUTS") {
148 + for entry in spec.split(';') {
149 + let mut parts = entry.split(':');
150 + let (Some(name), Some(coords)) = (parts.next(), parts.next()) else {
151 + continue;
152 + };
153 + let mut xy = coords
154 + .split(',')
155 + .filter_map(|value| value.trim().parse::<i32>().ok());
156 + if let (Some(x), Some(y)) = (xy.next(), xy.next()) {
157 + configs.entry(name.trim().to_string()).or_default().position =
158 + Some(Point::from((x, y)));
159 + }
160 + }
161 + }
162 +
163 + configs
78 164 }
79 165
80 166 impl MyCompositor {
@@ -97,26 +183,26 @@ impl MyCompositor {
97 183 /// "dziura" w przestrzeni, a okna z usuniętego ekranu nie mają gdzie być.
98 184 pub fn relayout_outputs(&mut self) {
99 185 let outputs: Vec<_> = self.space.outputs().cloned().collect();
100 - let manual = configured_positions();
101 - let configured_scale = configured_scale();
102 - let configured_transform = configured_transform();
186 + let configs = load_output_configs();
187 + // Globalne nadpisania (dotyczą monitorów nieopisanych w pliku).
188 + let global_scale = configured_scale();
189 + let global_transform = configured_transform();
103 190
104 191 let mut offset = Point::<i32, Logical>::from((0, 0));
105 192 let mut summary = Vec::with_capacity(outputs.len());
106 193
107 194 for output in &outputs {
195 + let config = configs.get(&output.name()).cloned().unwrap_or_default();
196 + let scale = config.scale.or(global_scale);
197 + let transform = config.transform.or(global_transform);
198 +
108 199 // HiDPI / obrót — tylko gdy użytkownik jawnie o to poprosił.
109 - if configured_scale.is_some() || configured_transform.is_some() {
110 - output.change_current_state(
111 - None,
112 - configured_transform,
113 - configured_scale.map(Scale::Fractional),
114 - None,
115 - );
200 + if scale.is_some() || transform.is_some() {
201 + output.change_current_state(None, transform, scale.map(Scale::Fractional), None);
116 202 }
117 203
118 - // Pozycja: ręczna z PAGAN_OUTPUTS, w przeciwnym razie poziomo od (0,0).
119 - let position = manual.get(&output.name()).copied().unwrap_or(offset);
204 + // Pozycja: ręczna (plik lub PAGAN_OUTPUTS), w przeciwnym razie poziomo od (0,0).
205 + let position = config.position.unwrap_or(offset);
120 206 self.space.map_output(output, position);
121 207
122 208 // Warstwy (panele) przeliczają swoje strefy po zmianie geometrii.
@@ -256,3 +342,50 @@ impl MyCompositor {
256 342 };
257 343 }
258 344 }
345 +
346 +#[cfg(test)]
347 +mod tests {
348 + use super::*;
349 +
350 + #[test]
351 + fn pelny_wiersz_z_skala_i_obrotem() {
352 + let (name, cfg) = parse_output_line("DP-1 2560 0 1.5 90").unwrap();
353 + assert_eq!(name, "DP-1");
354 + assert_eq!(cfg.position, Some(Point::from((2560, 0))));
355 + assert_eq!(cfg.scale, Some(1.5));
356 + assert_eq!(cfg.transform, Some(Transform::_90));
357 + }
358 +
359 + #[test]
360 + fn minimalny_wiersz_tylko_pozycja() {
361 + let (name, cfg) = parse_output_line("HDMI-A-1 0 1080").unwrap();
362 + assert_eq!(name, "HDMI-A-1");
363 + assert_eq!(cfg.position, Some(Point::from((0, 1080))));
364 + assert_eq!(cfg.scale, None);
365 + assert_eq!(cfg.transform, None);
366 + }
367 +
368 + #[test]
369 + fn wiersz_bez_pozycji_jest_odrzucany() {
370 + // Sama nazwa bez X/Y nie ma sensu — wolimy zignorować niż zgadywać.
371 + assert!(parse_output_line("DP-1").is_none());
372 + assert!(parse_output_line("DP-1 abc 0").is_none());
373 + }
374 +
375 + #[test]
376 + fn nieznany_obrot_jest_ignorowany_ale_reszta_zostaje() {
377 + let (_, cfg) = parse_output_line("DP-1 0 0 1.0 obrotowo").unwrap();
378 + assert_eq!(cfg.transform, None);
379 + assert_eq!(cfg.scale, Some(1.0));
380 + }
381 +
382 + #[test]
383 + fn rozpoznajemy_wszystkie_obroty() {
384 + assert_eq!(parse_transform("normal"), Some(Transform::Normal));
385 + assert_eq!(parse_transform("90"), Some(Transform::_90));
386 + assert_eq!(parse_transform("180"), Some(Transform::_180));
387 + assert_eq!(parse_transform("270"), Some(Transform::_270));
388 + assert_eq!(parse_transform("flipped"), Some(Transform::Flipped));
389 + assert_eq!(parse_transform("bzdurek"), None);
390 + }
391 +}
@@ -8,22 +8,32 @@
8 8 //! z konkretnym rendererem. Używamy `GlesRenderer` w obu backendach (winit, udev),
9 9 //! więc nie tracimy na tym nic, a zyskujemy prostsze typy.
10 10
11 -use std::fmt;
11 +use std::{fmt, time::Duration};
12 12
13 13 use smithay::{
14 14 backend::renderer::{
15 15 damage::{Error as OutputDamageTrackerError, OutputDamageTracker, RenderOutputResult},
16 - element::solid::SolidColorRenderElement,
16 + element::{
17 + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement},
18 + solid::SolidColorRenderElement,
19 + surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
20 + Id, Kind,
21 + },
17 22 gles::{GlesError, GlesRenderer, GlesTarget},
23 + utils::CommitCounter,
18 24 Color32F,
19 25 },
20 26 desktop::{space::SpaceRenderElements, Space},
27 + input::pointer::CursorImageStatus,
21 28 output::Output,
22 29 render_elements,
23 - utils::{Logical, Physical, Point, Rectangle, Scale, Size},
30 + utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, Size},
24 31 };
25 32
26 -use crate::window::{PaganWindow, WindowRenderElement};
33 +use crate::{
34 + state::DndIcon,
35 + window::{PaganWindow, WindowRenderElement},
36 +};
27 37
28 38 /// Kolor tła pulpitu (ciemny motyw PaganDE).
29 39 pub const CLEAR_COLOR: Color32F = Color32F::new(0.055, 0.06, 0.08, 1.0);
@@ -32,14 +42,21 @@ pub const CLEAR_COLOR: Color32F = Color32F::new(0.055, 0.06, 0.08, 1.0);
32 42 //
33 43 // Celowo specjalizowane do `GlesRenderer`, żeby dały się mieszać w jednej liście
34 44 // z elementami okien (które zawierają `PixelShaderElement`).
45 +// * `Surface` — kursor ustawiony przez klienta (`wl_pointer.set_cursor`),
46 +// * `Memory` — kursor z motywu XCursor (nasz domyślny),
47 +// * `Pointer` — zapasowy prostokąt, gdy nie ma motywu.
35 48 render_elements! {
36 49 pub CustomRenderElements<=GlesRenderer>;
50 + Surface=WaylandSurfaceRenderElement<GlesRenderer>,
51 + Memory=MemoryRenderBufferRenderElement<GlesRenderer>,
37 52 Pointer=SolidColorRenderElement,
38 53 }
39 54
40 55 impl fmt::Debug for CustomRenderElements {
41 56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 57 match self {
58 + Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
59 + Self::Memory(arg0) => f.debug_tuple("Memory").field(arg0).finish(),
43 60 Self::Pointer(arg0) => f.debug_tuple("Pointer").field(arg0).finish(),
44 61 Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
45 62 }
@@ -65,12 +82,19 @@ impl fmt::Debug for OutputRenderElements {
65 82
66 83 /// Kursor rysowany przez kompozytor.
67 84 ///
68 -/// PO CO własny, a nie XCursor: w tym kroku chcemy zero dodatkowych zależności
69 -/// i motywów. Rysujemy prosty, dwuwarstwowy prostokąt (ciemna obwódka + jasny
70 -/// środek), czytelny na każdym tle. Podłączenie motywu XCursor to osobny krok.
85 +/// Trzy źródła, w kolejności ważności:
86 +/// 1. kursor ustawiony przez klienta (`CursorImageStatus::Surface`),
87 +/// 2. nazwany kursor z motywu XCursor (`CursorImageStatus::Named`),
88 +/// 3. zapasowy prostokąt (gdy motywu nie ma — np. brak `libXcursor`/plików).
89 +///
90 +/// Motyw ładujemy raz (globalny `cursor::theme()`), a wbudowany obraz buforujemy
91 +/// per nazwa ikony — inaczej parsowalibyśmy plik kursora w każdej klatce.
71 92 pub struct PointerElement {
72 93 location: Point<i32, Physical>,
73 94 visible: bool,
95 + status: CursorImageStatus,
96 + /// (nazwa ikony, bufor, hotspot) ostatnio przygotowanego kursora z motywu.
97 + cached: Option<(String, MemoryRenderBuffer, (i32, i32))>,
74 98 }
75 99
76 100 impl Default for PointerElement {
@@ -78,6 +102,8 @@ impl Default for PointerElement {
78 102 Self {
79 103 location: Point::from((0, 0)),
80 104 visible: true,
105 + status: CursorImageStatus::default_named(),
106 + cached: None,
81 107 }
82 108 }
83 109 }
@@ -89,36 +115,103 @@ impl PointerElement {
89 115 self.visible = visible;
90 116 }
91 117
92 - /// Buduje elementy kursora.
93 - pub fn render_elements(&self) -> Vec<CustomRenderElements> {
118 + /// Ustawia żądany kształt kursora (pochodzi z `SeatHandler::cursor_image`).
119 + pub fn set_status(&mut self, status: CursorImageStatus) {
120 + self.status = status;
121 + }
122 +
123 + /// Buduje elementy kursora. Wymaga renderera, bo import tekstury z pamięci
124 + /// (`MemoryRenderBufferRenderElement`) jest operacją renderera.
125 + pub fn render_elements(&mut self, renderer: &mut GlesRenderer) -> Vec<CustomRenderElements> {
94 126 if !self.visible {
95 127 return Vec::new();
96 128 }
129 +
130 + match self.status.clone() {
131 + CursorImageStatus::Hidden => Vec::new(),
132 + CursorImageStatus::Named(icon) => {
133 + let name = icon.name().to_string();
134 + let needs_reload = self
135 + .cached
136 + .as_ref()
137 + .map(|(cached, _, _)| cached != &name)
138 + .unwrap_or(true);
139 + if needs_reload {
140 + self.cached = crate::cursor::theme()
141 + .buffer(icon, 1, Duration::ZERO)
142 + .map(|(buffer, hotspot)| (name.clone(), buffer, hotspot));
143 + }
144 +
145 + if let Some((_, buffer, (hot_x, hot_y))) = &self.cached {
146 + // Odejmujemy hotspot, żeby czubek strzałki trafiał w kursor,
147 + // a nie lewy górny róg obrazka.
148 + let loc = Point::<i32, Physical>::from((
149 + self.location.x - hot_x,
150 + self.location.y - hot_y,
151 + ));
152 + if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
153 + renderer,
154 + loc.to_f64(),
155 + buffer,
156 + None,
157 + None,
158 + None,
159 + Kind::Cursor,
160 + ) {
161 + return vec![CustomRenderElements::Memory(element)];
162 + }
163 + }
164 +
165 + // Brak motywu (albo błąd importu) → czytelny prostokąt zapasowy.
166 + self.fallback_rectangle()
167 + }
168 + CursorImageStatus::Surface(surface) => {
169 + let elements = render_elements_from_surface_tree(
170 + renderer,
171 + &surface,
172 + self.location,
173 + Scale::from(1.0),
174 + 1.0,
175 + Kind::Cursor,
176 + );
177 + elements
178 + .into_iter()
179 + .map(CustomRenderElements::Surface)
180 + .collect()
181 + }
182 + }
183 + }
184 +
185 + /// Awaryjny kursor: dwuwarstwowy prostokąt (ciemna obwódka + jasny środek),
186 + /// czytelny na każdym tle. Używany tylko, gdy nie ma kursora z motywu.
187 + fn fallback_rectangle(&self) -> Vec<CustomRenderElements> {
97 188 let loc = self.location;
98 189 vec![
99 190 CustomRenderElements::Pointer(SolidColorRenderElement::new(
100 - smithay::backend::renderer::element::Id::new(),
191 + Id::new(),
101 192 Rectangle::new(loc, Size::from((12, 18))),
102 - smithay::backend::renderer::utils::CommitCounter::default(),
193 + CommitCounter::default(),
103 194 Color32F::new(0.08, 0.08, 0.10, 1.0),
104 - smithay::backend::renderer::element::Kind::Cursor,
195 + Kind::Cursor,
105 196 )),
106 197 CustomRenderElements::Pointer(SolidColorRenderElement::new(
107 - smithay::backend::renderer::element::Id::new(),
198 + Id::new(),
108 199 Rectangle::new(Point::from((loc.x + 2, loc.y + 2)), Size::from((8, 13))),
109 - smithay::backend::renderer::utils::CommitCounter::default(),
200 + CommitCounter::default(),
110 201 Color32F::new(0.95, 0.95, 0.97, 1.0),
111 - smithay::backend::renderer::element::Kind::Cursor,
202 + Kind::Cursor,
112 203 )),
113 204 ]
114 205 }
115 206 }
116 207
117 -/// Składa elementy klatki: okna z przestrzeni + nakładki (kursor) na wierzchu.
208 +/// Składa elementy klatki: okna z przestrzeni + nakładki (kursor, ikona DnD).
118 209 pub fn output_elements(
119 210 space: &Space<PaganWindow>,
120 211 output: &Output,
121 - mut custom_elements: Vec<CustomRenderElements>,
212 + pointer: Option<&mut PointerElement>,
213 + dnd: Option<&DndIcon>,
214 + pointer_location: Point<f64, Logical>,
122 215 renderer: &mut GlesRenderer,
123 216 ) -> (Vec<OutputRenderElements>, Color32F) {
124 217 let space_elements = smithay::desktop::space::space_render_elements::<_, PaganWindow, _>(
@@ -129,11 +222,47 @@ pub fn output_elements(
129 222 )
130 223 .expect("output bez ustawionego trybu? (patrz: output.change_current_state)");
131 224
132 - let mut elements: Vec<OutputRenderElements> =
133 - space_elements.into_iter().map(OutputRenderElements::Space).collect();
225 + // KOLEJNOŚĆ JEST KRYTYCZNA: `render_output` oczekuje elementów
226 + // OD PRZODU DO TYŁU (indeks 0 = na wierzchu), a rysuje listę od końca.
227 + // Smithay robi tak samo w `space::render_output`: najpierw elementy
228 + // niestandardowe (kursor, DnD), dopiero potem zawartość przestrzeni.
229 + // Gdyby kursor trafił na koniec listy, wylądowałby POD oknami.
230 + let mut elements: Vec<OutputRenderElements> = Vec::with_capacity(space_elements.len() + 2);
231 +
232 + // 1) Kursor — absolutnie na wierzchu (indeks 0).
233 + if let Some(pointer) = pointer {
234 + elements.extend(
235 + pointer
236 + .render_elements(renderer)
237 + .into_iter()
238 + .map(OutputRenderElements::Custom),
239 + );
240 + }
241 +
242 + // 2) Ikona przeciągania (drag & drop) — tuż POD kursorem, ale nad oknami.
243 + // Bez tego użytkownik nie widzi, co przeciąga.
244 + if let Some(icon) = dnd.filter(|icon| icon.surface.alive()) {
245 + // Pozycja ikony = pozycja kursora + przesunięcie narzucone przez klienta
246 + // (`buffer_delta`), żeby ikona „trzymała się” tego samego punktu.
247 + let position = (pointer_location + icon.offset.to_f64())
248 + .to_physical(Scale::from(1.0))
249 + .to_i32_round();
250 + elements.extend(
251 + render_elements_from_surface_tree(
252 + renderer,
253 + &icon.surface,
254 + position,
255 + Scale::from(1.0),
256 + 1.0,
257 + Kind::Unspecified,
258 + )
259 + .into_iter()
260 + .map(|element| OutputRenderElements::Custom(CustomRenderElements::Surface(element))),
261 + );
262 + }
134 263
135 - // Kursor rysujemy nad oknami.
136 - elements.extend(custom_elements.drain(..).map(OutputRenderElements::Custom));
264 + // 3) Zawartość przestrzeni (okna razem z ich cieniami) — na spodzie.
265 + elements.extend(space_elements.into_iter().map(OutputRenderElements::Space));
137 266
138 267 (elements, CLEAR_COLOR)
139 268 }
@@ -142,13 +271,16 @@ pub fn output_elements(
142 271 pub fn render_output<'d>(
143 272 output: &Output,
144 273 space: &Space<PaganWindow>,
145 - custom_elements: Vec<CustomRenderElements>,
274 + pointer: Option<&mut PointerElement>,
275 + dnd: Option<&DndIcon>,
276 + pointer_location: Point<f64, Logical>,
146 277 renderer: &mut GlesRenderer,
147 278 framebuffer: &mut GlesTarget<'_>,
148 279 damage_tracker: &'d mut OutputDamageTracker,
149 280 age: usize,
150 281 ) -> Result<RenderOutputResult<'d>, OutputDamageTrackerError<GlesError>> {
151 - let (elements, clear_color) = output_elements(space, output, custom_elements, renderer);
282 + let (elements, clear_color) =
283 + output_elements(space, output, pointer, dnd, pointer_location, renderer);
152 284 damage_tracker.render_output(renderer, framebuffer, age, &elements, clear_color)
153 285 }
154 286
@@ -282,15 +282,23 @@ fn capture(
282 282 .map_err(|_| "nie udało się zbindować bufora offscreen")?;
283 283
284 284 // Elementy do narysowania: opcjonalnie kursor na wierzchu.
285 - let custom = if request.overlay_cursor {
285 + let mut overlay_pointer = if request.overlay_cursor {
286 286 let mut pointer = render::PointerElement::default();
287 287 pointer.update(render::pointer_physical(pointer_location), true);
288 - pointer.render_elements()
288 + Some(pointer)
289 289 } else {
290 - Vec::new()
290 + None
291 291 };
292 292
293 - let (elements, clear_color) = render::output_elements(space, output, custom, renderer);
293 + // Ikony DnD nie rysujemy w zrzutach — to stan przejściowy interakcji.
294 + let (elements, clear_color) = render::output_elements(
295 + space,
296 + output,
297 + overlay_pointer.as_mut(),
298 + None,
299 + pointer_location,
300 + renderer,
301 + );
294 302
295 303 // `size` pochodzi z trybu wyjścia, więc jest już w pikselach fizycznych
296 304 // (skala 1, brak obrotu) — nie ma czego przeliczać.
@@ -12,23 +12,66 @@
12 12 //! ┌─────────┬─────────┐ krawędź lewa/prawa → połowa ekranu
13 13 //! │ TL │ TR │ krawędź górna → pełny ekran (maksymalizacja)
14 14 //! ├─────────┼─────────┤ róg (2 krawędzie) → ćwiartka ekranu
15 -//! │ │ │ krawędź dolna → BEZ doklejania
16 -//! │ BL │ BR │
15 +//! │ │ │ krawędź dolna → bez doklejania
16 +//! │ BL │ BR │ (albo dolna połowa, gdy PAGAN_SNAP_BOTTOM=1)
17 17 //! └─────────┴─────────┘
18 18 //! ```
19 19 //!
20 20 //! „Stackowanie”: gdy okna doklejane są do przeciwległych połówek lub do rogów,
21 21 //! same układają się w kafelki (2 kolumny / siatka 2×2) — nie potrzebujemy do tego
22 22 //! żadnego menedżera kafelkowego, wystarczy, że każdy snap zajmuje inną strefę.
23 +//!
24 +//! Konfiguracja (zmienne środowiskowe, czytane raz przy starcie):
25 +//! * `PAGAN_SNAP_THRESHOLD` — odległość od krawędzi włączająca doklejanie (px),
26 +//! * `PAGAN_SNAP_BOTTOM=1` — włącza doklejanie do dolnej krawędzi (dolna połowa).
23 27
24 28 use smithay::utils::{Logical, Point, Rectangle};
25 29
26 -/// Odległość kursora od krawędzi wyjścia, przy której włączamy doklejanie
27 -/// (w pikselach logicznych). Wartość kompromisowa: na tyle duża, by łatwo
28 -/// „trafić” krawędź myszką, i na tyle mała, by nie snapowało przy zwykłym
30 +/// Domyślna odległość kursora od krawędzi wyjścia, przy której włączamy
31 +/// doklejanie (w pikselach logicznych). Wartość kompromisowa: na tyle duża, by
32 +/// łatwo „trafić” krawędź myszką, i na tyle mała, by nie snapowało przy zwykłym
29 33 /// przesuwaniu okna po środku ekranu.
30 34 pub const SNAP_THRESHOLD: f64 = 12.0;
31 35
36 +/// Konfiguracja doklejania (czytana ze środowiska przy starcie kompozytora).
37 +#[derive(Debug, Clone, Copy)]
38 +pub struct SnapConfig {
39 + /// Odległość od krawędzi włączająca doklejanie.
40 + pub threshold: f64,
41 + /// Czy dolna krawędź ma doklejać (dolna połowa). Domyślnie `false`, jak w Windows.
42 + pub bottom: bool,
43 +}
44 +
45 +impl Default for SnapConfig {
46 + fn default() -> Self {
47 + Self {
48 + threshold: SNAP_THRESHOLD,
49 + bottom: false,
50 + }
51 + }
52 +}
53 +
54 +impl SnapConfig {
55 + /// Buduje konfigurację z `PAGAN_SNAP_THRESHOLD` i `PAGAN_SNAP_BOTTOM`.
56 + ///
57 + /// PO CO z env, a nie na sztywno: ergonomia myszy jest subiektywna, a to
58 + /// pozwala dostroić próg bez rekompilacji (spójne z `PAGAN_SCALE` itd.).
59 + pub fn from_env() -> Self {
60 + let threshold = std::env::var("PAGAN_SNAP_THRESHOLD")
61 + .ok()
62 + .and_then(|value| value.trim().parse::<f64>().ok())
63 + .filter(|value| *value > 0.0)
64 + .unwrap_or(SNAP_THRESHOLD);
65 +
66 + let bottom = matches!(
67 + std::env::var("PAGAN_SNAP_BOTTOM").as_deref(),
68 + Ok("1") | Ok("true") | Ok("yes") | Ok("on")
69 + );
70 +
71 + Self { threshold, bottom }
72 + }
73 +}
74 +
32 75 /// Strefa doklejania wyznaczona z pozycji kursora.
33 76 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
34 77 pub enum SnapZone {
@@ -38,6 +81,8 @@ pub enum SnapZone {
38 81 Right,
39 82 /// Górna krawędź (bez rogów) → pełny ekran.
40 83 Top,
84 + /// Dolna krawędź (tylko gdy włączona w konfiguracji) → dolna połowa.
85 + Bottom,
41 86 /// Lewy górny róg → lewa górna ćwiartka.
42 87 TopLeft,
43 88 /// Prawy górny róg → prawa górna ćwiartka.
@@ -51,12 +96,12 @@ pub enum SnapZone {
51 96 /// Wyznacza strefę doklejania dla kursora w `pointer`.
52 97 ///
53 98 /// Zwraca `None`, gdy kursor jest poza obszarem wyjścia albo w jego wnętrzu
54 -/// (dalej niż `SNAP_THRESHOLD` od każdej istotnej krawędzi). Dolna krawędź
55 -/// celowo **nie** snapuje (jak w Aero Snap) — doklejanie do dołu jest mylące,
56 -/// bo zasłania pasek zadań/panele.
99 +/// (dalej niż próg od każdej istotnej krawędzi). Dolna krawędź celowo **nie**
100 +/// snapuje domyślnie (jak w Aero Snap) — doklejanie do dołu bywa mylące.
57 101 pub fn snap_zone(
58 102 pointer: Point<f64, Logical>,
59 103 output: Rectangle<i32, Logical>,
104 + config: SnapConfig,
60 105 ) -> Option<SnapZone> {
61 106 // Kursor poza wyjściem → brak doklejania (nie „przyciągamy” z drugiego monitora).
62 107 if !output.to_f64().contains(pointer) {
@@ -67,11 +112,12 @@ pub fn snap_zone(
67 112 let y0 = output.loc.y as f64;
68 113 let x1 = x0 + output.size.w as f64;
69 114 let y1 = y0 + output.size.h as f64;
115 + let t = config.threshold;
70 116
71 - let at_left = pointer.x - x0 <= SNAP_THRESHOLD;
72 - let at_right = x1 - pointer.x <= SNAP_THRESHOLD;
73 - let at_top = pointer.y - y0 <= SNAP_THRESHOLD;
74 - let at_bottom = y1 - pointer.y <= SNAP_THRESHOLD;
117 + let at_left = pointer.x - x0 <= t;
118 + let at_right = x1 - pointer.x <= t;
119 + let at_top = pointer.y - y0 <= t;
120 + let at_bottom = y1 - pointer.y <= t;
75 121
76 122 match (at_left, at_right, at_top, at_bottom) {
77 123 // Rogi mają priorytet nad pojedynczą krawędzią.
@@ -82,7 +128,9 @@ pub fn snap_zone(
82 128 (true, _, _, _) => Some(SnapZone::Left),
83 129 (_, true, _, _) => Some(SnapZone::Right),
84 130 (_, _, true, _) => Some(SnapZone::Top),
85 - // Dolna krawędź i środek: brak doklejania.
131 + // Dolna krawędź: tylko gdy użytkownik ją włączył.
132 + (_, _, _, true) if config.bottom => Some(SnapZone::Bottom),
133 + // Dolna krawędź bez wsparcia i środek: brak doklejania.
86 134 _ => None,
87 135 }
88 136 }
@@ -106,6 +154,8 @@ pub fn snap_geometry(zone: SnapZone, output: Rectangle<i32, Logical>) -> Rectang
106 154 SnapZone::Right => Rectangle::new((x + half_w, y).into(), (w - half_w, h).into()),
107 155 // „Górna krawędź” = zmaksymalizowane okno na całym wyjściu.
108 156 SnapZone::Top => Rectangle::new((x, y).into(), (w, h).into()),
157 + // Dolna krawędź (gdy włączona) = dolna połowa.
158 + SnapZone::Bottom => Rectangle::new((x, y + half_h).into(), (w, h - half_h).into()),
109 159 SnapZone::TopLeft => Rectangle::new((x, y).into(), (half_w, half_h).into()),
110 160 SnapZone::TopRight => Rectangle::new((x + half_w, y).into(), (w - half_w, half_h).into()),
111 161 SnapZone::BottomLeft => Rectangle::new((x, y + half_h).into(), (half_w, h - half_h).into()),
@@ -129,45 +179,97 @@ mod tests {
129 179 Point::from((x, y))
130 180 }
131 181
182 + fn cfg() -> SnapConfig {
183 + SnapConfig::default()
184 + }
185 +
132 186 #[test]
133 187 fn srodek_ekranu_nie_snapuje() {
134 - assert_eq!(snap_zone(p(960.0, 540.0), output()), None);
188 + assert_eq!(snap_zone(p(960.0, 540.0), output(), cfg()), None);
189 + }
190 +
191 + #[test]
192 + fn dolna_krawedz_nie_snapuje_domyslnie() {
193 + // Środek dolnej krawędzi — domyślnie bez doklejania (jak w Windows).
194 + assert_eq!(snap_zone(p(960.0, 1079.0), output(), cfg()), None);
195 + }
196 +
197 + #[test]
198 + fn dolna_krawedz_snapuje_gdy_wlaczona() {
199 + let config = SnapConfig {
200 + threshold: SNAP_THRESHOLD,
201 + bottom: true,
202 + };
203 + assert_eq!(
204 + snap_zone(p(960.0, 1079.0), output(), config),
205 + Some(SnapZone::Bottom)
206 + );
207 + // Geometria: dolna połowa 540..1080.
208 + assert_eq!(
209 + snap_geometry(SnapZone::Bottom, output()),
210 + Rectangle::new((0, 540).into(), (1920, 540).into())
211 + );
135 212 }
136 213
137 214 #[test]
138 - fn dolna_krawedz_nie_snapuje() {
139 - // Środek dolnej krawędzi — świadomie bez doklejania.
140 - assert_eq!(snap_zone(p(960.0, 1079.0), output()), None);
215 + fn prog_jest_konfigurowalny() {
216 + // Przy bardzo małym progu punkt 3 px od krawędzi już nie snapuje.
217 + let strict = SnapConfig {
218 + threshold: 1.0,
219 + bottom: false,
220 + };
221 + assert_eq!(snap_zone(p(3.0, 540.0), output(), strict), None);
222 + // A przy dużym progu snapuje nawet 20 px od krawędzi.
223 + let loose = SnapConfig {
224 + threshold: 25.0,
225 + bottom: false,
226 + };
227 + assert_eq!(
228 + snap_zone(p(20.0, 540.0), output(), loose),
229 + Some(SnapZone::Left)
230 + );
141 231 }
142 232
143 233 #[test]
144 234 fn krawedzie_daja_polowki_i_pelny_ekran() {
145 - assert_eq!(snap_zone(p(3.0, 540.0), output()), Some(SnapZone::Left));
146 - assert_eq!(snap_zone(p(1917.0, 540.0), output()), Some(SnapZone::Right));
147 - assert_eq!(snap_zone(p(960.0, 3.0), output()), Some(SnapZone::Top));
235 + assert_eq!(
236 + snap_zone(p(3.0, 540.0), output(), cfg()),
237 + Some(SnapZone::Left)
238 + );
239 + assert_eq!(
240 + snap_zone(p(1917.0, 540.0), output(), cfg()),
241 + Some(SnapZone::Right)
242 + );
243 + assert_eq!(
244 + snap_zone(p(960.0, 3.0), output(), cfg()),
245 + Some(SnapZone::Top)
246 + );
148 247 }
149 248
150 249 #[test]
151 250 fn rogi_maja_priorytet() {
152 - assert_eq!(snap_zone(p(3.0, 3.0), output()), Some(SnapZone::TopLeft));
153 251 assert_eq!(
154 - snap_zone(p(1917.0, 3.0), output()),
252 + snap_zone(p(3.0, 3.0), output(), cfg()),
253 + Some(SnapZone::TopLeft)
254 + );
255 + assert_eq!(
256 + snap_zone(p(1917.0, 3.0), output(), cfg()),
155 257 Some(SnapZone::TopRight)
156 258 );
157 259 assert_eq!(
158 - snap_zone(p(3.0, 1077.0), output()),
260 + snap_zone(p(3.0, 1077.0), output(), cfg()),
159 261 Some(SnapZone::BottomLeft)
160 262 );
161 263 assert_eq!(
162 - snap_zone(p(1917.0, 1077.0), output()),
264 + snap_zone(p(1917.0, 1077.0), output(), cfg()),
163 265 Some(SnapZone::BottomRight)
164 266 );
165 267 }
166 268
167 269 #[test]
168 270 fn kursor_poza_wyjściem_nie_snapuje() {
169 - assert_eq!(snap_zone(p(1925.0, 540.0), output()), None);
170 - assert_eq!(snap_zone(p(-5.0, 540.0), output()), None);
271 + assert_eq!(snap_zone(p(1925.0, 540.0), output(), cfg()), None);
272 + assert_eq!(snap_zone(p(-5.0, 540.0), output(), cfg()), None);
171 273 }
172 274
173 275 #[test]
@@ -213,7 +315,10 @@ mod tests {
213 315 fn dziala_na_drugim_monitorze() {
214 316 // Wyjście przesunięte w prawo — geometrie muszą respektować offset.
215 317 let out = Rectangle::new((1920, 0).into(), (1920, 1080).into());
216 - assert_eq!(snap_zone(p(1923.0, 540.0), out), Some(SnapZone::Left));
318 + assert_eq!(
319 + snap_zone(p(1923.0, 540.0), out, cfg()),
320 + Some(SnapZone::Left)
321 + );
217 322 assert_eq!(
218 323 snap_geometry(SnapZone::Left, out),
219 324 Rectangle::new((1920, 0).into(), (960, 1080).into())
@@ -27,6 +27,7 @@ use smithay::{
27 27 desktop::{
28 28 find_popup_root_surface, layer_map_for_output, LayerSurface as DesktopLayerSurface,
29 29 PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Space,
30 + Window,
30 31 },
31 32 input::{
32 33 keyboard::XkbConfig,
@@ -46,7 +47,7 @@ use smithay::{
46 47 Client, Display, DisplayHandle, Resource,
47 48 },
48 49 },
49 - utils::{Logical, Point, Serial, Size},
50 + utils::{IsAlive, Logical, Point, Serial, Size, SERIAL_COUNTER as SCOUNTER},
50 51 wayland::{
51 52 buffer::BufferHandler,
52 53 compositor::{
@@ -99,6 +100,14 @@ use smithay::{
99 100 #[cfg(feature = "udev")]
100 101 use smithay::backend::renderer::ImportDma;
101 102
103 +// XWayland (aplikacje X11).
104 +use smithay::{
105 + wayland::{
106 + xwayland_keyboard_grab::XWaylandKeyboardGrabState, xwayland_shell::XWaylandShellState,
107 + },
108 + xwayland::X11Wm,
109 +};
110 +
102 111 use crate::{
103 112 focus::{KeyboardFocusTarget, PointerFocusTarget},
104 113 grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeState, SurfaceData},
@@ -125,14 +134,25 @@ impl ClientData for ClientState {
125 134 /// Ikona przeciągania i upuszczania (drag & drop) trzymana na czas operacji.
126 135 ///
127 136 /// PO CO: DnD działa funkcjonalnie bez rysowania ikony, ale użytkownik powinien
128 -/// widzieć, co niesie. Trzymamy powierzchnię i przesunięcie; rysowanie podłączymy
129 -/// razem z kursorem z motywu XCursor.
137 +/// widzieć, co niesie. Trzymamy powierzchnię i przesunięcie; rysowanie podłączone
138 +/// jest w `render.rs` (`output_elements`).
130 139 #[derive(Debug, Clone)]
131 140 pub struct DndIcon {
132 141 pub surface: WlSurface,
133 142 pub offset: Point<i32, Logical>,
134 143 }
135 144
145 +/// Okno odłożone na bok przez minimalizację.
146 +///
147 +/// PO CO zapamiętujemy pozycję: bez menedżera zadań (paska zadań) okno musi
148 +/// wrócić dokładnie tam, gdzie było. Trzymamy więc okno ORAZ jego ostatnią
149 +/// pozycję w przestrzeni.
150 +#[derive(Debug, Clone)]
151 +pub struct MinimizedWindow {
152 + pub window: PaganWindow,
153 + pub location: Point<i32, Logical>,
154 +}
155 +
136 156 /// Główny stan kompozytora.
137 157 pub struct MyCompositor {
138 158 /// Przestrzeń okien — kolejność elementów to Z-order (od spodu do wierzchu).
@@ -178,6 +198,13 @@ pub struct MyCompositor {
178 198 /// Ikona przeciągania (DnD) na czas operacji.
179 199 pub dnd_icon: Option<DndIcon>,
180 200
201 + /// Zminimalizowane okna (unmapped z `space`) razem z ostatnią pozycją.
202 + ///
203 + /// PO CO poza `space`: `Space` rysuje wszystko, co zawiera — nie ma pojęcia
204 + /// "ukryte". Odkładamy więc okno tutaj i przywracamy na żądanie
205 + /// (skrót klawiszowy / przyszły pasek zadań).
206 + pub minimized: Vec<MinimizedWindow>,
207 +
181 208 // --- dmabuf + layer-shell ---
182 209 /// Delegat protokołu `linux-dmabuf` (bufory GPU).
183 210 pub dmabuf_state: DmabufState,
@@ -188,6 +215,19 @@ pub struct MyCompositor {
188 215 /// Kolejka przechwyceń ekranu (`wlr-screencopy`) do wykonania w pętli renderu.
189 216 pub screencopy_queue: Vec<crate::screencopy::PendingCapture>,
190 217
218 + // --- XWayland (aplikacje X11) ---
219 + /// Global protokołu `xwayland_shell` — bez niego XWayland nie ma jak
220 + /// skojarzyć okna X11 z powierzchnią Waylanda.
221 + pub xwayland_shell_state: XWaylandShellState,
222 + /// Menedżer okien X11 (reparenting WM). `None`, dopóki XWayland nie zgłosi
223 + /// gotowości (albo gdy binarki `Xwayland` nie ma w systemie).
224 + pub xwm: Option<X11Wm>,
225 + /// Numer wyświetlacza X (dla `DISPLAY=:N` uruchamianych aplikacji X11).
226 + pub xdisplay: Option<u32>,
227 +
228 + /// Konfiguracja doklejania okien (próg + dolna krawędź), z `PAGAN_SNAP_*`.
229 + pub snap_config: crate::snap::SnapConfig,
230 +
191 231 /// Stan backendu tty-udev (DRM/KMS + libinput). `None` dla backendu winit.
192 232 #[cfg(feature = "udev")]
193 233 pub udev: Option<crate::udev::UdevState>,
@@ -276,6 +316,12 @@ impl MyCompositor {
276 316 // Screencopy: rejestrujemy global; logika przechwycenia w `screencopy.rs`.
277 317 crate::screencopy::init(&dh);
278 318
319 + // XWayland: `xwayland_shell` (skojarzenie okna X11 z wl_surface) oraz grab
320 + // klawiatury X11. `XWaylandKeyboardGrabState` nie wymaga pola — trzyma go
321 + // display; tworzymy wyłącznie po to, by zarejestrować global.
322 + let xwayland_shell_state = XWaylandShellState::new::<Self>(&dh);
323 + XWaylandKeyboardGrabState::new::<Self>(&dh);
324 +
279 325 // Seat: mysz + klawiatura. Klawiaturę tworzymy z domyślną mapą xkb.
280 326 let mut seat_state = SeatState::new();
281 327 let mut seat = seat_state.new_wl_seat(&dh, "pagan".to_string());
@@ -312,10 +358,15 @@ impl MyCompositor {
312 358 fractional_scale_manager_state,
313 359 pointer_constraints_state,
314 360 dnd_icon: None,
361 + minimized: Vec::new(),
315 362 dmabuf_state: DmabufState::new(),
316 363 dmabuf_global: None,
317 364 layer_shell_state,
318 365 screencopy_queue: Vec::new(),
366 + xwayland_shell_state,
367 + xwm: None,
368 + xdisplay: None,
369 + snap_config: crate::snap::SnapConfig::from_env(),
319 370 #[cfg(feature = "udev")]
320 371 udev: None,
321 372 next_cascade,
@@ -371,6 +422,78 @@ impl MyCompositor {
371 422 }
372 423 }
373 424
425 + /// Minimalizuje okno: chowa je z przestrzeni i odkłada na listę schowanych.
426 + ///
427 + /// PO CO `unmap_elem`, a nie przesunięcie za ekran: okno ma zniknąć zarówno
428 + /// z kompozycji, jak i z trafień myszy/fokusa. Okno poza `space` nie jest
429 + /// rysowane ani nie łapie kliknięć.
430 + pub fn minimize_window(&mut self, window: &PaganWindow) {
431 + // Powtórna minimalizacja nie może zdublować wpisu na liście.
432 + if self.minimized.iter().any(|entry| &entry.window == window) {
433 + return;
434 + }
435 + let Some(location) = self.space.element_location(window) else {
436 + // Okno nie jest zmapowane — nie ma czego chować.
437 + return;
438 + };
439 +
440 + self.space.unmap_elem(window);
441 + self.minimized.push(MinimizedWindow {
442 + window: window.clone(),
443 + location,
444 + });
445 +
446 + // Fokus nie może wskazywać na okno, którego nie ma już w przestrzeni.
447 + let serial = SCOUNTER.next_serial();
448 + self.focus_top_window(serial);
449 + }
450 +
451 + /// Przywraca ostatnio zminimalizowane okno (stos LIFO).
452 + ///
453 + /// PO CO LIFO: to naturalna kolejność dla użytkownika — ostatnio schowane
454 + /// wraca pierwsze, jak zdejmowanie ze stosu kartek.
455 + pub fn restore_last_minimized(&mut self) {
456 + let serial = SCOUNTER.next_serial();
457 + // Pomijamy okna, które klient zdążył zamknąć, gdy były schowane.
458 + while let Some(entry) = self.minimized.pop() {
459 + if !entry.window.alive() {
460 + continue;
461 + }
462 + self.space
463 + .map_element(entry.window.clone(), entry.location, true);
464 + self.raise_and_focus(&entry.window, serial);
465 + return;
466 + }
467 + tracing::info!("brak zminimalizowanych okien do przywrócenia");
468 + }
469 +
470 + /// Nadaje fokus oknu z wierzchu stosu (albo czyści fokus, gdy nic nie zostało).
471 + fn focus_top_window(&mut self, serial: Serial) {
472 + // `elements()` idzie od spodu do wierzchu, więc ostatni element = wierzch.
473 + // Kolekcjonujemy od razu do `Option`, żeby nie trzymać pożyczki `space`
474 + // podczas mutującego `raise_and_focus`.
475 + let top = self.space.elements().next_back().cloned();
476 + match top {
477 + Some(window) => self.raise_and_focus(&window, serial),
478 + None => {
479 + if let Some(keyboard) = self.seat.get_keyboard() {
480 + keyboard.set_focus(self, Option::<KeyboardFocusTarget>::None, serial);
481 + }
482 + }
483 + }
484 + }
485 +
486 + /// Zamienia surowe okno Smithaya na nasze okno z cieniem (`PaganWindow`).
487 + ///
488 + /// PO CO: fokus klawiatury trzyma `Window` (tak wymaga Smithay), a operacje
489 + /// typu minimalizacja potrzebują wrappera `PaganWindow` obecnego w `space`.
490 + pub fn pagan_window_for(&self, window: &Window) -> Option<PaganWindow> {
491 + self.space
492 + .elements()
493 + .find(|candidate| &candidate.0 == window)
494 + .cloned()
495 + }
496 +
374 497 /// Prosi klienta o zmianę rozmiaru (poprzez nowy configure xdg-toplevel).
375 498 ///
376 499 /// PO CO przez configure, a nie bezpośrednio: w hybrydowym CSD to klient
@@ -627,6 +750,24 @@ impl CompositorHandler for MyCompositor {
627 750 }
628 751 }
629 752
753 + // Jeśli commituje powierzchnia ikony przeciągania (DnD), aktualizujemy jej
754 + // przesunięcie. Klient może przesuwać ikonę względem kursora (`buffer_delta`),
755 + // np. gdy przeciąga się kolumnę tabeli — chcemy to odwzorować.
756 + if let Some(icon) = self.dnd_icon.as_mut() {
757 + if &icon.surface == surface {
758 + let buffer_delta = with_states(surface, |states| {
759 + states
760 + .cached_state
761 + .get::<SurfaceAttributes>()
762 + .current()
763 + .buffer_delta
764 + .take()
765 + })
766 + .unwrap_or_default();
767 + icon.offset += buffer_delta;
768 + }
769 + }
770 +
630 771 self.popups.commit(surface);
631 772 }
632 773 }
@@ -861,9 +1002,12 @@ impl XdgShellHandler for MyCompositor {
861 1002 surface.send_pending_configure();
862 1003 }
863 1004
864 - fn minimize_request(&mut self, _surface: ToplevelSurface) {
865 - // Minimalizacja (unmap) nie jest jeszcze zaimplementowana. Świadomie
866 - // nie robimy nic, żeby nie zgubić okna bez mechanizmu przywracania.
1005 + fn minimize_request(&mut self, surface: ToplevelSurface) {
1006 + // Klient (np. nasz toolkit po kliknięciu przycisku) prosi o schowanie okna.
1007 + // Robimy to samo, co skrót `Ctrl+Alt+M` — jedno miejsce prawdy.
1008 + if let Some(window) = self.window_for_surface(surface.wl_surface()) {
1009 + self.minimize_window(&window);
1010 + }
867 1011 }
868 1012
869 1013 fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
@@ -168,6 +168,9 @@ pub fn run_udev() -> anyhow::Result<()> {
168 168 let mut state = MyCompositor::init(display, event_loop.handle(), true);
169 169 state.shm_state.update_formats(shm_formats);
170 170 state.enable_dmabuf(dmabuf_formats);
171 +
172 + // XWayland (aplikacje X11).
173 + state.start_xwayland();
171 174 state.attach_udev(UdevState {
172 175 session: session.clone(),
173 176 seat_name: seat_name.clone(),
@@ -667,48 +670,59 @@ impl MyCompositor {
667 670 return;
668 671 };
669 672
670 - // Kursor: aktualizujemy pozycję (fizyczną — tu skala 1).
671 - let pointer_location = self.pointer_location;
672 - let udev = match self.udev.as_mut() {
673 - Some(udev) => udev,
674 - None => return,
673 + let render_node = {
674 + let Some(udev) = self.udev.as_ref() else {
675 + return;
676 + };
677 + udev.backends
678 + .get(&node)
679 + .and_then(|device| device.render_node)
680 + .unwrap_or(udev.primary_gpu)
675 681 };
676 - udev.pointer_element
677 - .update(render::pointer_physical(pointer_location), true);
678 682
679 - let udev = match self.udev.as_mut() {
680 - Some(udev) => udev,
681 - None => return,
683 + let pointer_location = self.pointer_location;
684 + let cursor_status = self.cursor_status.clone();
685 +
686 + let Some(udev) = self.udev.as_mut() else {
687 + return;
682 688 };
683 689
684 - let render_node = udev
685 - .backends
686 - .get(&node)
687 - .and_then(|device| device.render_node)
688 - .unwrap_or(udev.primary_gpu);
690 + // Kursor WYJMUJEMY na czas renderu: `render_elements` potrzebuje `&mut`,
691 + // a jednocześnie pożyczamy `udev.gpus` i `udev.backends` — wyjęcie elementu
692 + // rozplątuje te pożyczki bez walki z borrow checkerem.
693 + let mut pointer_element = std::mem::take(&mut udev.pointer_element);
694 + pointer_element.update(render::pointer_physical(pointer_location), true);
695 + pointer_element.set_status(cursor_status);
689 696
690 697 let mut renderer = match udev.gpus.single_renderer(&render_node) {
691 698 Ok(renderer) => renderer,
692 699 Err(err) => {
693 700 warn!(?err, "brak renderera podczas renderowania");
701 + udev.pointer_element = pointer_element;
694 702 return;
695 703 }
696 704 };
697 705
698 - let custom_elements = udev.pointer_element.render_elements();
699 -
700 706 let Some(device) = udev.backends.get_mut(&node) else {
707 + udev.pointer_element = pointer_element;
701 708 return;
702 709 };
703 710 let Some(surface) = device.surfaces.get_mut(&crtc) else {
711 + udev.pointer_element = pointer_element;
704 712 return;
705 713 };
706 714
707 715 // Nasz renderer jest zbudowany na GlesRenderer (pixel shadery cienia), a
708 716 // `MultiRenderer` udostępnia wewnętrzny renderer przez `AsMut`.
709 717 let gles: &mut GlesRenderer = renderer.as_mut();
710 - let (elements, clear_color) =
711 - render::output_elements(&self.space, &output, custom_elements, gles);
718 + let (elements, clear_color) = render::output_elements(
719 + &self.space,
720 + &output,
721 + Some(&mut pointer_element),
722 + self.dnd_icon.as_ref(),
723 + pointer_location,
724 + gles,
725 + );
712 726
713 727 match surface
714 728 .drm_output
@@ -723,6 +737,9 @@ impl MyCompositor {
723 737 }
724 738 Err(err) => warn!(?err, "błąd renderowania wyjścia"),
725 739 }
740 +
741 + // Oddajemy kursor z powrotem do stanu backendu.
742 + udev.pointer_element = pointer_element;
726 743 }
727 744
728 745 /// Po VBlank: kończymy klatkę i planujemy kolejny render.
@@ -62,6 +62,20 @@ impl PaganWindow {
62 62 PaganWindow(Window::new_wayland_window(toplevel))
63 63 }
64 64
65 + /// Tworzy element przestrzeni z okna X11 (XWayland).
66 + ///
67 + /// PO CO osobny konstruktor: aplikacje X11 są zarządzane przez nasz `X11Wm`,
68 + /// a nie przez xdg-shell — ale w przestrzeni i w renderowaniu zachowują się
69 + /// identycznie (XWayland daje każdemu oknu `wl_surface`).
70 + pub fn new_x11(surface: smithay::xwayland::X11Surface) -> Self {
71 + PaganWindow(Window::new_x11_window(surface))
72 + }
73 +
74 + /// Powierzchnia X11 tego okna (jeśli to okno X11 — z XWayland).
75 + pub fn x11_surface(&self) -> Option<&smithay::xwayland::X11Surface> {
76 + self.0.x11_surface()
77 + }
78 +
65 79 /// Powierzchnia Waylanda tego okna.
66 80 pub fn wl_surface(&self) -> Option<std::borrow::Cow<'_, WlSurface>> {
67 81 self.0.wl_surface()
@@ -81,6 +81,9 @@ pub fn run_winit() -> anyhow::Result<()> {
81 81 .update_formats(backend.renderer().shm_formats());
82 82 state.enable_dmabuf(backend.renderer().dmabuf_formats());
83 83
84 + // XWayland (aplikacje X11). Wołane po `init`, bo wstawia źródło do pętli.
85 + state.start_xwayland();
86 +
84 87 if let Some(name) = state.socket_name.clone() {
85 88 info!(socket = %name, "PaganDE gotowy — ustaw WAYLAND_DISPLAY, aby podłączyć klienta");
86 89 // Wypisujemy na stdout, żeby łatwo podstawić do uruchomienia demo toolkitu.
@@ -124,16 +127,22 @@ pub fn run_winit() -> anyhow::Result<()> {
124 127 {
125 128 let pointer_physical = render::pointer_physical(state.pointer_location);
126 129 pointer_element.update(pointer_physical, true);
127 - let cursor_elements = pointer_element.render_elements();
130 + // Kształt kursora pochodzi z `SeatHandler::cursor_image` (klient albo my).
131 + pointer_element.set_status(state.cursor_status.clone());
128 132
129 133 let age = backend.buffer_age().unwrap_or(0);
130 134 let space = &state.space;
135 + // Ikona DnD i pozycja kursora są tylko czytane w tym bloku.
136 + let dnd = state.dnd_icon.as_ref();
137 + let pointer_location = state.pointer_location;
131 138
132 139 let render_res = backend.bind().and_then(|(renderer, mut framebuffer)| {
133 140 render::render_output(
134 141 &output,
135 142 space,
136 - cursor_elements,
143 + Some(&mut pointer_element),
144 + dnd,
145 + pointer_location,
137 146 renderer,
138 147 &mut framebuffer,
139 148 &mut damage_tracker,
140 149 new file mode 100644
@@ -0,0 +1,453 @@
1 +//! XWayland — obsługa aplikacji X11 w kompozytorze Waylanda.
2 +//!
3 +//! PO CO osobny moduł: integracja X11 jest z natury odseparowana od reszty —
4 +//! uruchamiamy zewnętrzny serwer `Xwayland`, a sami występujemy jako **menedżer
5 +//! okien X11** (reparenting WM). Smithay daje nam do tego `X11Wm`, ale to my
6 +//! decydujemy, jak okna X11 trafiają do naszej przestrzeni, jak się zachowują
7 +//! i jak obsługujemy schowek.
8 +//!
9 +//! Przepływ:
10 +//! 1. `start_xwayland()` uruchamia `Xwayland` i wpina jego zdarzenia w calloop,
11 +//! 2. gdy serwer zgłosi `Ready`, startujemy `X11Wm` (`XwmHandler`),
12 +//! 3. każde okno X11 dostaje powiązaną `wl_surface` (przez `xwayland_shell`),
13 +//! dzięki czemu renderuje się i fokusuje **tak samo** jak okna Waylanda,
14 +//! 4. ruch/rozmiar/snapowanie obsługują te same graby co dla Waylanda
15 +//! (`grabs.rs` rozgałęzia się po typie powierzchni).
16 +//!
17 +//! WAŻNE: wymaga binarki `Xwayland` w `PATH`. Jeśli jej nie ma, kompozytor
18 +//! działa dalej — tylko bez aplikacji X11 (logujemy ostrzeżenie z instrukcją).
19 +
20 +use std::{cell::RefCell, os::unix::io::OwnedFd, process::Stdio};
21 +
22 +use smithay::{
23 + input::pointer::Focus,
24 + reexports::wayland_server::protocol::wl_surface::WlSurface,
25 + utils::{Logical, Point, Rectangle, SERIAL_COUNTER},
26 + wayland::{
27 + selection::{
28 + data_device::{
29 + clear_data_device_selection, current_data_device_selection_userdata,
30 + request_data_device_client_selection, set_data_device_selection,
31 + },
32 + primary_selection::{
33 + clear_primary_selection, current_primary_selection_userdata,
34 + request_primary_client_selection, set_primary_selection,
35 + },
36 + SelectionTarget,
37 + },
38 + xwayland_keyboard_grab::XWaylandKeyboardGrabHandler,
39 + xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
40 + },
41 + xwayland::{
42 + xwm::{Reorder, ResizeEdge as X11ResizeEdge, X11Window, XwmId},
43 + X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler,
44 + },
45 +};
46 +use tracing::{error, info, trace, warn};
47 +
48 +use crate::{
49 + focus::KeyboardFocusTarget,
50 + grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab},
51 + state::MyCompositor,
52 + window::PaganWindow,
53 +};
54 +
55 +/// Zapamiętany prostokąt okna X11 przed maksymalizacją — żeby móc je przywrócić.
56 +///
57 +/// PO CO w `user_data` powierzchni X11: stan musi przeżyć między zdarzeniami,
58 +/// a nie chcemy dodawać pól do `MyCompositor` zależnych od konkretnego okna.
59 +#[derive(Debug, Default)]
60 +struct OldGeometry(RefCell<Option<Rectangle<i32, Logical>>>);
61 +
62 +impl OldGeometry {
63 + fn save(&self, geometry: Rectangle<i32, Logical>) {
64 + *self.0.borrow_mut() = Some(geometry);
65 + }
66 +
67 + fn restore(&self) -> Option<Rectangle<i32, Logical>> {
68 + self.0.borrow_mut().take()
69 + }
70 +}
71 +
72 +impl MyCompositor {
73 + /// Znajduje nasz element przestrzeni odpowiadający oknu X11.
74 + ///
75 + /// PO CO po powierzchni, a nie po samym `X11Surface`: w przestrzeni trzymamy
76 + /// `PaganWindow`, a porównanie robimy po powiązanej `X11Surface` (tak jak anvil).
77 + fn element_for_x11(&self, window: &X11Surface) -> Option<PaganWindow> {
78 + self.space
79 + .elements()
80 + .find(|element| matches!(element.x11_surface(), Some(w) if w == window))
81 + .cloned()
82 + }
83 +
84 + /// Uruchamia XWayland i wpina obsługę jego zdarzeń w pętlę główną.
85 + ///
86 + /// PO CO osobna metoda, wołana przez backendy: tworzy źródło calloop, więc
87 + /// musi mieć `LoopHandle` — tak samo jak `MyCompositor::init`.
88 + ///
89 + /// Gdy w systemie nie ma binarki `Xwayland`, wypisujemy instrukcję i wracamy:
90 + /// kompozytor działa dalej, tylko bez aplikacji X11 (świadome, miękkie
91 + /// zachowanie — brak XWaylanda nie powinien blokować startu sesji).
92 + pub fn start_xwayland(&mut self) {
93 + let (xwayland, client) = match XWayland::spawn(
94 + &self.display_handle,
95 + None,
96 + std::iter::empty::<(String, String)>(),
97 + true,
98 + Stdio::null(),
99 + Stdio::null(),
100 + |_| (),
101 + ) {
102 + Ok(pair) => pair,
103 + Err(err) => {
104 + warn!(
105 + %err,
106 + "nie udało się uruchomić XWayland — aplikacje X11 będą niedostępne. \
107 + Zainstaluj binarkę `Xwayland` (PaganOS: `sudo pag install xorg-xwayland`, \
108 + Arch/Manjaro: `sudo pacman -S xorg-xwayland`)."
109 + );
110 + return;
111 + }
112 + };
113 +
114 + let ret = self.handle.clone().insert_source(
115 + xwayland,
116 + move |event, _, data: &mut MyCompositor| match event {
117 + XWaylandEvent::Ready {
118 + x11_socket,
119 + display_number,
120 + } => match X11Wm::start_wm(data.handle.clone(), x11_socket, client.clone()) {
121 + Ok(wm) => {
122 + data.xwm = Some(wm);
123 + data.xdisplay = Some(display_number);
124 + info!(display = display_number, "XWayland gotowy (DISPLAY=:N)");
125 + }
126 + Err(err) => warn!(%err, "nie udało się wystartować menedżera okien X11"),
127 + },
128 + XWaylandEvent::Error => warn!("XWayland nie wystartował"),
129 + },
130 + );
131 +
132 + if let Err(err) = ret {
133 + error!(%err, "nie udało się dodać XWayland do pętli zdarzeń");
134 + }
135 + }
136 +}
137 +
138 +impl XWaylandShellHandler for MyCompositor {
139 + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
140 + &mut self.xwayland_shell_state
141 + }
142 +}
143 +
144 +impl XWaylandKeyboardGrabHandler for MyCompositor {
145 + /// Na czyj cel fokusa klawiatury zamienić powierzchnię XWaylanda, gdy ta
146 + /// prosi o grab (np. gra przechwytująca klawiaturę).
147 + fn keyboard_focus_for_xsurface(&self, surface: &WlSurface) -> Option<KeyboardFocusTarget> {
148 + let window = self.window_for_surface(surface)?;
149 + Some(KeyboardFocusTarget::Window(window.0.clone()))
150 + }
151 +}
152 +
153 +impl XwmHandler for MyCompositor {
154 + fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
155 + self.xwm
156 + .as_mut()
157 + .expect("XwmHandler wołane bez uruchomionego X11Wm")
158 + }
159 +
160 + fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
161 +
162 + fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
163 +
164 + fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
165 + // Dopiero `set_mapped(true)` czyni okno widocznym — jako WM mamy kontrolę.
166 + if let Err(err) = window.set_mapped(true) {
167 + warn!(%err, "X11: nie udało się zmapować okna");
168 + return;
169 + }
170 +
171 + let element = PaganWindow::new_x11(window.clone());
172 + // Ta sama polityka co dla okien Waylanda: kaskada na monitorze pod kursorem.
173 + self.place_new_window(element.clone());
174 +
175 + // Dopasuj okno X11 do nadanej geometrii (pozycja + jego własny rozmiar).
176 + if let Some(bbox) = self.space.element_bbox(&element) {
177 + let _ = window.configure(bbox);
178 + }
179 + trace!(title = %window.title(), "X11: zmapowano okno");
180 + }
181 +
182 + fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
183 + // Okien override-redirect (menu, tooltipy, popupy) NIE zarządzamy —
184 + // odtwarzamy tylko ich własną pozycję.
185 + let location = window.geometry().loc;
186 + self.space
187 + .map_element(PaganWindow::new_x11(window), location, true);
188 + }
189 +
190 + fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
191 + if let Some(element) = self.element_for_x11(&window) {
192 + self.space.unmap_elem(&element);
193 + }
194 + if !window.is_override_redirect() {
195 + let _ = window.set_mapped(false);
196 + }
197 + }
198 +
199 + fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
200 +
201 + fn configure_request(
202 + &mut self,
203 + _xwm: XwmId,
204 + window: X11Surface,
205 + _x: Option<i32>,
206 + _y: Option<i32>,
207 + w: Option<u32>,
208 + h: Option<u32>,
209 + _reorder: Option<Reorder>,
210 + ) {
211 + // Akceptujemy tylko rozmiar — pozycją zarządza kompozytor.
212 + let mut geometry = window.geometry();
213 + if let Some(w) = w {
214 + geometry.size.w = w as i32;
215 + }
216 + if let Some(h) = h {
217 + geometry.size.h = h as i32;
218 + }
219 + let _ = window.configure(geometry);
220 + }
221 +
222 + fn configure_notify(
223 + &mut self,
224 + _xwm: XwmId,
225 + window: X11Surface,
226 + geometry: Rectangle<i32, Logical>,
227 + _above: Option<X11Window>,
228 + ) {
229 + if let Some(element) = self.element_for_x11(&window) {
230 + self.space.map_element(element, geometry.loc, false);
231 + }
232 + }
233 +
234 + fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
235 + let Some(element) = self.element_for_x11(&window) else {
236 + return;
237 + };
238 + let Some(old_geometry) = self.space.element_bbox(&element) else {
239 + return;
240 + };
241 +
242 + let output = self
243 + .space
244 + .outputs_for_element(&element)
245 + .first()
246 + .cloned()
247 + .or_else(|| self.space.outputs().next().cloned());
248 + let Some(output) = output else { return };
249 + let Some(area) = self.space.output_geometry(&output) else {
250 + return;
251 + };
252 +
253 + let _ = window.set_maximized(true);
254 + let _ = window.configure(area);
255 + window.user_data().insert_if_missing(OldGeometry::default);
256 + if let Some(data) = window.user_data().get::<OldGeometry>() {
257 + data.save(old_geometry);
258 + }
259 + self.space.map_element(element, area.loc, false);
260 + }
261 +
262 + fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
263 + let Some(element) = self.element_for_x11(&window) else {
264 + return;
265 + };
266 + let _ = window.set_maximized(false);
267 + if let Some(old) = window
268 + .user_data()
269 + .get::<OldGeometry>()
270 + .and_then(|d| d.restore())
271 + {
272 + let _ = window.configure(old);
273 + self.space.map_element(element, old.loc, false);
274 + }
275 + }
276 +
277 + fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
278 + let Some(element) = self.element_for_x11(&window) else {
279 + return;
280 + };
281 + let output = self
282 + .space
283 + .outputs_for_element(&element)
284 + .first()
285 + .cloned()
286 + .or_else(|| self.space.outputs().next().cloned());
287 + let Some(output) = output else { return };
288 + let Some(area) = self.space.output_geometry(&output) else {
289 + return;
290 + };
291 +
292 + let _ = window.set_fullscreen(true);
293 + let _ = window.configure(area);
294 + self.space.map_element(element, area.loc, false);
295 + }
296 +
297 + fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
298 + let Some(element) = self.element_for_x11(&window) else {
299 + return;
300 + };
301 + let _ = window.set_fullscreen(false);
302 + if let Some(area) = self.space.element_bbox(&element) {
303 + let _ = window.configure(area);
304 + }
305 + }
306 +
307 + fn resize_request(
308 + &mut self,
309 + _xwm: XwmId,
310 + window: X11Surface,
311 + _button: u32,
312 + edges: X11ResizeEdge,
313 + ) {
314 + let Some(start_data) = self.pointer.grab_start_data() else {
315 + return;
316 + };
317 + let Some(element) = self.element_for_x11(&window) else {
318 + return;
319 + };
320 + let geometry = element.geometry();
321 + let location = self
322 + .space
323 + .element_location(&element)
324 + .unwrap_or_else(|| Point::from((0, 0)));
325 +
326 + let grab = PointerResizeSurfaceGrab {
327 + start_data,
328 + window: element,
329 + edges: edges.into(),
330 + initial_window_location: location,
331 + initial_window_size: geometry.size,
332 + last_window_size: geometry.size,
333 + };
334 + let pointer = self.pointer.clone();
335 + pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
336 + }
337 +
338 + fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) {
339 + let Some(start_data) = self.pointer.grab_start_data() else {
340 + return;
341 + };
342 + let Some(element) = self.element_for_x11(&window) else {
343 + return;
344 + };
345 + let mut initial_window_location = self
346 + .space
347 + .element_location(&element)
348 + .unwrap_or_else(|| Point::from((0, 0)));
349 +
350 + // Przeciągnięcie zmaksymalizowanego okna X11 najpierw je przywraca
351 + // (spójnie z zachowaniem okien Waylanda).
352 + if window.is_maximized() {
353 + let _ = window.set_maximized(false);
354 + let pos = self.pointer.current_location();
355 + initial_window_location = Point::from((pos.x as i32, pos.y as i32));
356 + if let Some(old) = window
357 + .user_data()
358 + .get::<OldGeometry>()
359 + .and_then(|d| d.restore())
360 + {
361 + let _ = window.configure(Rectangle::new(initial_window_location, old.size));
362 + }
363 + }
364 +
365 + let initial_window_size = window.geometry().size;
366 + let grab = PointerMoveSurfaceGrab {
367 + start_data,
368 + window: element,
369 + initial_window_location,
370 + initial_window_size,
371 + current_snap: None,
372 + };
373 + let pointer = self.pointer.clone();
374 + pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
375 + }
376 +
377 + // ── Schowek między X11 a Waylandem ──
378 + // Bez tego kopiuj/wklej między aplikacją X11 a Waylandem nie działa.
379 +
380 + fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
381 + // Zezwalamy tylko, gdy fokus klawiatury należy do OKNA X11 tego samego XWM.
382 + if let Some(keyboard) = self.seat.get_keyboard() {
383 + if let Some(KeyboardFocusTarget::Window(window)) = keyboard.current_focus() {
384 + if let Some(surface) = window.x11_surface() {
385 + return surface.xwm_id() == Some(xwm);
386 + }
387 + }
388 + }
389 + false
390 + }
391 +
392 + fn send_selection(
393 + &mut self,
394 + _xwm: XwmId,
395 + selection: SelectionTarget,
396 + mime_type: String,
397 + fd: OwnedFd,
398 + ) {
399 + match selection {
400 + SelectionTarget::Clipboard => {
401 + if let Err(err) = request_data_device_client_selection(&self.seat, mime_type, fd) {
402 + error!(?err, "X11: nie udało się pobrać schowka Waylanda");
403 + }
404 + }
405 + SelectionTarget::Primary => {
406 + if let Err(err) = request_primary_client_selection(&self.seat, mime_type, fd) {
407 + error!(
408 + ?err,
409 + "X11: nie udało się pobrać selekcji pierwotnej Waylanda"
410 + );
411 + }
412 + }
413 + }
414 + }
415 +
416 + fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
417 + trace!(?selection, ?mime_types, "X11: nowa selekcja");
418 + match selection {
419 + SelectionTarget::Clipboard => {
420 + set_data_device_selection(&self.display_handle, &self.seat, mime_types, ())
421 + }
422 + SelectionTarget::Primary => {
423 + set_primary_selection(&self.display_handle, &self.seat, mime_types, ())
424 + }
425 + }
426 + }
427 +
428 + fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
429 + match selection {
430 + SelectionTarget::Clipboard => {
431 + if current_data_device_selection_userdata(&self.seat).is_some() {
432 + clear_data_device_selection(&self.display_handle, &self.seat)
433 + }
434 + }
435 + SelectionTarget::Primary => {
436 + if current_primary_selection_userdata(&self.seat).is_some() {
437 + clear_primary_selection(&self.display_handle, &self.seat)
438 + }
439 + }
440 + }
441 + }
442 +
443 + fn disconnected(&mut self, _xwm: XwmId) {
444 + warn!("XWayland: utracono połączenie z serwerem X11");
445 + self.xwm = None;
446 + self.xdisplay = None;
447 + }
448 +}
449 +
450 +// Delegacje protokołów: `xwayland_shell` (skojarzenie okna X11 z wl_surface)
451 +// oraz grab klawiatury X11.
452 +smithay::delegate_xwayland_shell!(MyCompositor);
453 +smithay::delegate_xwayland_keyboard_grab!(MyCompositor);
0 454 new file mode 100644
@@ -0,0 +1,213 @@
1 +#!/usr/bin/env python3
2 +"""Narzędzie DEV: podglądanie okien X11 (geometria, nazwa, stan).
3 +
4 +PO CO: kompozytor w trybie `winit` jest zwykłym oknem X11. Żeby automatycznie
5 +przetestować renderowanie kursora albo interakcje, trzeba wiedzieć GDZIE to okno
6 +jest na ekranie i przeliczyć współrzędne ekranu → współrzędne kompozytora.
7 +`xwininfo`/`xdotool` bywają niedostępne, a ten skrypt korzysta tylko z libX11.
8 +
9 +Użycie:
10 +
11 + python3 tools/x11_probe.py list # wypisz widoczne okna + geometrię
12 + python3 tools/x11_probe.py find 1280 800 # znajdź okno o dokładnym rozmiarze
13 +
14 +Skrypt szuka wyłącznie okien potomnych korzenia (top-level), co w praktyce
15 +wystarcza: okno `winit` jest właśnie takie.
16 +"""
17 +
18 +import ctypes
19 +import ctypes.util
20 +
21 +x11 = ctypes.CDLL(ctypes.util.find_library("X11"))
22 +# WAŻNE: dla każdej funkcji ustawiamy `argtypes`/`restype`. Bez tego ctypes
23 +# potraktuje wskaźnik Display i 64-bitowe id okna jak zwykłe inty i obetnie je
24 +# do 32 bitów — dostaniemy segfault albo pustą listę okien.
25 +x11.XOpenDisplay.restype = ctypes.c_void_p
26 +x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
27 +x11.XDefaultRootWindow.restype = ctypes.c_ulong
28 +x11.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
29 +x11.XQueryTree.argtypes = [
30 + ctypes.c_void_p,
31 + ctypes.c_ulong,
32 + ctypes.POINTER(ctypes.c_ulong),
33 + ctypes.POINTER(ctypes.c_ulong),
34 + ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)),
35 + ctypes.POINTER(ctypes.c_uint),
36 +]
37 +x11.XFetchName.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char_p)]
38 +x11.XFree.argtypes = [ctypes.c_void_p]
39 +
40 +# XWindowAttributes — bierzemy tylko pola, których potrzebujemy. Układ musi
41 +# odpowiadać nagłówkowi Xlib.h; dopełniamy buforem, bo interesuje nas początek
42 +# struktury (x, y, width, height, border_width, depth, ...).
43 +class Attr(ctypes.Structure):
44 + _fields_ = [
45 + ("x", ctypes.c_int),
46 + ("y", ctypes.c_int),
47 + ("width", ctypes.c_int),
48 + ("height", ctypes.c_int),
49 + ("border_width", ctypes.c_int),
50 + ("depth", ctypes.c_int),
51 + ("visual", ctypes.c_void_p),
52 + ("root", ctypes.c_ulong),
53 + ("class_", ctypes.c_int),
54 + ("bit_gravity", ctypes.c_int),
55 + ("win_gravity", ctypes.c_int),
56 + ("backing_store", ctypes.c_int),
57 + ("backing_planes", ctypes.c_ulong),
58 + ("backing_pixel", ctypes.c_ulong),
59 + ("save_under", ctypes.c_int),
60 + ("colormap", ctypes.c_ulong),
61 + ("map_installed", ctypes.c_int),
62 + ("map_state", ctypes.c_int),
63 + ("all_event_masks", ctypes.c_long),
64 + ("your_event_mask", ctypes.c_long),
65 + ("do_not_propagate_mask", ctypes.c_long),
66 + ("override_redirect", ctypes.c_int),
67 + ("screen", ctypes.c_void_p),
68 + ]
69 +
70 +
71 +_display = None
72 +_root = None
73 +
74 +
75 +def open_display():
76 + global _display, _root
77 + _display = x11.XOpenDisplay(None)
78 + if not _display:
79 + raise SystemExit("nie mogę otworzyć DISPLAY (ustaw DISPLAY=:N)")
80 + _root = x11.XDefaultRootWindow(_display)
81 +
82 +
83 +def children(win):
84 + """Zwraca listę okien potomnych w kolejności stosu (od spodu do wierzchu)."""
85 + root_return = ctypes.c_ulong()
86 + parent_return = ctypes.c_ulong()
87 + arr = ctypes.POINTER(ctypes.c_ulong)()
88 + count = ctypes.c_uint()
89 + x11.XQueryTree(
90 + _display,
91 + ctypes.c_ulong(win),
92 + ctypes.byref(root_return),
93 + ctypes.byref(parent_return),
94 + ctypes.byref(arr),
95 + ctypes.byref(count),
96 + )
97 + result = [arr[i] for i in range(count.value)]
98 + if arr:
99 + x11.XFree(arr)
100 + return result
101 +
102 +
103 +def attributes(win):
104 + x11.XGetWindowAttributes.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(Attr)]
105 + x11.XGetWindowAttributes.restype = ctypes.c_int
106 + attr = Attr()
107 + ok = x11.XGetWindowAttributes(_display, ctypes.c_ulong(win), ctypes.byref(attr))
108 + return attr if ok else None
109 +
110 +
111 +def name(win):
112 + ptr = ctypes.c_char_p()
113 + if x11.XFetchName(_display, ctypes.c_ulong(win), ctypes.byref(ptr)) and ptr.value:
114 + title = ptr.value.decode("utf-8", "replace")
115 + x11.XFree(ptr)
116 + return title
117 + return ""
118 +
119 +
120 +def main(argv):
121 + open_display()
122 + cmd = argv[1] if len(argv) > 1 else "list"
123 +
124 + if cmd == "list":
125 + for win in children(_root):
126 + attr = attributes(win)
127 + if not attr or attr.map_state != 2: # 2 = IsViewable
128 + continue
129 + # Interesują nas okna o sensownym rozmiarze (pomijamy 1×1 pomocnicze).
130 + if attr.width < 50 or attr.height < 50:
131 + continue
132 + print(
133 + f"0x{win:x} {attr.width}x{attr.height}+{attr.x}+{attr.y} "
134 + f"title={name(win)!r}"
135 + )
136 + elif cmd == "find":
137 + want_w, want_h = int(argv[2]), int(argv[3])
138 +
139 + def walk(win, ox, oy):
140 + attr = attributes(win)
141 + if not attr or attr.map_state != 2:
142 + return None
143 + ax, ay = ox + attr.x, oy + attr.y
144 + if attr.width == want_w and attr.height == want_h:
145 + return (ax, ay)
146 + # Schodzimy w głąb — okno klienta bywa dzieckiem ramki menedżera okien.
147 + for child in children(win):
148 + found = walk(child, ax, ay)
149 + if found:
150 + return found
151 + return None
152 +
153 + result = walk(_root, 0, 0)
154 + if result:
155 + print(f"{result[0]} {result[1]}")
156 + return
157 + raise SystemExit(f"nie znaleziono widocznego okna {want_w}x{want_h}")
158 + elif cmd == "tree":
159 + def dump(win, ox, oy, depth):
160 + attr = attributes(win)
161 + if not attr or attr.map_state != 2:
162 + return
163 + ax, ay = ox + attr.x, oy + attr.y
164 + print(
165 + " " * depth
166 + + f"0x{win:x} {attr.width}x{attr.height}+{ax}+{ay} title={name(win)!r}"
167 + )
168 + for child in children(win):
169 + dump(child, ax, ay, depth + 1)
170 +
171 + for top in children(_root):
172 + dump(top, 0, 0, 0)
173 + elif cmd == "pointer":
174 + # Bieżąca pozycja kursora X (bezwzględna). Przydaje się, by sprawdzić,
175 + # czy wstrzyknięcie myszy przez XTest faktycznie zadziałało.
176 + root_ret = ctypes.c_ulong()
177 + child_ret = ctypes.c_ulong()
178 + rx = ctypes.c_int()
179 + ry = ctypes.c_int()
180 + wx = ctypes.c_int()
181 + wy = ctypes.c_int()
182 + mask = ctypes.c_uint()
183 + x11.XQueryPointer.argtypes = [
184 + ctypes.c_void_p,
185 + ctypes.c_ulong,
186 + ctypes.POINTER(ctypes.c_ulong),
187 + ctypes.POINTER(ctypes.c_ulong),
188 + ctypes.POINTER(ctypes.c_int),
189 + ctypes.POINTER(ctypes.c_int),
190 + ctypes.POINTER(ctypes.c_int),
191 + ctypes.POINTER(ctypes.c_int),
192 + ctypes.POINTER(ctypes.c_uint),
193 + ]
194 + x11.XQueryPointer(
195 + _display,
196 + ctypes.c_ulong(_root),
197 + ctypes.byref(root_ret),
198 + ctypes.byref(child_ret),
199 + ctypes.byref(rx),
200 + ctypes.byref(ry),
201 + ctypes.byref(wx),
202 + ctypes.byref(wy),
203 + ctypes.byref(mask),
204 + )
205 + print(f"{rx.value} {ry.value} child=0x{child_ret.value:x}")
206 + else:
207 + raise SystemExit(__doc__)
208 +
209 +
210 +if __name__ == "__main__":
211 + import sys
212 +
213 + main(sys.argv)