← PaganDE

Commit 8089421

6
plików
+269
dodanych
-58
usuniętych
@@ -12,9 +12,12 @@
12 12
13 13 ## Filozofia: spójne dekoracje serwerowe (SSD)
14 14
15 -Kompozytor rysuje pasek tytułu dla **każdego** okna i wymusza tryb serwerowy
16 -(`xdg-decoration` → `ServerSide`). Obce aplikacje (GTK, X11) nie rysują wtedy
17 -swoich dekoracji, więc cała sesja wygląda jednolicie.
15 +Kompozytor rysuje pasek tytułu dla okien, które oddają mu dekoracje, i wymusza
16 +tryb serwerowy (`xdg-decoration` → `ServerSide`). Dotyczy to naszego toolkitu,
17 +Qt, GTK4 oraz WSZYSTKICH aplikacji X11 (X11 nie zna CSD). Wyjątkiem są klienty,
18 +które tego protokołu nie znają (GTK3) — one rysują własne dekoracje, więc nie
19 +dokładamy drugiego paska. Szczegóły i obejście dla GTK:
20 +[Dekoracje serwerowe (SSD)](#dekoracje-serwerowe-ssd-i-menu-aplikacji-w-pasku).
18 21
19 22 Menu aplikacji jest jedynym elementem, którego kompozytor nie zna z góry —
20 23 aplikacje mają różne menu. Rozwiązuje to **własny protokół** `pagan_app_menu_v1`:
@@ -141,9 +144,22 @@ ręcznie. Kolory to `#RRGGBB` / `#RRGGBBAA`. Implementacja: `src/theme.rs`
141 144 ### Dekoracje serwerowe (SSD) i menu aplikacji w pasku
142 145
143 146 Kompozytor **wymusza** tryb dekoracji serwerowych (`xdg-decoration` → `ServerSide`)
144 -dla każdego okna najwyższego poziomu, żeby całe środowisko wyglądało spójnie —
145 -niezależnie od tego, czy aplikacja jest Waylandowa, GTK, czy X11. Pasek jest
146 -płaski (bez gradientów), z zaokrąglonymi górnymi rogami i cieniem pod ramką.
147 +dla okien najwyższego poziomu, które ten protokół znają (nasz toolkit, Qt, GTK4).
148 +Pasek jest płaski (bez gradientów), z zaokrąglonymi górnymi rogami i cieniem pod ramką.
149 +
150 +Dwa świadome wyjątki:
151 +
152 +* **Klienty CSD (np. GTK3).** GTK3 na Waylandzie ZAWSZE rysuje własne dekoracje
153 +i nie tworzy obiektu `zxdg_toplevel_decoration_v1`. Nie da się na nim wymusić
154 +naszego paska, więc kompozytor NIE dokłada drugiego — inaczej oba paski
155 +nakładałyby się na siebie (wyglądało to jak „przesunięta dekoracja”). Aby GTK
156 +dostało NASZ pasek, launcher uruchamia je przez XWayland (patrz niżej).
157 +* **Okna `override-redirect`** (menu, podpowiedzi X11) to elementy aplikacji,
158 +a nie samodzielne okna — pasek nad nimi wyglądałby jak przyklejony do menu.
159 +
160 +Wysokości panelu nie zgadujemy: kompozytor czyta **obszar roboczy** wyjścia
161 +(`LayerMap::non_exclusive_zone`), więc kaskada nowych okien i maksymalizacja
162 +lądują POD panelem, a nie za nim.
147 163
148 164 W pasku, w **jednym rzędzie**, leżą: menu aplikacji (po lewej), tytuł okna
149 165 (wyśrodkowany, o ile się mieści) oraz przyciski minimalizuj/maksymalizuj/zamknij
@@ -212,12 +228,16 @@ w sesji XFCE ustawione jest `XDG_SESSION_TYPE=x11` i `DISPLAY=:0.0`, więc GUI
212 228 domyślnie wybiera X11 i ląduje na pulpicie XFCE — poza naszym kompozytorem
213 229 (a wtedy okno i jego dekoracja nie mają ze sobą nic wspólnego). Dlatego launcher:
214 230
215 -* wymusza `GDK_BACKEND=wayland` i `QT_QPA_PLATFORM=wayland`,
216 231 * ustawia `XDG_SESSION_TYPE=wayland`,
232 +* dla **GTK** wymusza `GDK_BACKEND=x11` (czyli NASZ XWayland). Powód: GTK3 nie
233 + zna `xdg-decoration` na Waylandzie, więc tylko na X11 dostaje nasz pasek SSD;
234 + nadpisz przez `PAGAN_GDK_BACKEND=wayland`, jeśli wolisz natywny Wayland,
235 +* dla **Qt** wymusza `QT_QPA_PLATFORM=wayland` (Qt zna `xdg-decoration` i sam
236 + rezygnuje z CSD, więc dostaje nasz pasek bez XWayland),
217 237 * podaje aplikacjom `DISPLAY` **naszego** XWayland, który kompozytor publikuje
218 238 w `$XDG_RUNTIME_DIR/pagan/xwayland-display`.
219 239
220 -Dzięki temu aplikacje Wayland i X11 trafiają do naszego kompozytora i dostają
240 +Dzięki temu i aplikacje Wayland, i X11 trafiają do naszego kompozytora i dostają
221 241 spójną dekorację SSD.
222 242
223 243 ### Testy interakcji bez machania myszką (dev)
@@ -13,13 +13,13 @@
13 13
14 14 use std::{collections::HashMap, path::PathBuf};
15 15
16 -use tracing::{info, warn};
16 +use tracing::{debug, info, warn};
17 17
18 18 use smithay::{
19 19 desktop::{layer_map_for_output, space::SpaceElement},
20 20 // UWAGA: `Scale` do wyjść to `smithay::output::Scale` (enum Integer/Fractional),
21 21 // a NIE `smithay::utils::Scale` (struktura x/y). To dwa różne typy o tej samej nazwie.
22 - output::Scale,
22 + output::{Output, Scale},
23 23 utils::{Logical, Point, Rectangle, Transform},
24 24 };
25 25
@@ -276,45 +276,84 @@ impl MyCompositor {
276 276 .cloned()
277 277 .or_else(|| self.space.outputs().next().cloned());
278 278
279 + // Używamy OBSZARU ROBOCZEGO (bez paska panelu rezerwującego miejsce).
280 + // Dzięki temu kaskada startuje POD panelem, a nasz pasek SSD jest widoczny.
279 281 let area = output
280 282 .as_ref()
281 - .and_then(|output| self.space.output_geometry(output))
283 + .and_then(|output| self.output_work_area(output))
282 284 .unwrap_or_else(|| Rectangle::from_size((1280, 720).into()));
283 285
284 286 let location = self.next_cascade_in(area);
285 287 let location = self.adjust_for_ssd(&window, location);
288 + debug!(
289 + ?area,
290 + ?location,
291 + step = ?self.next_cascade,
292 + ssd = window.wants_ssd(),
293 + negotiated = crate::window::decoration_negotiated(&window),
294 + "nowe okno: pozycja kaskady"
295 + );
286 296 self.space.map_element(window, location, true);
287 297 self.advance_cascade(area);
288 298 }
289 299
300 + /// Obszar wyjścia dostępny dla okien: pełna geometria MINUS strefy wyłączne
301 + /// warstw (`wlr-layer-shell`, np. panel rezerwujący 36 px u góry).
302 + ///
303 + /// PO CO: bez tego pasek SSD (rysowany NAD treścią) chowałby się pod panelem,
304 + /// co wygląda jak ucięta/przesunięta dekoracja. Smithay liczy ten prostokąt
305 + /// sam (`LayerMap::non_exclusive_zone`), więc nie zgadujemy wysokości panelu.
306 + pub(crate) fn output_work_area(&self, output: &Output) -> Option<Rectangle<i32, Logical>> {
307 + let zone = layer_map_for_output(output).non_exclusive_zone();
308 + if zone.size.w > 0 && zone.size.h > 0 {
309 + Some(zone)
310 + } else {
311 + // Brak warstw/rezerwacji — cały ekran jest obszarem roboczym.
312 + self.space.output_geometry(output)
313 + }
314 + }
315 +
316 + /// Obszar roboczy wyjścia zawierającego podany punkt (awaryjnie: pierwszego).
317 + fn work_area_for_point(&self, point: Point<i32, Logical>) -> Option<Rectangle<i32, Logical>> {
318 + let outputs: Vec<_> = self.space.outputs().cloned().collect();
319 + outputs
320 + .iter()
321 + .find(|output| {
322 + self.space
323 + .output_geometry(output)
324 + .is_some_and(|geometry| geometry.contains(point))
325 + })
326 + .and_then(|output| self.output_work_area(output))
327 + .or_else(|| {
328 + outputs
329 + .first()
330 + .and_then(|output| self.output_work_area(output))
331 + })
332 + }
333 +
290 334 /// Przesuwa pozycję okna tak, aby pasek tytułu (SSD) był widoczny.
291 335 ///
292 336 /// PO CO: w naszym modelu `location` to początek TREŚCI okna, a pasek SSD
293 - /// rysujemy NAD nią (`location.y - TITLEBAR_HEIGHT`). Bez tej korekty okno
294 - /// postawione w kaskadzie na (20,20) miałoby pasek poza ekranem.
337 + /// rysujemy NAD nią (`location.y - TITLEBAR_HEIGHT`). Przesuwamy treść poniżej
338 + /// GÓRNEJ KRAWĘDZI OBSZARU ROBOCZEGO, więc pasek ląduje pod panelem, a nie za nim.
295 339 pub(crate) fn adjust_for_ssd(
296 340 &self,
297 341 window: &PaganWindow,
298 342 mut location: Point<i32, Logical>,
299 343 ) -> Point<i32, Logical> {
300 344 if window.wants_ssd() {
301 - let top = self
302 - .space
303 - .outputs()
304 - .next()
305 - .and_then(|output| self.space.output_geometry(output))
306 - .map(|area| area.loc.y)
307 - .unwrap_or(0);
308 - location.y = location.y.max(top + TITLEBAR_HEIGHT);
345 + if let Some(area) = self.work_area_for_point(location) {
346 + location.y = location.y.max(area.loc.y + TITLEBAR_HEIGHT);
347 + }
309 348 }
310 349 location
311 350 }
312 351
313 - /// Zwraca prostokąty wszystkich wyjść.
352 + /// Zwraca prostokąty obszarów roboczych wszystkich wyjść.
314 353 fn output_areas(&self) -> Vec<Rectangle<i32, Logical>> {
315 354 self.space
316 355 .outputs()
317 - .filter_map(|output| self.space.output_geometry(output))
356 + .filter_map(|output| self.output_work_area(output))
318 357 .collect()
319 358 }
320 359
@@ -574,10 +574,10 @@ impl MyCompositor {
574 574 }
575 575 }
576 576
577 - /// Maksymalizuje okno (w pływającym WM = rozmiar wyjścia pod oknem).
577 + /// Maksymalizuje okno (w pływającym WM = obszar roboczy wyjścia pod oknem).
578 578 pub fn maximize_window(&mut self, window: &PaganWindow) {
579 579 if let Some(toplevel) = window.toplevel() {
580 - let Some(area) = self.output_area_for_window(window) else {
580 + let Some(area) = self.output_work_area_for_window(window) else {
581 581 return;
582 582 };
583 583 toplevel.with_pending_state(|state| {
@@ -653,6 +653,29 @@ impl MyCompositor {
653 653 })
654 654 }
655 655
656 + /// Prostokąt OBSZARU ROBOCZEGO wyjścia, na którym leży okno (bez paneli).
657 + ///
658 + /// PO CO osobno od `output_area_for_window`: pełny ekran ma zasłaniać panel,
659 + /// a maksymalizacja — nie (okno zmaksymalizowane nadal widzi pasek systemowy).
660 + fn output_work_area_for_window(&self, window: &PaganWindow) -> Option<Rectangle<i32, Logical>> {
661 + let location = self.space.element_location(window);
662 + let under = location.and_then(|location| {
663 + self.space.outputs().find_map(|output| {
664 + let geometry = self.space.output_geometry(output)?;
665 + geometry
666 + .contains(location)
667 + .then(|| self.output_work_area(output))
668 + .flatten()
669 + })
670 + });
671 + under.or_else(|| {
672 + self.space
673 + .outputs()
674 + .next()
675 + .and_then(|output| self.output_work_area(output))
676 + })
677 + }
678 +
656 679 /// Prosi klienta o zmianę rozmiaru (poprzez nowy configure xdg-toplevel).
657 680 ///
658 681 /// PO CO przez configure, a nie bezpośrednio: w hybrydowym CSD to klient
@@ -1008,12 +1031,14 @@ impl SeatHandler for MyCompositor {
1008 1031 set_data_device_focus(dh, seat, client.clone());
1009 1032 set_primary_focus(dh, seat, client);
1010 1033
1011 - // Wyróżnij wizualnie okno AKTYWNE (firmowy gradient + linia akcentu),
1012 - // a pozostałe przygaś. Bez tego wszystkie okna wyglądałyby identycznie.
1034 + // Wyróżnij wizualnie okno AKTYWNE (firmowy pasek aktywny + akcent),
1035 + // a pozostałe przygaś. Porównujemy po OKNIE, a nie po `wl_surface`:
1036 + // okno X11 (XWayland) może nie mieć jeszcze powiązanej powierzchni
1037 + // Waylanda w chwili nadawania fokusu, a mimo to jest aktywnym oknem.
1013 1038 for window in self.space.elements() {
1014 1039 let is_focused = matches!(
1015 - (window.wl_surface(), wl_surface.as_ref()),
1016 - (Some(surface), Some(target)) if surface.as_ref() == target
1040 + target,
1041 + Some(KeyboardFocusTarget::Window(focused)) if focused == &window.0
1017 1042 );
1018 1043 window.set_focused(is_focused);
1019 1044 }
@@ -1026,17 +1051,23 @@ impl SeatHandler for MyCompositor {
1026 1051
1027 1052 impl XdgDecorationHandler for MyCompositor {
1028 1053 fn new_decoration(&mut self, toplevel: ToplevelSurface) {
1029 - // DECYZJA: wymuszamy tryb SERVER-SIDE. To odwrócenie wcześniejszego
1030 - // "hybrydowego CSD" — od teraz to KOMPOZYTOR rysuje pasek tytułu
1031 - // (spójnie dla WSZYSTKICH aplikacji: GTK przestaje rysować swoje CSD,
1032 - // a X11 i tak nie ma dekoracji, więc dostaje nasze).
1054 + // DECYZJA: wymuszamy tryb SERVER-SIDE. Od tego momentu to KOMPOZYTOR
1055 + // rysuje pasek tytułu — spójnie dla aplikacji, które znają ten protokół
1056 + // (nasz toolkit, Qt, GTK4).
1033 1057 //
1034 - // Nasz własny toolkit nie tworzy obiektu dekoracji, więc tego nie
1035 - // negocjuje i zachowuje swoje CSD — dlatego krok 2 (toolkit) musi
1036 - // nastąpić, żeby nie było podwójnych pasków.
1058 + // Klienci, którzy NIE tworzą tego obiektu (np. GTK3), rysują własne CSD —
1059 + // dla nich `PaganWindow::wants_ssd` zwróci `false`, żeby nie nakładać
1060 + // dwóch pasków (patrz `mark_ssd_negotiated`).
1037 1061 toplevel.with_pending_state(|state| {
1038 1062 state.decoration_mode = Some(DecorationMode::ServerSide);
1039 1063 });
1064 + crate::window::mark_ssd_negotiated(&toplevel);
1065 + // Negocjacja potrafi przyjść PO pierwszym umieszczeniu okna w kaskadzie
1066 + // (klient tworzy obiekt dekoracji chwilę po pierwszym commicie). Wtedy
1067 + // okno stoi już tak, jakby paska nie było — dosuwamy je, żeby nasz pasek
1068 + // nie schował się pod panelem.
1069 + self.reposition_for_ssd(&toplevel);
1070 + tracing::debug!("dekoracja: klient negocjuje (new_decoration)");
1040 1071 }
1041 1072
1042 1073 fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
@@ -1044,6 +1075,7 @@ impl XdgDecorationHandler for MyCompositor {
1044 1075 toplevel.with_pending_state(|state| {
1045 1076 state.decoration_mode = Some(DecorationMode::ServerSide);
1046 1077 });
1078 + tracing::debug!("dekoracja: request_mode -> ServerSide");
1047 1079 }
1048 1080
1049 1081 fn unset_mode(&mut self, toplevel: ToplevelSurface) {
@@ -1054,6 +1086,26 @@ impl XdgDecorationHandler for MyCompositor {
1054 1086 }
1055 1087 }
1056 1088
1089 +impl MyCompositor {
1090 + /// Dosuwa już umieszczone okno tak, aby pasek SSD zmieścił się na ekranie.
1091 + ///
1092 + /// PO CO osobno od `place_new_window`: w momencie pierwszego mapowania okno
1093 + /// Waylanda może jeszcze nie mieć wynegocjowanych dekoracji, więc ląduje
1094 + /// wyżej. Gdy klient je zgłosi, korygujemy pozycję (bez zmiany rozmiaru).
1095 + fn reposition_for_ssd(&mut self, toplevel: &ToplevelSurface) {
1096 + let Some(window) = self.window_for_surface(toplevel.wl_surface()) else {
1097 + return;
1098 + };
1099 + let Some(location) = self.space.element_location(&window) else {
1100 + return;
1101 + };
1102 + let adjusted = self.adjust_for_ssd(&window, location);
1103 + if adjusted != location {
1104 + self.space.map_element(window, adjusted, false);
1105 + }
1106 + }
1107 +}
1108 +
1057 1109 impl XdgShellHandler for MyCompositor {
1058 1110 fn xdg_shell_state(&mut self) -> &mut XdgShellState {
1059 1111 &mut self.xdg_shell_state
@@ -1194,18 +1246,18 @@ impl XdgShellHandler for MyCompositor {
1194 1246 }
1195 1247
1196 1248 fn maximize_request(&mut self, surface: ToplevelSurface) {
1197 - // Maksymalizacja w menedżerze pływającym = rozmiar całego wyjścia.
1198 - let output = self.space.outputs().next().cloned();
1199 - if let Some(output) = output {
1200 - if let Some(geometry) = self.space.output_geometry(&output) {
1201 - if let Some(window) = self.window_for_surface(surface.wl_surface()) {
1202 - surface.with_pending_state(|state| {
1203 - state.states.set(xdg_toplevel::State::Maximized);
1204 - state.size = Some(geometry.size);
1205 - });
1206 - self.space.map_element(window, geometry.loc, true);
1207 - }
1208 - }
1249 + // Maksymalizacja w menedżerze pływającym = obszar ROBOCZY wyjścia
1250 + // (bez panelu), żeby pasek systemowy pozostał widoczny.
1251 + let Some(window) = self.window_for_surface(surface.wl_surface()) else {
1252 + surface.send_configure();
1253 + return;
1254 + };
1255 + if let Some(area) = self.output_work_area_for_window(&window) {
1256 + surface.with_pending_state(|state| {
1257 + state.states.set(xdg_toplevel::State::Maximized);
1258 + state.size = Some(area.size);
1259 + });
1260 + self.space.map_element(window, area.loc, true);
1209 1261 }
1210 1262 surface.send_configure();
1211 1263 }
@@ -116,6 +116,57 @@ struct SsdState {
116 116 // pochodzą z motywu (`theme::Theme`) — patrz `~/.config/pagan/theme.conf`.
117 117 // Zmiana motywu jest widoczna w następnej klatce, bo parametry są uniformami.
118 118
119 +/// Marker w danych powierzchni: klient **negocjował** dekoracje
120 +/// (`zxdg_toplevel_decoration_v1`).
121 +///
122 +/// PO CO: tylko wtedy możemy wymusić tryb serwerowy i narysować własny pasek
123 +/// tytułu. Klienci, którzy tego protokołu nie znają (np. GTK3 — `xfce4-terminal`,
124 +/// `thunar`), rysują WŁASNE dekoracje (CSD). Dołożenie drugiego paska
125 +/// nakładałoby je na siebie — to właśnie efekt „przesuniętej dekoracji”.
126 +#[derive(Debug)]
127 +pub struct NegotiatedSsd;
128 +
129 +/// Zapisuje fakt negocjacji dekoracji przez klienta (wołane z `new_decoration`).
130 +///
131 +/// PO CO osobna funkcja: `XdgDecorationHandler` żyje w `state.rs`, a decyzję
132 +/// „czy rysować SSD” podejmuje `PaganWindow` w tym module — marker w danych
133 +/// powierzchni łączy oba miejsca bez przeciągania stanu przez cały kompozytor.
134 +pub fn mark_ssd_negotiated(toplevel: &ToplevelSurface) {
135 + with_states(toplevel.wl_surface(), |states| {
136 + states.data_map.insert_if_missing(|| NegotiatedSsd);
137 + });
138 +}
139 +
140 +/// Czy klient negocjował dekoracje serwerowe (patrz `PaganWindow::wants_ssd`)?
141 +fn ssd_negotiated(toplevel: &ToplevelSurface) -> bool {
142 + with_states(toplevel.wl_surface(), |states| {
143 + states.data_map.get::<NegotiatedSsd>().is_some()
144 + })
145 +}
146 +
147 +/// Reguła decyzji o dekoracjach serwerowych — CZYSTA, żeby dała się przetestować
148 +/// bez żywych powierzchni Waylanda.
149 +///
150 +/// * `x11`: `Some(override_redirect)` dla okna X11, `None` dla okna Waylanda.
151 +/// * `toplevel`: czy to okno najwyższego poziomu (nie popup/subsurface).
152 +/// * `negotiated`: czy klient wynegocjował `zxdg_toplevel_decoration_v1`.
153 +///
154 +/// PO CO taka separacja: X11 nie zna pojęcia CSD, więc zawsze dostaje nasz pasek
155 +/// (poza menu/podpowiedziami `override-redirect`). Okno Waylanda dostaje pasek
156 +/// tylko wtedy, gdy oddało dekoracje kompozytorowi — inaczej (np. GTK3)
157 +/// nakładalibyśmy drugi pasek na jego własny.
158 +fn ssd_decision(x11: Option<bool>, toplevel: bool, negotiated: bool) -> bool {
159 + match x11 {
160 + Some(override_redirect) => !override_redirect,
161 + None => toplevel && negotiated,
162 + }
163 +}
164 +
165 +/// Publiczny dostęp do faktu negocjacji — używany w logach diagnostycznych.
166 +pub fn decoration_negotiated(window: &PaganWindow) -> bool {
167 + window.toplevel().is_some_and(ssd_negotiated)
168 +}
169 +
119 170 #[derive(Debug, Clone, PartialEq)]
120 171 pub struct PaganWindow(pub Window);
121 172
@@ -214,10 +265,14 @@ impl PaganWindow {
214 265 /// ikony przeciągania) to elementy aplikacji, a nie samodzielne okna —
215 266 /// pasek nad nimi wyglądałby jak „przesunięta dekoracja” przyklejona do menu.
216 267 pub fn wants_ssd(&self) -> bool {
217 - if let Some(x11) = self.x11_surface() {
218 - return !x11.is_override_redirect();
219 - }
220 - self.toplevel().is_some()
268 + let x11 = self
269 + .x11_surface()
270 + .map(|surface| surface.is_override_redirect());
271 + ssd_decision(
272 + x11,
273 + self.toplevel().is_some(),
274 + self.toplevel().is_some_and(ssd_negotiated),
275 + )
221 276 }
222 277
223 278 /// Dane użytkownika okna z flagami dekoracji (tworzone leniwie).
@@ -1186,4 +1241,35 @@ mod tests {
1186 1241 // Środki etykiet i przycisków leżą w pionowym środku paska.
1187 1242 assert_eq!(label_y, bar.loc.y + TITLEBAR_HEIGHT / 2);
1188 1243 }
1244 +
1245 + /// Okno X11 zawsze dostaje nasz pasek (X11 nie ma CSD).
1246 + #[test]
1247 + fn x11_okno_dostaje_ssd() {
1248 + assert!(ssd_decision(Some(false), false, false));
1249 + }
1250 +
1251 + /// Menu/podpowiedź X11 (`override-redirect`) NIE dostaje paska.
1252 + #[test]
1253 + fn x11_override_redirect_bez_ssd() {
1254 + assert!(!ssd_decision(Some(true), false, false));
1255 + }
1256 +
1257 + /// Okno Waylanda bez negocjacji dekoracji (np. GTK3) NIE dostaje paska —
1258 + /// inaczej nasz pasek nakładałby się na jego CSD.
1259 + #[test]
1260 + fn wayland_bez_negocjacji_bez_ssd() {
1261 + assert!(!ssd_decision(None, true, false));
1262 + }
1263 +
1264 + /// Okno Waylanda, które oddało dekoracje kompozytorowi, dostaje nasz pasek.
1265 + #[test]
1266 + fn wayland_z_negocjacja_dostaje_ssd() {
1267 + assert!(ssd_decision(None, true, true));
1268 + }
1269 +
1270 + /// Popup/subsurface nie jest samodzielnym oknem — bez paska.
1271 + #[test]
1272 + fn wayland_popup_bez_ssd() {
1273 + assert!(!ssd_decision(None, false, false));
1274 + }
1189 1275 }
@@ -221,6 +221,13 @@ impl XwmHandler for MyCompositor {
221 221 let size = element.geometry().size;
222 222 let _ = window.configure(Rectangle::new(location, size));
223 223 }
224 +
225 + // Nowe okno X11 przejmuje fokus tak samo jak okno Waylanda — inaczej
226 + // klawisze trafiałyby do poprzedniego okna, a pasek SSD pozostałby
227 + // przygaszony (stan „nieaktywne”) mimo że to okno jest na wierzchu.
228 + let serial = SERIAL_COUNTER.next_serial();
229 + self.raise_and_focus(&element, serial);
230 +
224 231 trace!(title = %window.title(), "X11: zmapowano okno");
225 232 }
226 233
@@ -293,7 +300,9 @@ impl XwmHandler for MyCompositor {
293 300 .cloned()
294 301 .or_else(|| self.space.outputs().next().cloned());
295 302 let Some(output) = output else { return };
296 - let Some(area) = self.space.output_geometry(&output) else {
303 + // Obszar ROBOCZY (bez panelu): zmaksymalizowane okno ma zostawić pasek
304 + // systemowy widoczny, tak jak w każdym innym menedżerze okien.
305 + let Some(area) = self.output_work_area(&output) else {
297 306 return;
298 307 };
299 308
@@ -520,14 +520,19 @@ fn which(name: &str) -> bool {
520 520 /// X11 i ląduje na pulpicie XFCE — POZA naszym kompozytorem. Wtedy okno i jego
521 521 /// dekoracja nie mają ze sobą nic wspólnego.
522 522 ///
523 -/// Dlatego:
524 -/// * wymuszamy backend Wayland dla GTK (`GDK_BACKEND`) i Qt (`QT_QPA_PLATFORM`),
525 -/// * aplikacjom X11 dajemy `DISPLAY` NASZEGO XWayland (jeśli kompozytor je
526 -/// opublikował, patrz `xwayland-display` w `XDG_RUNTIME_DIR/pagan`).
523 +/// Wybór backendu zależy od tego, czy aplikacja umie oddać dekoracje:
524 +/// * **GTK → XWayland (`x11`)**. GTK3 na Waylandzie ZAWSZE rysuje własne CSD
525 +/// i nie zna `zxdg_decoration_manager_v1`, więc nie da się na nim wymusić
526 +/// naszego paska. Na X11 CSD nie istnieje — dekorację rysuje kompozytor,
527 +/// czyli MY, spójnie z resztą środowiska. Nadpisz: `PAGAN_GDK_BACKEND=wayland`.
528 +/// * **Qt → Wayland**. Qt zna `xdg-decoration` i honoruje wymuszony tryb
529 +/// serwerowy, więc dostaje nasz pasek bez XWayland.
530 +/// * Aplikacjom czysto X11 (bez GDK/Qt) dajemy `DISPLAY` NASZEGO XWayland.
527 531 fn spawn_in_session(command: &str) -> std::io::Result<std::process::Child> {
528 532 let mut cmd = Command::new(command);
529 533 cmd.env("XDG_SESSION_TYPE", "wayland");
530 - cmd.env("GDK_BACKEND", "wayland");
534 + let gdk_backend = std::env::var("PAGAN_GDK_BACKEND").unwrap_or_else(|_| "x11".to_string());
535 + cmd.env("GDK_BACKEND", gdk_backend);
531 536 cmd.env("QT_QPA_PLATFORM", "wayland");
532 537 if let Some(display) = compositor_xdisplay() {
533 538 cmd.env("DISPLAY", display);