🔒 Repository is read-only – file editing is disabled.
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);