//! `wlr-foreign-toplevel-management` — lista i sterowanie oknami (serwer od zera). //! //! PO CO: to protokół, na którym opierają się panele i paski zadań (waybar, sfwbar, //! nasz przyszły panel PaganDE). Daje klientowi **listę okien** (tytuł, `app_id`, //! stan) oraz pozwala nimi **sterować** (aktywuj, zamknij, minimalizuj, maksymalizuj), //! czego nie potrafi nowszy `ext-foreign-toplevel-list-v1` (tylko odczyt). //! //! Smithay 0.7 nie ma tego protokołu, więc — podobnie jak `screencopy` — piszemy //! serwer sami. Definicje („kabel”) pochodzą z `wayland-protocols-wlr`, cała //! semantyka jest tutaj. //! //! Jak to działa: //! * `zwlr_foreign_toplevel_manager_v1` to global; każde okno ma dla każdego //! klienta osobny obiekt `zwlr_foreign_toplevel_handle_v1`, //! * stan wysyłamy **porcjami**: `title`/`app_id`/`state` kończy zdarzenie `done`, //! dzięki czemu klient widzi spójny obraz (zmiany są atomowe), //! * przy zniszczeniu okna wysyłamy `closed`. //! //! PO CO synchronizacja „przyrostowa” co klatkę, a nie haki we wszystkich //! miejscach: stan okna zmienia się w wielu ścieżkach (commit, snap, fokus, //! minimalizacja…). Jedna funkcja `sync` porównuje stan z ostatnio wysłanym i //! wysyła tylko różnice — mniej miejsc, w których można się pomylić. use std::{ collections::HashMap, sync::atomic::{AtomicU64, Ordering}, }; use smithay::{ reexports::{ wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_protocols_wlr::foreign_toplevel::v1::server::{ zwlr_foreign_toplevel_handle_v1::{self, State, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, }, wayland_server::{ backend::{ClientId, ObjectId}, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }, }, utils::IsAlive, wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}, }; use tracing::debug; use crate::{focus::KeyboardFocusTarget, state::MyCompositor, window::PaganWindow}; /// Maksymalna wersja protokołu obsługiwana przez `wayland-protocols-wlr 0.3`. const VERSION: u32 = 3; /// Tożsamość okna dla protokołu — stabilna przez całe życie okna. /// /// PO CO własny licznik, a nie np. wskaźnik: `Window` nie implementuje `Hash`/`Eq`, /// a numerek jest wygodny jako klucz mapy. #[derive(Debug, Clone, Copy)] struct ToplevelId(u64); /// Licznik identyfikatorów okien (globalny — kolejność nie ma znaczenia). static NEXT_TOPLEVEL_ID: AtomicU64 = AtomicU64::new(1); /// Dane obiektu `zwlr_foreign_toplevel_handle_v1`. /// /// Trzymamy tylko identyfikator okna — resztę (tytuł, stan) pamiętamy w jednym /// miejscu (`ToplevelEntry`), żeby nie duplikować prawdy. #[derive(Debug)] pub struct HandleUserData { window_id: u64, } /// Stan protokołu w kompozytorze. pub struct ForeignToplevelState { /// Obiekty menedżera, po jednym na klienta, który się podłączył. managers: Vec, /// Wpisy okien: co już wysłaliśmy i do kogo. toplevels: HashMap, } impl std::fmt::Debug for ForeignToplevelState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ForeignToplevelState") .field("managers", &self.managers.len()) .field("toplevels", &self.toplevels.len()) .finish() } } impl ForeignToplevelState { pub fn new() -> Self { Self { managers: Vec::new(), toplevels: HashMap::new(), } } } impl Default for ForeignToplevelState { fn default() -> Self { Self::new() } } /// Wpis jednego okna: uchwyty klientów + ostatnio wysłany stan (do porównania). struct ToplevelEntry { window: PaganWindow, /// (id menedżera, uchwyt) — jeden uchwyt na klienta. handles: Vec<(ObjectId, ZwlrForeignToplevelHandleV1)>, title: String, app_id: String, states: Vec, /// Czy trzeba wysłać pełny stan (nowy uchwyt albo zmiana danych). dirty: bool, } impl ToplevelEntry { fn new(window: PaganWindow) -> Self { Self { window, handles: Vec::new(), title: String::new(), app_id: String::new(), states: Vec::new(), dirty: true, } } } /// Rejestruje global `zwlr_foreign_toplevel_manager_v1`. pub fn init(display: &DisplayHandle) { display.create_global::(VERSION, ()); debug!("global zwlr_foreign_toplevel_manager_v1 zarejestrowany"); } /// Koduje listę stanów do tablicy bajtów w formacie protokołu (tablica `u32`). fn encode_states(states: &[State]) -> Vec { states .iter() .flat_map(|state| (*state as u32).to_ne_bytes()) .collect() } // --- Menedżer ----------------------------------------------------------------- impl GlobalDispatch for MyCompositor { fn bind( state: &mut MyCompositor, _handle: &DisplayHandle, _client: &Client, resource: New, _data: &(), data_init: &mut DataInit<'_, MyCompositor>, ) { let manager = data_init.init(resource, ()); // Nowy klient (panel) od razu dostanie listę istniejących okien — zrobi to // najbliższy `sync_foreign_toplevels`, bo tworzy uchwyty „przy okazji”. state.foreign_toplevel.managers.push(manager); // Wymuś pełną synchronizację, żeby nowy klient nie czekał na zmianę stanu. for entry in state.foreign_toplevel.toplevels.values_mut() { entry.dirty = true; } } } impl Dispatch for MyCompositor { fn request( state: &mut MyCompositor, _client: &Client, manager: &ZwlrForeignToplevelManagerV1, request: zwlr_foreign_toplevel_manager_v1::Request, _data: &(), _handle: &DisplayHandle, _data_init: &mut DataInit<'_, MyCompositor>, ) { match request { // Klient nie chce już nowych okien. Zgodnie z protokołem wysyłamy // `finished` i przestajemy go obsługiwać. zwlr_foreign_toplevel_manager_v1::Request::Stop => { manager.finished(); state .foreign_toplevel .managers .retain(|candidate| candidate != manager); } _ => {} } } fn destroyed( state: &mut MyCompositor, _client: ClientId, manager: &ZwlrForeignToplevelManagerV1, _data: &(), ) { state .foreign_toplevel .managers .retain(|candidate| candidate != manager); // Uchwyty tego klienta staną się bezużyteczne — usuwamy je przy okazji sync. for entry in state.foreign_toplevel.toplevels.values_mut() { entry.handles.retain(|(_, handle)| handle.is_alive()); entry.dirty = true; } } } // --- Uchwyt okna -------------------------------------------------------------- impl Dispatch for MyCompositor { fn request( state: &mut MyCompositor, _client: &Client, handle: &ZwlrForeignToplevelHandleV1, request: zwlr_foreign_toplevel_handle_v1::Request, data: &HandleUserData, _display: &DisplayHandle, _data_init: &mut DataInit<'_, MyCompositor>, ) { // Żądania sterujące zamieniamy na akcje menedżera okien. To jedyne miejsce, // w którym panel może wpływać na okna — trzymamy je cienkie. let Some(window) = state.window_by_foreign_id(data.window_id) else { return; }; match request { zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => { // `seat` pomijamy: mamy jedno siedzenie (single-seat desktop). state.focus_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::Close => { state.close_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => { state.minimize_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => { state.restore_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => { state.maximize_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => { state.unmaximize_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { .. } => { state.set_fullscreen_window(&window); } zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => { state.unset_fullscreen_window(&window); } // `set_rectangle` to tylko podpowiedź gdzie panel rysuje kafelek — // używana np. przy animacji minimalizacji. Nie potrzebujemy jej. zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => {} zwlr_foreign_toplevel_handle_v1::Request::Destroy => { remove_handle(state, data.window_id, handle); } _ => {} } } fn destroyed( state: &mut MyCompositor, _client: ClientId, handle: &ZwlrForeignToplevelHandleV1, data: &HandleUserData, ) { remove_handle(state, data.window_id, handle); } } /// Usuwa uchwyt z wpisu okna (wywoływane przy `destroy` i zniszczeniu obiektu). fn remove_handle(state: &mut MyCompositor, window_id: u64, handle: &ZwlrForeignToplevelHandleV1) { if let Some(entry) = state.foreign_toplevel.toplevels.get_mut(&window_id) { entry.handles.retain(|(_, candidate)| candidate != handle); } } // --- Synchronizacja stanu ----------------------------------------------------- impl MyCompositor { /// Znajduje okno po identyfikatorze protokołu. pub fn window_by_foreign_id(&self, window_id: u64) -> Option { self.foreign_toplevel .toplevels .get(&window_id) .map(|entry| entry.window.clone()) } /// Wysyła klientom różnice stanu okien (wołane raz na klatkę). /// /// Zbiera okna **zmapowane i zminimalizowane** (zminimalizowane też mają być /// na pasku zadań!), tworzy brakujące uchwyty i wysyła tylko to, co się zmieniło. pub fn sync_foreign_toplevels(&mut self) { if self.foreign_toplevel.managers.is_empty() { return; } // Kopia zasobów (tanie, to `Arc`) uwalnia pożyczkę `self.foreign_toplevel` // na czas mutowania wpisów. let managers = self.foreign_toplevel.managers.clone(); let display = self.display_handle.clone(); // 1) Wpisy po martwych oknach: powiadamiamy klientów zdarzeniem `closed`. let dead: Vec = self .foreign_toplevel .toplevels .iter() .filter(|(_, entry)| !entry.window.alive()) .map(|(id, _)| *id) .collect(); for id in dead { if let Some(entry) = self.foreign_toplevel.toplevels.remove(&id) { for (_, handle) in entry.handles { handle.closed(); } debug!(window_id = id, "obce-okno: zamknięte"); } } // 2) Bieżące okna: zmapowane + zminimalizowane. let mut live: Vec = self.space.elements().cloned().collect(); live.extend(self.minimized.iter().map(|entry| entry.window.clone())); for window in live { let id = foreign_id(&window); // Dane zbieramy PRZED pożyczeniem wpisu, żeby nie mieszać pożyczek `self`. let title = window_title(&window); let app_id = window_app_id(&window); let states = self.window_foreign_states(&window); let entry = self .foreign_toplevel .toplevels .entry(id) .or_insert_with(|| ToplevelEntry::new(window.clone())); if entry.title != title { entry.title = title; entry.dirty = true; } if entry.app_id != app_id { entry.app_id = app_id; entry.dirty = true; } if entry.states != states { entry.states = states; entry.dirty = true; } // 2a) Nowi klienci: dołóż uchwyt dla każdego menedżera, który go nie ma. for manager in &managers { if !manager.is_alive() { continue; } if entry .handles .iter() .any(|(manager_id, _)| manager_id == &manager.id()) { continue; } let Ok(client) = display.get_client(manager.id()) else { continue; }; let Ok(handle) = client .create_resource::( &display, manager.version(), HandleUserData { window_id: id }, ) else { continue; }; // Kolejność: najpierw zdarzenie `toplevel` (tworzy obiekt u klienta), // potem szczegóły i `done`. manager.toplevel(&handle); entry.handles.push((manager.id(), handle)); entry.dirty = true; } // 2b) Wyślij stan tylko wtedy, gdy coś się zmieniło. if entry.dirty { for (_, handle) in &entry.handles { if !handle.is_alive() { continue; } handle.title(entry.title.clone()); handle.app_id(entry.app_id.clone()); handle.state(encode_states(&entry.states)); handle.done(); } entry.dirty = false; } } } /// Wylicza stan okna widziany przez protokół (aktywne, zminimalizowane, …). fn window_foreign_states(&self, window: &PaganWindow) -> Vec { let mut states = Vec::new(); if self.minimized.iter().any(|entry| &entry.window == window) { states.push(State::Minimized); } let focused = self .seat .get_keyboard() .and_then(|keyboard| keyboard.current_focus()) .and_then(|target| match target { KeyboardFocusTarget::Window(focused) => Some(&focused == &window.0), // Popup ma własny fokus, ale to nie jest „aktywne okno” na pasku. KeyboardFocusTarget::Popup(_) => None, }) .unwrap_or(false); if focused { states.push(State::Activated); } if let Some(toplevel) = window.toplevel() { let mut maximized = false; let mut fullscreen = false; toplevel.with_pending_state(|pending| { maximized = pending.states.contains(xdg_toplevel::State::Maximized); fullscreen = pending.states.contains(xdg_toplevel::State::Fullscreen); }); if maximized { states.push(State::Maximized); } if fullscreen { states.push(State::Fullscreen); } } else if let Some(x11) = window.x11_surface() { if x11.is_maximized() { states.push(State::Maximized); } if x11.is_fullscreen() { states.push(State::Fullscreen); } } states } } /// Zwraca (i w razie potrzeby nadaje) stabilny identyfikator okna. fn foreign_id(window: &PaganWindow) -> u64 { let id = window .user_data() .get_or_insert(|| ToplevelId(NEXT_TOPLEVEL_ID.fetch_add(1, Ordering::Relaxed))); id.0 } /// Tytuł okna (Wayland: `xdg_toplevel`, X11: `WM_NAME`). fn window_title(window: &PaganWindow) -> String { if let Some(surface) = window.wl_surface() { if window.toplevel().is_some() { return with_states(&surface, |states| { states .data_map .get::() .and_then(|data| data.lock().unwrap().title.clone()) }) .unwrap_or_default(); } } if let Some(x11) = window.x11_surface() { return x11.title(); } String::new() } /// `app_id` okna (Wayland: `app_id`, X11: klasa/instancja, np. `firefox`). fn window_app_id(window: &PaganWindow) -> String { if let Some(surface) = window.wl_surface() { if window.toplevel().is_some() { return with_states(&surface, |states| { states .data_map .get::() .and_then(|data| data.lock().unwrap().app_id.clone()) }) .unwrap_or_default(); } } if let Some(x11) = window.x11_surface() { let class = x11.class(); if !class.is_empty() { return class.to_lowercase(); } return x11.instance().to_lowercase(); } String::new() }