🔒 Repository is read-only – file editing is disabled.

PaganDE/pagan-compositor/src/foreign_toplevel.rs main

492 linii Raw ← Powrót
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()
}