🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
//! `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<ZwlrForeignToplevelManagerV1>,
/// Wpisy okien: co już wysłaliśmy i do kogo.
toplevels: HashMap<u64, ToplevelEntry>,
}
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<State>,
/// 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::<MyCompositor, ZwlrForeignToplevelManagerV1, ()>(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<u8> {
states
.iter()
.flat_map(|state| (*state as u32).to_ne_bytes())
.collect()
}
// --- Menedżer -----------------------------------------------------------------
impl GlobalDispatch<ZwlrForeignToplevelManagerV1, (), MyCompositor> for MyCompositor {
fn bind(
state: &mut MyCompositor,
_handle: &DisplayHandle,
_client: &Client,
resource: New<ZwlrForeignToplevelManagerV1>,
_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<ZwlrForeignToplevelManagerV1, (), MyCompositor> 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<ZwlrForeignToplevelHandleV1, HandleUserData, MyCompositor> 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<PaganWindow> {
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<u64> = 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<PaganWindow> = 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::<ZwlrForeignToplevelHandleV1, HandleUserData, MyCompositor>(
&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<State> {
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::<XdgToplevelSurfaceData>()
.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::<XdgToplevelSurfaceData>()
.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()
}