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)