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

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

509 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
//! XWayland — obsługa aplikacji X11 w kompozytorze Waylanda.
//!
//! PO CO osobny moduł: integracja X11 jest z natury odseparowana od reszty —
//! uruchamiamy zewnętrzny serwer `Xwayland`, a sami występujemy jako **menedżer
//! okien X11** (reparenting WM). Smithay daje nam do tego `X11Wm`, ale to my
//! decydujemy, jak okna X11 trafiają do naszej przestrzeni, jak się zachowują
//! i jak obsługujemy schowek.
//!
//! Przepływ:
//! 1. `start_xwayland()` uruchamia `Xwayland` i wpina jego zdarzenia w calloop,
//! 2. gdy serwer zgłosi `Ready`, startujemy `X11Wm` (`XwmHandler`),
//! 3. każde okno X11 dostaje powiązaną `wl_surface` (przez `xwayland_shell`),
//!    dzięki czemu renderuje się i fokusuje **tak samo** jak okna Waylanda,
//! 4. ruch/rozmiar/snapowanie obsługują te same graby co dla Waylanda
//!    (`grabs.rs` rozgałęzia się po typie powierzchni).
//!
//! WAŻNE: wymaga binarki `Xwayland` w `PATH`. Jeśli jej nie ma, kompozytor
//! działa dalej — tylko bez aplikacji X11 (logujemy ostrzeżenie z instrukcją).

use std::{cell::RefCell, os::unix::io::OwnedFd, process::Stdio};

use smithay::{
    input::pointer::Focus,
    reexports::wayland_server::protocol::wl_surface::WlSurface,
    utils::{Logical, Point, Rectangle, SERIAL_COUNTER},
    wayland::{
        selection::{
            data_device::{
                clear_data_device_selection, current_data_device_selection_userdata,
                request_data_device_client_selection, set_data_device_selection,
            },
            primary_selection::{
                clear_primary_selection, current_primary_selection_userdata,
                request_primary_client_selection, set_primary_selection,
            },
            SelectionTarget,
        },
        xwayland_keyboard_grab::XWaylandKeyboardGrabHandler,
        xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
    },
    xwayland::{
        xwm::{Reorder, ResizeEdge as X11ResizeEdge, X11Window, XwmId},
        X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler,
    },
};
use tracing::{error, info, trace, warn};

use crate::{
    focus::KeyboardFocusTarget,
    grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab},
    state::MyCompositor,
    window::PaganWindow,
};

/// Zapamiętany prostokąt okna X11 przed maksymalizacją — żeby móc je przywrócić.
///
/// PO CO w `user_data` powierzchni X11: stan musi przeżyć między zdarzeniami,
/// a nie chcemy dodawać pól do `MyCompositor` zależnych od konkretnego okna.
#[derive(Debug, Default)]
struct OldGeometry(RefCell<Option<Rectangle<i32, Logical>>>);

impl OldGeometry {
    fn save(&self, geometry: Rectangle<i32, Logical>) {
        *self.0.borrow_mut() = Some(geometry);
    }

    fn restore(&self) -> Option<Rectangle<i32, Logical>> {
        self.0.borrow_mut().take()
    }
}

impl MyCompositor {
    /// Znajduje nasz element przestrzeni odpowiadający oknu X11.
    ///
    /// PO CO po powierzchni, a nie po samym `X11Surface`: w przestrzeni trzymamy
    /// `PaganWindow`, a porównanie robimy po powiązanej `X11Surface` (tak jak anvil).
    fn element_for_x11(&self, window: &X11Surface) -> Option<PaganWindow> {
        self.space
            .elements()
            .find(|element| matches!(element.x11_surface(), Some(w) if w == window))
            .cloned()
    }

