← PaganDE

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 +}