Commit ad89935
8
plików
+809
dodanych
-1
usuniętych
@@ -0,0 +1,492 @@
1
+//! `wlr-foreign-toplevel-management` — lista i sterowanie oknami (serwer od zera).
2
+//!
3
+//! PO CO: to protokół, na którym opierają się panele i paski zadań (waybar, sfwbar,
4
+//! nasz przyszły panel PaganDE). Daje klientowi **listę okien** (tytuł, `app_id`,
5
+//! stan) oraz pozwala nimi **sterować** (aktywuj, zamknij, minimalizuj, maksymalizuj),
6
+//! czego nie potrafi nowszy `ext-foreign-toplevel-list-v1` (tylko odczyt).
7
+//!
8
+//! Smithay 0.7 nie ma tego protokołu, więc — podobnie jak `screencopy` — piszemy
9
+//! serwer sami. Definicje („kabel”) pochodzą z `wayland-protocols-wlr`, cała
10
+//! semantyka jest tutaj.
11
+//!
12
+//! Jak to działa:
13
+//! * `zwlr_foreign_toplevel_manager_v1` to global; każde okno ma dla każdego
14
+//! klienta osobny obiekt `zwlr_foreign_toplevel_handle_v1`,
15
+//! * stan wysyłamy **porcjami**: `title`/`app_id`/`state` kończy zdarzenie `done`,
16
+//! dzięki czemu klient widzi spójny obraz (zmiany są atomowe),
17
+//! * przy zniszczeniu okna wysyłamy `closed`.
18
+//!
19
+//! PO CO synchronizacja „przyrostowa” co klatkę, a nie haki we wszystkich
20
+//! miejscach: stan okna zmienia się w wielu ścieżkach (commit, snap, fokus,
21
+//! minimalizacja…). Jedna funkcja `sync` porównuje stan z ostatnio wysłanym i
22
+//! wysyła tylko różnice — mniej miejsc, w których można się pomylić.
23
+
24
+use std::{
25
+ collections::HashMap,
26
+ sync::atomic::{AtomicU64, Ordering},
27
+};
28
+
29
+use smithay::{
30
+ reexports::{
31
+ wayland_protocols::xdg::shell::server::xdg_toplevel,
32
+ wayland_protocols_wlr::foreign_toplevel::v1::server::{
33
+ zwlr_foreign_toplevel_handle_v1::{self, State, ZwlrForeignToplevelHandleV1},
34
+ zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
35
+ },
36
+ wayland_server::{
37
+ backend::{ClientId, ObjectId},
38
+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
39
+ },
40
+ },
41
+ utils::IsAlive,
42
+ wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData},
43
+};
44
+use tracing::debug;
45
+
46
+use crate::{focus::KeyboardFocusTarget, state::MyCompositor, window::PaganWindow};
47
+
48
+/// Maksymalna wersja protokołu obsługiwana przez `wayland-protocols-wlr 0.3`.
49
+const VERSION: u32 = 3;
50
+
51
+/// Tożsamość okna dla protokołu — stabilna przez całe życie okna.
52
+///
53
+/// PO CO własny licznik, a nie np. wskaźnik: `Window` nie implementuje `Hash`/`Eq`,
54
+/// a numerek jest wygodny jako klucz mapy.
55
+#[derive(Debug, Clone, Copy)]
56
+struct ToplevelId(u64);
57
+
58
+/// Licznik identyfikatorów okien (globalny — kolejność nie ma znaczenia).
59
+static NEXT_TOPLEVEL_ID: AtomicU64 = AtomicU64::new(1);
60
+
61
+/// Dane obiektu `zwlr_foreign_toplevel_handle_v1`.
62
+///
63
+/// Trzymamy tylko identyfikator okna — resztę (tytuł, stan) pamiętamy w jednym
64
+/// miejscu (`ToplevelEntry`), żeby nie duplikować prawdy.
65
+#[derive(Debug)]
66
+pub struct HandleUserData {
67
+ window_id: u64,
68
+}
69
+
70
+/// Stan protokołu w kompozytorze.
71
+pub struct ForeignToplevelState {
72
+ /// Obiekty menedżera, po jednym na klienta, który się podłączył.
73
+ managers: Vec<ZwlrForeignToplevelManagerV1>,
74
+ /// Wpisy okien: co już wysłaliśmy i do kogo.
75
+ toplevels: HashMap<u64, ToplevelEntry>,
76
+}
77
+
78
+impl std::fmt::Debug for ForeignToplevelState {
79
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80
+ f.debug_struct("ForeignToplevelState")
81
+ .field("managers", &self.managers.len())
82
+ .field("toplevels", &self.toplevels.len())
83
+ .finish()
84
+ }
85
+}
86
+
87
+impl ForeignToplevelState {
88
+ pub fn new() -> Self {
89
+ Self {
90
+ managers: Vec::new(),
91
+ toplevels: HashMap::new(),
92
+ }
93
+ }
94
+}
95
+
96
+impl Default for ForeignToplevelState {
97
+ fn default() -> Self {
98
+ Self::new()
99
+ }
100
+}
101
+
102
+/// Wpis jednego okna: uchwyty klientów + ostatnio wysłany stan (do porównania).
103
+struct ToplevelEntry {
104
+ window: PaganWindow,
105
+ /// (id menedżera, uchwyt) — jeden uchwyt na klienta.
106
+ handles: Vec<(ObjectId, ZwlrForeignToplevelHandleV1)>,
107
+ title: String,
108
+ app_id: String,
109
+ states: Vec<State>,
110
+ /// Czy trzeba wysłać pełny stan (nowy uchwyt albo zmiana danych).
111
+ dirty: bool,
112
+}
113
+
114
+impl ToplevelEntry {
115
+ fn new(window: PaganWindow) -> Self {
116
+ Self {
117
+ window,
118
+ handles: Vec::new(),
119
+ title: String::new(),
120
+ app_id: String::new(),
121
+ states: Vec::new(),
122
+ dirty: true,
123
+ }
124
+ }
125
+}
126
+
127
+/// Rejestruje global `zwlr_foreign_toplevel_manager_v1`.
128
+pub fn init(display: &DisplayHandle) {
129
+ display.create_global::<MyCompositor, ZwlrForeignToplevelManagerV1, ()>(VERSION, ());
130
+ debug!("global zwlr_foreign_toplevel_manager_v1 zarejestrowany");
131
+}
132
+
133
+/// Koduje listę stanów do tablicy bajtów w formacie protokołu (tablica `u32`).
134
+fn encode_states(states: &[State]) -> Vec<u8> {
135
+ states
136
+ .iter()
137
+ .flat_map(|state| (*state as u32).to_ne_bytes())
138
+ .collect()
139
+}
140
+
141
+// --- Menedżer -----------------------------------------------------------------
142
+
143
+impl GlobalDispatch<ZwlrForeignToplevelManagerV1, (), MyCompositor> for MyCompositor {
144
+ fn bind(
145
+ state: &mut MyCompositor,
146
+ _handle: &DisplayHandle,
147
+ _client: &Client,
148
+ resource: New<ZwlrForeignToplevelManagerV1>,
149
+ _data: &(),
150
+ data_init: &mut DataInit<'_, MyCompositor>,
151
+ ) {
152
+ let manager = data_init.init(resource, ());
153
+ // Nowy klient (panel) od razu dostanie listę istniejących okien — zrobi to
154
+ // najbliższy `sync_foreign_toplevels`, bo tworzy uchwyty „przy okazji”.
155
+ state.foreign_toplevel.managers.push(manager);
156
+ // Wymuś pełną synchronizację, żeby nowy klient nie czekał na zmianę stanu.
157
+ for entry in state.foreign_toplevel.toplevels.values_mut() {
158
+ entry.dirty = true;
159
+ }
160
+ }
161
+}
162
+
163
+impl Dispatch<ZwlrForeignToplevelManagerV1, (), MyCompositor> for MyCompositor {
164
+ fn request(
165
+ state: &mut MyCompositor,
166
+ _client: &Client,
167
+ manager: &ZwlrForeignToplevelManagerV1,
168
+ request: zwlr_foreign_toplevel_manager_v1::Request,
169
+ _data: &(),
170
+ _handle: &DisplayHandle,
171
+ _data_init: &mut DataInit<'_, MyCompositor>,
172
+ ) {
173
+ match request {
174
+ // Klient nie chce już nowych okien. Zgodnie z protokołem wysyłamy
175
+ // `finished` i przestajemy go obsługiwać.
176
+ zwlr_foreign_toplevel_manager_v1::Request::Stop => {
177
+ manager.finished();
178
+ state
179
+ .foreign_toplevel
180
+ .managers
181
+ .retain(|candidate| candidate != manager);
182
+ }
183
+ _ => {}
184
+ }
185
+ }
186
+
187
+ fn destroyed(
188
+ state: &mut MyCompositor,
189
+ _client: ClientId,
190
+ manager: &ZwlrForeignToplevelManagerV1,
191
+ _data: &(),
192
+ ) {
193
+ state
194
+ .foreign_toplevel
195
+ .managers
196
+ .retain(|candidate| candidate != manager);
197
+ // Uchwyty tego klienta staną się bezużyteczne — usuwamy je przy okazji sync.
198
+ for entry in state.foreign_toplevel.toplevels.values_mut() {
199
+ entry.handles.retain(|(_, handle)| handle.is_alive());
200
+ entry.dirty = true;
201
+ }
202
+ }
203
+}
204
+
205
+// --- Uchwyt okna --------------------------------------------------------------
206
+
207
+impl Dispatch<ZwlrForeignToplevelHandleV1, HandleUserData, MyCompositor> for MyCompositor {
208
+ fn request(
209
+ state: &mut MyCompositor,
210
+ _client: &Client,
211
+ handle: &ZwlrForeignToplevelHandleV1,
212
+ request: zwlr_foreign_toplevel_handle_v1::Request,
213
+ data: &HandleUserData,
214
+ _display: &DisplayHandle,
215
+ _data_init: &mut DataInit<'_, MyCompositor>,
216
+ ) {
217
+ // Żądania sterujące zamieniamy na akcje menedżera okien. To jedyne miejsce,
218
+ // w którym panel może wpływać na okna — trzymamy je cienkie.
219
+ let Some(window) = state.window_by_foreign_id(data.window_id) else {
220
+ return;
221
+ };
222
+
223
+ match request {
224
+ zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
225
+ // `seat` pomijamy: mamy jedno siedzenie (single-seat desktop).
226
+ state.focus_window(&window);
227
+ }
228
+ zwlr_foreign_toplevel_handle_v1::Request::Close => {
229
+ state.close_window(&window);
230
+ }
231
+ zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => {
232
+ state.minimize_window(&window);
233
+ }
234
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => {
235
+ state.restore_window(&window);
236
+ }
237
+ zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => {
238
+ state.maximize_window(&window);
239
+ }
240
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => {
241
+ state.unmaximize_window(&window);
242
+ }
243
+ zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { .. } => {
244
+ state.set_fullscreen_window(&window);
245
+ }
246
+ zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
247
+ state.unset_fullscreen_window(&window);
248
+ }
249
+ // `set_rectangle` to tylko podpowiedź gdzie panel rysuje kafelek —
250
+ // używana np. przy animacji minimalizacji. Nie potrzebujemy jej.
251
+ zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => {}
252
+ zwlr_foreign_toplevel_handle_v1::Request::Destroy => {
253
+ remove_handle(state, data.window_id, handle);
254
+ }
255
+ _ => {}
256
+ }
257
+ }
258
+
259
+ fn destroyed(
260
+ state: &mut MyCompositor,
261
+ _client: ClientId,
262
+ handle: &ZwlrForeignToplevelHandleV1,
263
+ data: &HandleUserData,
264
+ ) {
265
+ remove_handle(state, data.window_id, handle);
266
+ }
267
+}
268
+
269
+/// Usuwa uchwyt z wpisu okna (wywoływane przy `destroy` i zniszczeniu obiektu).
270
+fn remove_handle(state: &mut MyCompositor, window_id: u64, handle: &ZwlrForeignToplevelHandleV1) {
271
+ if let Some(entry) = state.foreign_toplevel.toplevels.get_mut(&window_id) {
272
+ entry.handles.retain(|(_, candidate)| candidate != handle);
273
+ }
274
+}
275
+
276
+// --- Synchronizacja stanu -----------------------------------------------------
277
+
278
+impl MyCompositor {
279
+ /// Znajduje okno po identyfikatorze protokołu.
280
+ pub fn window_by_foreign_id(&self, window_id: u64) -> Option<PaganWindow> {
281
+ self.foreign_toplevel
282
+ .toplevels
283
+ .get(&window_id)
284
+ .map(|entry| entry.window.clone())
285
+ }
286
+
287
+ /// Wysyła klientom różnice stanu okien (wołane raz na klatkę).
288
+ ///
289
+ /// Zbiera okna **zmapowane i zminimalizowane** (zminimalizowane też mają być
290
+ /// na pasku zadań!), tworzy brakujące uchwyty i wysyła tylko to, co się zmieniło.
291
+ pub fn sync_foreign_toplevels(&mut self) {
292
+ if self.foreign_toplevel.managers.is_empty() {
293
+ return;
294
+ }
295
+
296
+ // Kopia zasobów (tanie, to `Arc`) uwalnia pożyczkę `self.foreign_toplevel`
297
+ // na czas mutowania wpisów.
298
+ let managers = self.foreign_toplevel.managers.clone();
299
+ let display = self.display_handle.clone();
300
+
301
+ // 1) Wpisy po martwych oknach: powiadamiamy klientów zdarzeniem `closed`.
302
+ let dead: Vec<u64> = self
303
+ .foreign_toplevel
304
+ .toplevels
305
+ .iter()
306
+ .filter(|(_, entry)| !entry.window.alive())
307
+ .map(|(id, _)| *id)
308
+ .collect();
309
+ for id in dead {
310
+ if let Some(entry) = self.foreign_toplevel.toplevels.remove(&id) {
311
+ for (_, handle) in entry.handles {
312
+ handle.closed();
313
+ }
314
+ debug!(window_id = id, "obce-okno: zamknięte");
315
+ }
316
+ }
317
+
318
+ // 2) Bieżące okna: zmapowane + zminimalizowane.
319
+ let mut live: Vec<PaganWindow> = self.space.elements().cloned().collect();
320
+ live.extend(self.minimized.iter().map(|entry| entry.window.clone()));
321
+
322
+ for window in live {
323
+ let id = foreign_id(&window);
324
+
325
+ // Dane zbieramy PRZED pożyczeniem wpisu, żeby nie mieszać pożyczek `self`.
326
+ let title = window_title(&window);
327
+ let app_id = window_app_id(&window);
328
+ let states = self.window_foreign_states(&window);
329
+
330
+ let entry = self
331
+ .foreign_toplevel
332
+ .toplevels
333
+ .entry(id)
334
+ .or_insert_with(|| ToplevelEntry::new(window.clone()));
335
+
336
+ if entry.title != title {
337
+ entry.title = title;
338
+ entry.dirty = true;
339
+ }
340
+ if entry.app_id != app_id {
341
+ entry.app_id = app_id;
342
+ entry.dirty = true;
343
+ }
344
+ if entry.states != states {
345
+ entry.states = states;
346
+ entry.dirty = true;
347
+ }
348
+
349
+ // 2a) Nowi klienci: dołóż uchwyt dla każdego menedżera, który go nie ma.
350
+ for manager in &managers {
351
+ if !manager.is_alive() {
352
+ continue;
353
+ }
354
+ if entry
355
+ .handles
356
+ .iter()
357
+ .any(|(manager_id, _)| manager_id == &manager.id())
358
+ {
359
+ continue;
360
+ }
361
+ let Ok(client) = display.get_client(manager.id()) else {
362
+ continue;
363
+ };
364
+ let Ok(handle) = client
365
+ .create_resource::<ZwlrForeignToplevelHandleV1, HandleUserData, MyCompositor>(
366
+ &display,
367
+ manager.version(),
368
+ HandleUserData { window_id: id },
369
+ )
370
+ else {
371
+ continue;
372
+ };
373
+ // Kolejność: najpierw zdarzenie `toplevel` (tworzy obiekt u klienta),
374
+ // potem szczegóły i `done`.
375
+ manager.toplevel(&handle);
376
+ entry.handles.push((manager.id(), handle));
377
+ entry.dirty = true;
378
+ }
379
+
380
+ // 2b) Wyślij stan tylko wtedy, gdy coś się zmieniło.
381
+ if entry.dirty {
382
+ for (_, handle) in &entry.handles {
383
+ if !handle.is_alive() {
384
+ continue;
385
+ }
386
+ handle.title(entry.title.clone());
387
+ handle.app_id(entry.app_id.clone());
388
+ handle.state(encode_states(&entry.states));
389
+ handle.done();
390
+ }
391
+ entry.dirty = false;
392
+ }
393
+ }
394
+ }
395
+
396
+ /// Wylicza stan okna widziany przez protokół (aktywne, zminimalizowane, …).
397
+ fn window_foreign_states(&self, window: &PaganWindow) -> Vec<State> {
398
+ let mut states = Vec::new();
399
+
400
+ if self.minimized.iter().any(|entry| &entry.window == window) {
401
+ states.push(State::Minimized);
402
+ }
403
+
404
+ let focused = self
405
+ .seat
406
+ .get_keyboard()
407
+ .and_then(|keyboard| keyboard.current_focus())
408
+ .and_then(|target| match target {
409
+ KeyboardFocusTarget::Window(focused) => Some(&focused == &window.0),
410
+ // Popup ma własny fokus, ale to nie jest „aktywne okno” na pasku.
411
+ KeyboardFocusTarget::Popup(_) => None,
412
+ })
413
+ .unwrap_or(false);
414
+ if focused {
415
+ states.push(State::Activated);
416
+ }
417
+
418
+ if let Some(toplevel) = window.toplevel() {
419
+ let mut maximized = false;
420
+ let mut fullscreen = false;
421
+ toplevel.with_pending_state(|pending| {
422
+ maximized = pending.states.contains(xdg_toplevel::State::Maximized);
423
+ fullscreen = pending.states.contains(xdg_toplevel::State::Fullscreen);
424
+ });
425
+ if maximized {
426
+ states.push(State::Maximized);
427
+ }
428
+ if fullscreen {
429
+ states.push(State::Fullscreen);
430
+ }
431
+ } else if let Some(x11) = window.x11_surface() {
432
+ if x11.is_maximized() {
433
+ states.push(State::Maximized);
434
+ }
435
+ if x11.is_fullscreen() {
436
+ states.push(State::Fullscreen);
437
+ }
438
+ }
439
+
440
+ states
441
+ }
442
+}
443
+
444
+/// Zwraca (i w razie potrzeby nadaje) stabilny identyfikator okna.
445
+fn foreign_id(window: &PaganWindow) -> u64 {
446
+ let id = window
447
+ .user_data()
448
+ .get_or_insert(|| ToplevelId(NEXT_TOPLEVEL_ID.fetch_add(1, Ordering::Relaxed)));
449
+ id.0
450
+}
451
+
452
+/// Tytuł okna (Wayland: `xdg_toplevel`, X11: `WM_NAME`).
453
+fn window_title(window: &PaganWindow) -> String {
454
+ if let Some(surface) = window.wl_surface() {
455
+ if window.toplevel().is_some() {
456
+ return with_states(&surface, |states| {
457
+ states
458
+ .data_map
459
+ .get::<XdgToplevelSurfaceData>()
460
+ .and_then(|data| data.lock().unwrap().title.clone())
461
+ })
462
+ .unwrap_or_default();
463
+ }
464
+ }
465
+ if let Some(x11) = window.x11_surface() {
466
+ return x11.title();
467
+ }
468
+ String::new()
469
+}
470
+
471
+/// `app_id` okna (Wayland: `app_id`, X11: klasa/instancja, np. `firefox`).
472
+fn window_app_id(window: &PaganWindow) -> String {
473
+ if let Some(surface) = window.wl_surface() {
474
+ if window.toplevel().is_some() {
475
+ return with_states(&surface, |states| {
476
+ states
477
+ .data_map
478
+ .get::<XdgToplevelSurfaceData>()
479
+ .and_then(|data| data.lock().unwrap().app_id.clone())
480
+ })
481
+ .unwrap_or_default();
482
+ }
483
+ }
484
+ if let Some(x11) = window.x11_surface() {
485
+ let class = x11.class();
486
+ if !class.is_empty() {
487
+ return class.to_lowercase();
488
+ }
489
+ return x11.instance().to_lowercase();
490
+ }
491
+ String::new()
492
+}
@@ -15,6 +15,7 @@
15
15
16
16
mod cursor;
17
17
mod focus;
18
+mod foreign_toplevel;
18
19
mod grabs;
19
20
mod input;
20
21
mod output;
@@ -47,7 +47,7 @@ use smithay::{
47
47
Client, Display, DisplayHandle, Resource,
48
48
},
49
49
},
50
- utils::{IsAlive, Logical, Point, Serial, Size, SERIAL_COUNTER as SCOUNTER},
50
+ utils::{IsAlive, Logical, Point, Rectangle, Serial, Size, SERIAL_COUNTER as SCOUNTER},
51
51
wayland::{
52
52
buffer::BufferHandler,
53
53
compositor::{
@@ -110,6 +110,7 @@ use smithay::{
110
110
111
111
use crate::{
112
112
focus::{KeyboardFocusTarget, PointerFocusTarget},
113
+ foreign_toplevel::ForeignToplevelState,
113
114
grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeState, SurfaceData},
114
115
window::PaganWindow,
115
116
};
@@ -205,6 +206,9 @@ pub struct MyCompositor {
205
206
/// (skrót klawiszowy / przyszły pasek zadań).
206
207
pub minimized: Vec<MinimizedWindow>,
207
208
209
+ /// Stan protokołu `wlr-foreign-toplevel-management` (lista okien dla paneli).
210
+ pub foreign_toplevel: ForeignToplevelState,
211
+
208
212
// --- dmabuf + layer-shell ---
209
213
/// Delegat protokołu `linux-dmabuf` (bufory GPU).
210
214
pub dmabuf_state: DmabufState,
@@ -316,6 +320,9 @@ impl MyCompositor {
316
320
// Screencopy: rejestrujemy global; logika przechwycenia w `screencopy.rs`.
317
321
crate::screencopy::init(&dh);
318
322
323
+ // Lista i sterowanie oknami dla paneli/pasków zadań.
324
+ crate::foreign_toplevel::init(&dh);
325
+
319
326
// XWayland: `xwayland_shell` (skojarzenie okna X11 z wl_surface) oraz grab
320
327
// klawiatury X11. `XWaylandKeyboardGrabState` nie wymaga pola — trzyma go
321
328
// display; tworzymy wyłącznie po to, by zarejestrować global.
@@ -363,6 +370,7 @@ impl MyCompositor {
363
370
pointer_constraints_state,
364
371
dnd_icon: None,
365
372
minimized: Vec::new(),
373
+ foreign_toplevel: ForeignToplevelState::new(),
366
374
dmabuf_state: DmabufState::new(),
367
375
dmabuf_global: None,
368
376
layer_shell_state,
@@ -498,6 +506,129 @@ impl MyCompositor {
498
506
.cloned()
499
507
}
500
508
509
+ /// Przywraca KONKRETNE okno z listy zminimalizowanych.
510
+ ///
511
+ /// PO CO osobno od `restore_last_minimized`: panel wie, który kafelek
512
+ /// kliknięto, więc przywracamy wskazane okno, a nie ostatnie z brzegu.
513
+ pub fn restore_window(&mut self, window: &PaganWindow) {
514
+ let Some(position) = self
515
+ .minimized
516
+ .iter()
517
+ .position(|entry| &entry.window == window)
518
+ else {
519
+ return;
520
+ };
521
+ let entry = self.minimized.remove(position);
522
+ self.space
523
+ .map_element(entry.window.clone(), entry.location, true);
524
+ let serial = SCOUNTER.next_serial();
525
+ self.raise_and_focus(&entry.window, serial);
526
+ }
527
+
528
+ /// Podnosi i aktywuje okno; jeśli było schowane — najpierw je przywraca.
529
+ ///
530
+ /// To wspólna akcja dla skrótu, kliknięcia w pasek zadań i protokołu
531
+ /// `foreign-toplevel`.
532
+ pub fn focus_window(&mut self, window: &PaganWindow) {
533
+ if self.minimized.iter().any(|entry| &entry.window == window) {
534
+ self.restore_window(window);
535
+ return;
536
+ }
537
+ if !window.alive() {
538
+ return;
539
+ }
540
+ let serial = SCOUNTER.next_serial();
541
+ self.raise_and_focus(window, serial);
542
+ }
543
+
544
+ /// Prosi okno o zamknięcie (Wayland: `xdg_toplevel.close`, X11: `WM_DELETE_WINDOW`).
545
+ pub fn close_window(&mut self, window: &PaganWindow) {
546
+ if let Some(toplevel) = window.toplevel() {
547
+ toplevel.send_close();
548
+ } else if let Some(x11) = window.x11_surface() {
549
+ let _ = x11.close();
550
+ }
551
+ }
552
+
553
+ /// Maksymalizuje okno (w pływającym WM = rozmiar wyjścia pod oknem).
554
+ pub fn maximize_window(&mut self, window: &PaganWindow) {
555
+ if let Some(toplevel) = window.toplevel() {
556
+ let Some(area) = self.output_area_for_window(window) else {
557
+ return;
558
+ };
559
+ toplevel.with_pending_state(|state| {
560
+ state.states.set(xdg_toplevel::State::Maximized);
561
+ state.size = Some(area.size);
562
+ });
563
+ self.space.map_element(window.clone(), area.loc, true);
564
+ toplevel.send_configure();
565
+ } else if let Some(x11) = window.x11_surface() {
566
+ // Dla X11 zmiana stanu jest prośbą do klienta; geometrią zajmuje się
567
+ // `XwmHandler::maximize_request` (zapamiętuje starą geometrię).
568
+ let _ = x11.set_maximized(true);
569
+ }
570
+ }
571
+
572
+ /// Cofa maksymalizację.
573
+ pub fn unmaximize_window(&mut self, window: &PaganWindow) {
574
+ if let Some(toplevel) = window.toplevel() {
575
+ toplevel.with_pending_state(|state| {
576
+ state.states.unset(xdg_toplevel::State::Maximized);
577
+ state.size = None;
578
+ });
579
+ toplevel.send_pending_configure();
580
+ } else if let Some(x11) = window.x11_surface() {
581
+ let _ = x11.set_maximized(false);
582
+ }
583
+ }
584
+
585
+ /// Pełny ekran na wyjściu pod oknem.
586
+ pub fn set_fullscreen_window(&mut self, window: &PaganWindow) {
587
+ if let Some(toplevel) = window.toplevel() {
588
+ let Some(area) = self.output_area_for_window(window) else {
589
+ return;
590
+ };
591
+ toplevel.with_pending_state(|state| {
592
+ state.states.set(xdg_toplevel::State::Fullscreen);
593
+ state.size = Some(area.size);
594
+ });
595
+ self.space.map_element(window.clone(), area.loc, true);
596
+ toplevel.send_configure();
597
+ } else if let Some(x11) = window.x11_surface() {
598
+ let _ = x11.set_fullscreen(true);
599
+ }
600
+ }
601
+
602
+ /// Wychodzi z pełnego ekranu.
603
+ pub fn unset_fullscreen_window(&mut self, window: &PaganWindow) {
604
+ if let Some(toplevel) = window.toplevel() {
605
+ toplevel.with_pending_state(|state| {
606
+ state.states.unset(xdg_toplevel::State::Fullscreen);
607
+ state.size = None;
608
+ });
609
+ toplevel.send_pending_configure();
610
+ } else if let Some(x11) = window.x11_surface() {
611
+ let _ = x11.set_fullscreen(false);
612
+ }
613
+ }
614
+
615
+ /// Prostokąt wyjścia, na którym leży okno (a jeśli nie wiadomo — pierwszego).
616
+ fn output_area_for_window(&self, window: &PaganWindow) -> Option<Rectangle<i32, Logical>> {
617
+ let location = self.space.element_location(window);
618
+ let under = location.and_then(|location| {
619
+ self.space.outputs().find_map(|output| {
620
+ let geometry = self.space.output_geometry(output)?;
621
+ geometry.contains(location).then_some(geometry)
622
+ })
623
+ });
624
+ under.or_else(|| {
625
+ self.space
626
+ .outputs()
627
+ .next()
628
+ .and_then(|output| self.space.output_geometry(output))
629
+ })
630
+ }
631
+
501
632
/// Prosi klienta o zmianę rozmiaru (poprzez nowy configure xdg-toplevel).
502
633
///
503
634
/// PO CO przez configure, a nie bezpośrednio: w hybrydowym CSD to klient
@@ -299,6 +299,8 @@ pub fn run_udev() -> anyhow::Result<()> {
299
299
continue;
300
300
}
301
301
state.space.refresh();
302
+ // Lista okien dla paneli (tytuły, stan aktywny/zminimalizowany).
303
+ state.sync_foreign_toplevels();
302
304
state.popups.cleanup();
303
305
let _ = state.display_handle.flush_clients();
304
306
}
@@ -190,6 +190,8 @@ pub fn run_winit() -> anyhow::Result<()> {
190
190
}
191
191
192
192
state.space.refresh();
193
+ // Lista okien dla paneli (tytuły, stan aktywny/zminimalizowany).
194
+ state.sync_foreign_toplevels();
193
195
state.popups.cleanup();
194
196
display_handle
195
197
.flush_clients()
@@ -255,6 +255,7 @@ dependencies = [
255
255
"tracing-subscriber",
256
256
"wayland-client",
257
257
"wayland-protocols",
258
+ "wayland-protocols-wlr",
258
259
]
259
260
260
261
[[package]]
@@ -600,6 +601,19 @@ dependencies = [
600
601
"wayland-scanner",
601
602
]
602
603
604
+[[package]]
605
+name = "wayland-protocols-wlr"
606
+version = "0.3.12"
607
+source = "registry+https://github.com/rust-lang/crates.io-index"
608
+checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
609
+dependencies = [
610
+ "bitflags",
611
+ "wayland-backend",
612
+ "wayland-client",
613
+ "wayland-protocols",
614
+ "wayland-scanner",
615
+]
616
+
603
617
[[package]]
604
618
name = "wayland-scanner"
605
619
version = "0.31.11"
@@ -14,6 +14,8 @@ readme = "README.md"
14
14
[dependencies]
15
15
wayland-client = "0.31"
16
16
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
17
+# Protokoły WLR (lista okien dla paska zadań, panel na warstwie).
18
+wayland-protocols-wlr = { version = "0.3", features = ["client"] }
17
19
memmap2 = "0.9"
18
20
cairo-rs = "0.22"
19
21
tempfile = "3"
@@ -23,3 +25,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
23
25
[[example]]
24
26
name = "demo"
25
27
path = "examples/demo.rs"
28
+
29
+[[example]]
30
+name = "toplevels"
31
+path = "examples/toplevels.rs"
26
32
new file mode 100644
@@ -0,0 +1,160 @@
1
+//! Narzędzie DEV: podejrzyj listę okien z kompozytora.
2
+//!
3
+//! PO CO: weryfikuje protokół `wlr-foreign-toplevel-management` po stronie
4
+//! kompozytora (tytuł, `app_id`, stan). To ten sam protokół, na którym będzie
5
+//! opierał się pasek zadań panelu PaganDE — dobrze go najpierw zobaczyć „gołym
6
+//! okiem”, zanim wbudujemy go w UI.
7
+//!
8
+//! Użycie:
9
+//! WAYLAND_DISPLAY=wayland-1 cargo run --example toplevels
10
+//!
11
+//! Zostaw uruchomione i otwieraj/zamykaj okna — wypisze zmiany na żywo.
12
+
13
+use std::collections::HashMap;
14
+use std::process::ExitCode;
15
+
16
+use wayland_client::{
17
+ globals::{registry_queue_init, GlobalListContents},
18
+ protocol::wl_registry,
19
+ Connection, Dispatch, QueueHandle,
20
+};
21
+use wayland_protocols_wlr::foreign_toplevel::v1::client::{
22
+ zwlr_foreign_toplevel_handle_v1 as handle,
23
+ zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
24
+ zwlr_foreign_toplevel_manager_v1 as manager,
25
+ zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
26
+};
27
+
28
+/// Stan klienta. Trzymamy uchwyty, żeby wiedzieć, ile okien widzimy.
29
+#[derive(Default)]
30
+struct State {
31
+ handles: HashMap<usize, ZwlrForeignToplevelHandleV1>,
32
+ titles: HashMap<usize, String>,
33
+}
34
+
35
+/// Klucz uchwytu (stabilny w obrębie sesji klienta).
36
+fn key(handle: &ZwlrForeignToplevelHandleV1) -> usize {
37
+ use wayland_client::Proxy;
38
+ handle.id().protocol_id() as usize
39
+}
40
+
41
+/// Dekoduje tablicę stanów protokołu (`u32` → nazwa).
42
+fn decode_states(bytes: &[u8]) -> Vec<&'static str> {
43
+ bytes
44
+ .chunks_exact(4)
45
+ .map(|chunk| {
46
+ // Tablica przychodzi w kolejności bajtów hosta (native endian).
47
+ let value = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
48
+ match value {
49
+ 0 => "maximized",
50
+ 1 => "minimized",
51
+ 2 => "activated",
52
+ 3 => "fullscreen",
53
+ _ => "?",
54
+ }
55
+ })
56
+ .collect()
57
+}
58
+
59
+impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
60
+ fn event(
61
+ _state: &mut Self,
62
+ _registry: &wl_registry::WlRegistry,
63
+ _event: wl_registry::Event,
64
+ _data: &GlobalListContents,
65
+ _conn: &Connection,
66
+ _qh: &QueueHandle<Self>,
67
+ ) {
68
+ }
69
+}
70
+
71
+impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for State {
72
+ // Zdarzenie `toplevel` tworzy nowy obiekt u klienta — `wayland-client` wymaga
73
+ // wskazania, jakim typem go utworzyć (inaczej panikuje).
74
+ wayland_client::event_created_child!(State, ZwlrForeignToplevelManagerV1, [
75
+ manager::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ())
76
+ ]);
77
+
78
+ fn event(
79
+ _state: &mut Self,
80
+ _manager: &ZwlrForeignToplevelManagerV1,
81
+ event: manager::Event,
82
+ _data: &(),
83
+ _conn: &Connection,
84
+ _qh: &QueueHandle<Self>,
85
+ ) {
86
+ match event {
87
+ manager::Event::Toplevel { toplevel } => {
88
+ println!("[+] nowe okno (obiekt {})", key(&toplevel));
89
+ // Uchwyt poznajemy po zdarzeniach; trzymamy go, by nie zniknął.
90
+ _state.handles.insert(key(&toplevel), toplevel);
91
+ }
92
+ manager::Event::Finished => println!("[*] kompozytor zakończył listę okien"),
93
+ _ => {}
94
+ }
95
+ }
96
+}
97
+
98
+impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for State {
99
+ fn event(
100
+ state: &mut Self,
101
+ handle: &ZwlrForeignToplevelHandleV1,
102
+ event: handle::Event,
103
+ _data: &(),
104
+ _conn: &Connection,
105
+ _qh: &QueueHandle<Self>,
106
+ ) {
107
+ match event {
108
+ handle::Event::Title { title } => {
109
+ println!(" tytuł: {title}");
110
+ state.titles.insert(key(handle), title);
111
+ }
112
+ handle::Event::AppId { app_id } => println!(" app_id: {app_id}"),
113
+ handle::Event::State { state: bytes } => {
114
+ println!(" stan: [{}]", decode_states(&bytes).join(", "));
115
+ }
116
+ handle::Event::Done => println!(" ---"),
117
+ handle::Event::Closed => {
118
+ let id = key(handle);
119
+ let title = state.titles.remove(&id).unwrap_or_default();
120
+ state.handles.remove(&id);
121
+ println!("[-] zamknięte: {title}");
122
+ }
123
+ _ => {}
124
+ }
125
+ }
126
+}
127
+
128
+fn main() -> ExitCode {
129
+ let connection = match Connection::connect_to_env() {
130
+ Ok(connection) => connection,
131
+ Err(err) => {
132
+ eprintln!("nie mogę połączyć się z Waylandem: {err}");
133
+ return ExitCode::FAILURE;
134
+ }
135
+ };
136
+
137
+ let (globals, mut queue) = match registry_queue_init::<State>(&connection) {
138
+ Ok(pair) => pair,
139
+ Err(err) => {
140
+ eprintln!("błąd rejestru Waylanda: {err}");
141
+ return ExitCode::FAILURE;
142
+ }
143
+ };
144
+ let qh = queue.handle();
145
+ let mut state = State::default();
146
+
147
+ // Bindujemy menedżera listy okien (wersja maks. 3).
148
+ if let Err(err) = globals.bind::<ZwlrForeignToplevelManagerV1, State, ()>(&qh, 1..=3, ()) {
149
+ eprintln!("kompozytor nie oferuje wlr-foreign-toplevel-management: {err}");
150
+ return ExitCode::FAILURE;
151
+ }
152
+
153
+ println!("nasłuchuję na listę okien (Ctrl+C kończy)...");
154
+ loop {
155
+ if let Err(err) = queue.blocking_dispatch(&mut state) {
156
+ eprintln!("błąd pętli zdarzeń: {err}");
157
+ return ExitCode::FAILURE;
158
+ }
159
+ }
160
+}