//! 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>>); impl OldGeometry { fn save(&self, geometry: Rectangle) { *self.0.borrow_mut() = Some(geometry); } fn restore(&self) -> Option> { 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 { 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::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 { 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, _y: Option, w: Option, h: Option, _reorder: Option, ) { // 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, _above: Option, ) { 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::() { 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::() .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::() .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) { 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);