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);