    /// Uruchamia XWayland i wpina obsługę jego zdarzeń w pętlę główną.
    ///
    /// PO CO osobna metoda, wołana przez backendy: tworzy źródło calloop, więc
    /// musi mieć `LoopHandle` — tak samo jak `MyCompositor::init`.
    ///
    /// Gdy w systemie nie ma binarki `Xwayland`, wypisujemy instrukcję i wracamy:
    /// kompozytor działa dalej, tylko bez aplikacji X11 (świadome, miękkie
    /// zachowanie — brak XWaylanda nie powinien blokować startu sesji).
    pub fn start_xwayland(&mut self) {
        let (xwayland, client) = match XWayland::spawn(
            &self.display_handle,
            None,
            std::iter::empty::<(String, String)>(),
            true,
            Stdio::null(),
            Stdio::null(),
            |_| (),
        ) {
            Ok(pair) => pair,
            Err(err) => {
                warn!(
                    %err,
                    "nie udało się uruchomić XWayland — aplikacje X11 będą niedostępne. \
                     Zainstaluj binarkę `Xwayland` (PaganOS: `sudo pag install xorg-xwayland`, \
                     Arch/Manjaro: `sudo pacman -S xorg-xwayland`)."
                );
                return;
            }
        };

        let ret = self.handle.clone().insert_source(
            xwayland,
            move |event, _, data: &mut MyCompositor| match event {
                XWaylandEvent::Ready {
                    x11_socket,
                    display_number,
                } => match X11Wm::start_wm(data.handle.clone(), x11_socket, client.clone()) {
                    Ok(wm) => {
                        data.xwm = Some(wm);
                        data.xdisplay = Some(display_number);
                        Self::publish_display(display_number);
                        info!(display = display_number, "XWayland gotowy (DISPLAY=:N)");
                    }
                    Err(err) => warn!(%err, "nie udało się wystartować menedżera okien X11"),
                },
                XWaylandEvent::Error => warn!("XWayland nie wystartował"),
            },
        );

        if let Err(err) = ret {
            error!(%err, "nie udało się dodać XWayland do pętli zdarzeń");
        }
    }

    /// Publikuje numer wyświetlacza XWayland dla launcherów (panel, skrypty).
    ///
    /// PO CO: aplikacje uruchamiane przez panel dziedziczą `DISPLAY` sesji XFCE
    /// i lądują na pulpicie XFCE — POZA naszym kompozytorem (a wtedy nasza
    /// dekoracja jest gdzie indziej niż okno). Panel czyta ten plik i ustawia
    /// dzieciom `DISPLAY` wskazujące na NASZ XWayland.
    fn publish_display(display_number: u32) {
        let Some(path) = Self::display_file() else {
            return;
        };
        if let Some(dir) = path.parent() {
            let _ = std::fs::create_dir_all(dir);
        }
        match std::fs::write(&path, format!(":{display_number}")) {
            Ok(()) => info!(
                path = %path.display(),
                display = display_number,
                "opublikowano DISPLAY XWayland dla launcherów"
            ),
            Err(err) => {
                warn!(%err, path = %path.display(), "nie udało się zapisać DISPLAY XWayland")
            }
        }
    }

    /// Ścieżka pliku z numerem wyświetlacza XWayland (wspólna z panelem).
    ///
    /// PO CO w `XDG_RUNTIME_DIR`: to katalog prywatny sesji użytkownika, więc
    /// nie zaśmiecamy `$HOME` i nie ma ryzyka konfliktu między sesjami.
    pub(crate) fn display_file() -> Option<std::path::PathBuf> {
        std::env::var("XDG_RUNTIME_DIR").ok().map(|dir| {
            std::path::PathBuf::from(dir)
                .join("pagan")
                .join("xwayland-display")
        })
    }
}

impl XWaylandShellHandler for MyCompositor {
    fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
        &mut self.xwayland_shell_state
    }
}

impl XWaylandKeyboardGrabHandler for MyCompositor {
    /// Na czyj cel fokusa klawiatury zamienić powierzchnię XWaylanda, gdy ta
    /// prosi o grab (np. gra przechwytująca klawiaturę).
    fn keyboard_focus_for_xsurface(&self, surface: &WlSurface) -> Option<KeyboardFocusTarget> {
        let window = self.window_for_surface(surface)?;
        Some(KeyboardFocusTarget::Window(window.0.clone()))
    }
}

impl XwmHandler for MyCompositor {
    fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
        self.xwm
            .as_mut()
            .expect("XwmHandler wołane bez uruchomionego X11Wm")
    }

    fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}

    fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}

    fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
        // Dopiero `set_mapped(true)` czyni okno widocznym — jako WM mamy kontrolę.
        if let Err(err) = window.set_mapped(true) {
            warn!(%err, "X11: nie udało się zmapować okna");
            return;
        }

        let element = PaganWindow::new_x11(window.clone());
        // Ta sama polityka co dla okien Waylanda: kaskada na monitorze pod kursorem.
        self.place_new_window(element.clone());

        // Dopasuj okno X11 do nadanej geometrii **TREŚCI**, a nie ramki.
        //
        // UWAGA (naprawiony błąd): `space.element_bbox()` zwraca prostokąt RAMY,
        // czyli z paskiem SSD (bo nasz `bbox` go obejmuje). Użycie go tutaj
        // przesuwało okno X11 o wysokość paska i rozciągało je w dół — aplikacja
        // rysowała treść w innym miejscu niż nasz pasek, co wyglądało jak
        // „przesunięta dekoracja". Konfigurujemy więc samą treść.
        if let Some(location) = self.space.element_location(&element) {
            let size = element.geometry().size;
            let _ = window.configure(Rectangle::new(location, size));
        }

        // Nowe okno X11 przejmuje fokus tak samo jak okno Waylanda — inaczej
        // klawisze trafiałyby do poprzedniego okna, a pasek SSD pozostałby
        // przygaszony (stan „nieaktywne”) mimo że to okno jest na wierzchu.
        let serial = SERIAL_COUNTER.next_serial();
        self.raise_and_focus(&element, serial);

        trace!(title = %window.title(), "X11: zmapowano okno");
    }

    fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
        // Okien override-redirect (menu, tooltipy, popupy) NIE zarządzamy —
        // odtwarzamy tylko ich własną pozycję.
        let location = window.geometry().loc;
        self.space
            .map_element(PaganWindow::new_x11(window), location, true);
    }

    fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
        if let Some(element) = self.element_for_x11(&window) {
            self.space.unmap_elem(&element);
        }
        if !window.is_override_redirect() {
            let _ = window.set_mapped(false);
        }
    }

    fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {}

    fn configure_request(
        &mut self,
        _xwm: XwmId,
        window: X11Surface,
        _x: Option<i32>,
        _y: Option<i32>,
        w: Option<u32>,
        h: Option<u32>,
        _reorder: Option<Reorder>,
    ) {
        // Akceptujemy tylko rozmiar — pozycją zarządza kompozytor.
        let mut geometry = window.geometry();
        if let Some(w) = w {
            geometry.size.w = w as i32;
        }
        if let Some(h) = h {
            geometry.size.h = h as i32;
        }
        let _ = window.configure(geometry);
    }

    fn configure_notify(
        &mut self,
        _xwm: XwmId,
        window: X11Surface,
        geometry: Rectangle<i32, Logical>,
        _above: Option<X11Window>,
    ) {
        if let Some(element) = self.element_for_x11(&window) {
            // X11 podaje pozycję TREŚCI; dla SSD pilnujemy, żeby pasek był na ekranie.
            let location = self.adjust_for_ssd(&element, geometry.loc);
            self.space.map_element(element, location, false);
        }
    }

    fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let Some(old_geometry) = self.space.element_bbox(&element) else {
            return;
        };

        let output = self
            .space
            .outputs_for_element(&element)
            .first()
            .cloned()
            .or_else(|| self.space.outputs().next().cloned());
        let Some(output) = output else { return };
        // Obszar ROBOCZY (bez panelu): zmaksymalizowane okno ma zostawić pasek
        // systemowy widoczny, tak jak w każdym innym menedżerze okien.
        let Some(area) = self.output_work_area(&output) else {
            return;
        };

        let _ = window.set_maximized(true);
        let _ = window.configure(area);
        window.user_data().insert_if_missing(OldGeometry::default);
        if let Some(data) = window.user_data().get::<OldGeometry>() {
            data.save(old_geometry);
        }
        self.space.map_element(element, area.loc, false);
    }

    fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let _ = window.set_maximized(false);
        if let Some(old) = window
            .user_data()
            .get::<OldGeometry>()
            .and_then(|d| d.restore())
        {
            let _ = window.configure(old);
            self.space.map_element(element, old.loc, false);
        }
    }

    fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let output = self
            .space
            .outputs_for_element(&element)
            .first()
            .cloned()
            .or_else(|| self.space.outputs().next().cloned());
        let Some(output) = output else { return };
        let Some(area) = self.space.output_geometry(&output) else {
            return;
        };

        let _ = window.set_fullscreen(true);
        let _ = window.configure(area);
        self.space.map_element(element, area.loc, false);
    }

    fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let _ = window.set_fullscreen(false);
        if let Some(area) = self.space.element_bbox(&element) {
            let _ = window.configure(area);
        }
    }

    fn resize_request(
        &mut self,
        _xwm: XwmId,
        window: X11Surface,
        _button: u32,
        edges: X11ResizeEdge,
    ) {
        let Some(start_data) = self.pointer.grab_start_data() else {
            return;
        };
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let geometry = element.geometry();
        let location = self
            .space
            .element_location(&element)
            .unwrap_or_else(|| Point::from((0, 0)));

        let grab = PointerResizeSurfaceGrab {
            start_data,
            window: element,
            edges: edges.into(),
            initial_window_location: location,
            initial_window_size: geometry.size,
            last_window_size: geometry.size,
        };
        let pointer = self.pointer.clone();
        pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
    }

    fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) {
        let Some(start_data) = self.pointer.grab_start_data() else {
            return;
        };
        let Some(element) = self.element_for_x11(&window) else {
            return;
        };
        let mut initial_window_location = self
            .space
            .element_location(&element)
            .unwrap_or_else(|| Point::from((0, 0)));

        // Przeciągnięcie zmaksymalizowanego okna X11 najpierw je przywraca
        // (spójnie z zachowaniem okien Waylanda).
        if window.is_maximized() {
            let _ = window.set_maximized(false);
            let pos = self.pointer.current_location();
            initial_window_location = Point::from((pos.x as i32, pos.y as i32));
            if let Some(old) = window
                .user_data()
                .get::<OldGeometry>()
                .and_then(|d| d.restore())
            {
                let _ = window.configure(Rectangle::new(initial_window_location, old.size));
            }
        }

        let initial_window_size = window.geometry().size;
        let grab = PointerMoveSurfaceGrab {
            start_data,
            window: element,
            initial_window_location,
            initial_window_size,
            current_snap: None,
        };
        let pointer = self.pointer.clone();
        pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
    }

    // ── Schowek między X11 a Waylandem ──
    // Bez tego kopiuj/wklej między aplikacją X11 a Waylandem nie działa.

    fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
        // Zezwalamy tylko, gdy fokus klawiatury należy do OKNA X11 tego samego XWM.
        if let Some(keyboard) = self.seat.get_keyboard() {
            if let Some(KeyboardFocusTarget::Window(window)) = keyboard.current_focus() {
                if let Some(surface) = window.x11_surface() {
                    return surface.xwm_id() == Some(xwm);
                }
            }
        }
        false
    }

    fn send_selection(
        &mut self,
        _xwm: XwmId,
        selection: SelectionTarget,
        mime_type: String,
        fd: OwnedFd,
    ) {
        match selection {
            SelectionTarget::Clipboard => {
                if let Err(err) = request_data_device_client_selection(&self.seat, mime_type, fd) {
                    error!(?err, "X11: nie udało się pobrać schowka Waylanda");
                }
            }
            SelectionTarget::Primary => {
                if let Err(err) = request_primary_client_selection(&self.seat, mime_type, fd) {
                    error!(
                        ?err,
                        "X11: nie udało się pobrać selekcji pierwotnej Waylanda"
                    );
                }
            }
        }
    }

    fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
        trace!(?selection, ?mime_types, "X11: nowa selekcja");
        match selection {
            SelectionTarget::Clipboard => {
                set_data_device_selection(&self.display_handle, &self.seat, mime_types, ())
            }
            SelectionTarget::Primary => {
                set_primary_selection(&self.display_handle, &self.seat, mime_types, ())
            }
        }
    }

    fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
        match selection {
            SelectionTarget::Clipboard => {
                if current_data_device_selection_userdata(&self.seat).is_some() {
                    clear_data_device_selection(&self.display_handle, &self.seat)
                }
            }
            SelectionTarget::Primary => {
                if current_primary_selection_userdata(&self.seat).is_some() {
                    clear_primary_selection(&self.display_handle, &self.seat)
                }
            }
        }
    }

    fn disconnected(&mut self, _xwm: XwmId) {
        warn!("XWayland: utracono połączenie z serwerem X11");
        self.xwm = None;
        self.xdisplay = None;
    }
}

// Delegacje protokołów: `xwayland_shell` (skojarzenie okna X11 z wl_surface)
// oraz grab klawiatury X11.
smithay::delegate_xwayland_shell!(MyCompositor);
smithay::delegate_xwayland_keyboard_grab!(MyCompositor);