🔒 Repository is read-only – file editing is disabled.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
//! XWM — menedżer okien X11 dla XWayland.
//!
//! Selene jest kompozytorem, więc rolę menedżera okien X11 musi pełnić sam: przejmuje
//! `SubstructureRedirect` na oknie głównym X, zarządza oknami X11 i tłumaczy ich stan na
//! model Luny (tytuł, typ okna, zamykanie, maksymalizacja, geometria).
//!
//! Dlaczego osobny wątek: X11 ma własny protokół i własne gniazdo, a pętla protokołu
//! Waylanda nie może się na nim blokować. Kompozytor rozmawia z XWM przez kanał
//! ([`XwmCommand`]), a odczytuje stan przez [`Xwm::info_for_serial`].
//!
//! Powiązanie okno X11 ↔ powierzchnia Wayland: Xwayland wysyła do okna X11 komunikat
//! `WL_SURFACE_SERIAL` z serialem, który wcześniej ustawił na powierzchni
//! (`xwayland_surface_v1.set_serial`). Dlatego trzymamy mapę `serial → okno X11`.
use std::collections::HashMap;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use wayland_server::Resource;
use x11rb::connection::Connection;
use x11rb::protocol::composite::{ConnectionExt as CompositeExt, Redirect};
use x11rb::protocol::xproto::{
Atom, AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux,
ConnectionExt, EventMask, InputFocus, PropMode, Window,
};
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::x11_utils::Serialize;
use crate::compositor::SurfaceData;
use crate::state::State;
use crate::sync::LockExt;
/// Rodzaj okna X11 (z `_NET_WM_WINDOW_TYPE`), o tyle, o ile wpływa na wygląd.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum X11Kind {
#[default]
Normal,
Dialog,
/// Menu, podpowiedź, listy rozwijane — bez dekoracji, nad innymi oknami.
Transient,
}
/// Stan okna X11, którego nie da się odczytać z Waylanda.
#[derive(Debug, Clone, Default)]
pub struct X11Info {
pub title: String,
pub override_redirect: bool,
pub kind: X11Kind,
/// Klient wyraźnie poprosił o brak ramki (`_MOTIF_WM_HINTS` bez dekoracji).
/// Tak robią aplikacje z własnymi dekoracjami (CSD) — nie dokładamy wtedy naszego paska.
pub client_decorations: bool,
/// Pozycja i rozmiar okna w przestrzeni roota (dla menu/podpowiedzi).
/// Menu X11 (override-redirect) ma współrzędne absolutne — musimy je znać,
/// żeby narysować je tam, gdzie klient je umieścił, a nie w kaskadzie.
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl X11Info {
/// Czy okno ma dostać nasze dekoracje (menu i podpowiedzi — nie).
pub fn decorated(&self) -> bool {
!self.override_redirect && self.kind != X11Kind::Transient && !self.client_decorations
}
}
/// Zmiana stanu okna X11, o której kompozytor musi się dowiedzieć (XWM to osobny wątek).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X11Change {
Fullscreen(bool),
Maximized(bool),
/// Klient prosi o zminimalizowanie/przywrócenie (`WM_CHANGE_STATE` / IconicState).
Minimize(bool),
/// Interaktywne przesunięcie od pozycji wskaźnika (`_NET_WM_MOVERESIZE`, kierunek 8).
Move {
x: i32,
y: i32,
},
/// Interaktywna zmiana rozmiaru (bitmaska jak w `seat`: 1=góra, 2=dół, 4=lewo, 8=prawo).
Resize {
edges: u8,
x: i32,
y: i32,
},
}
/// Polecenia z kompozytora do wątku XWM.
#[derive(Debug, Clone)]
pub enum XwmCommand {
Close(Window),
SetGeometry {
window: Window,
x: i32,
y: i32,
w: u32,
h: u32,
},
SetActive(Option<Window>),
SetMaximized {
window: Window,
on: bool,
},
/// Informuje klienta X11, ile miejsca zajmują nasze dekoracje.
SetFrameExtents {
window: Window,
top: u32,
},
/// Prosi klienta o wejście/wyjście z pełnego ekranu (EWMH `_NET_WM_STATE_FULLSCREEN`).
SetFullscreen {
window: Window,
on: bool,
},
}
/// Współdzielony stan XWM (czytany przez kompozytor).
#[derive(Default)]
pub struct XwmState {
/// `seriał powierzchni Wayland → okno X11` (z komunikatu `WL_SURFACE_SERIAL`).
by_serial: HashMap<u64, Window>,
/// `okno X11 → informacje` (tytuł, typ, override-redirect).
windows: HashMap<Window, X11Info>,
/// Okna zarządzane (na liście klientów), w kolejności pojawiania się.
managed: Vec<Window>,
/// Zmiany stanu zgłoszone przez klientów X11 (np. `_NET_WM_STATE_FULLSCREEN`).
changes: Vec<(Window, X11Change)>,
/// Ostatnio znany stan EWMH okna: `(fullscreen, maximized)`.
ewmh: HashMap<Window, (bool, bool)>,
/// Czy udało się zostać menedżerem okien X11.
pub is_wm: bool,
}
impl XwmState {
/// Informacje o oknie powiązanym z serialem powierzchni Wayland.
pub fn info_for_serial(&self, serial: u64) -> Option<X11Info> {
let win = *self.by_serial.get(&serial)?;
self.windows.get(&win).cloned()
}
/// Okno X11 powiązane z serialem.
pub fn window_for_serial(&self, serial: u64) -> Option<Window> {
self.by_serial.get(&serial).copied()
}
/// Serial powierzchni Wayland odpowiadający oknu X11.
pub fn serial_for_window(&self, window: Window) -> Option<u64> {
self.by_serial
.iter()
.find(|(_, w)| **w == window)
.map(|(serial, _)| *serial)
}
/// Zmiany stanu oczekujące na odebranie przez kompozytor.
pub fn take_changes(&mut self) -> Vec<(Window, X11Change)> {
std::mem::take(&mut self.changes)
}
}
/// Uchwyt XWM: kanał poleceń + współdzielony stan.
#[derive(Clone)]
pub struct Xwm {
tx: Sender<XwmCommand>,
state: Arc<Mutex<XwmState>>,
}
/// Globalny uchwyt XWM.
///
/// Jest dokładnie jeden kompozytor w procesie, a XWM powstaje raz przy starcie — dzięki
/// temu kompozytor nie musi przekazywać go przez cały model okien.
static GLOBAL: OnceLock<Arc<Xwm>> = OnceLock::new();
/// Rejestruje działający XWM jako globalny.
pub fn set_global(xwm: Xwm) {
let _ = GLOBAL.set(Arc::new(xwm));
}
/// Globalny XWM (jeśli X11 działa).
pub fn global() -> Option<&'static Arc<Xwm>> {
GLOBAL.get()
}
impl Xwm {
pub fn info_for_serial(&self, serial: u64) -> Option<X11Info> {
self.state.lock_or_recover().info_for_serial(serial)
}
pub fn window_for_serial(&self, serial: u64) -> Option<Window> {
self.state.lock_or_recover().window_for_serial(serial)
}
pub fn serial_for_window(&self, window: Window) -> Option<u64> {
self.state.lock_or_recover().serial_for_window(window)
}
pub fn is_wm(&self) -> bool {
self.state.lock_or_recover().is_wm
}
fn send(&self, cmd: XwmCommand) {
// Kanał jest jedynym sposobem rozmowy z wątkiem X11; jego zamknięcie (X11 padł)
// nie może przewrócić kompozytora.
let _ = self.tx.send(cmd);
}
pub fn close(&self, window: Window) {
self.send(XwmCommand::Close(window));
}
pub fn set_geometry(&self, window: Window, x: i32, y: i32, w: u32, h: u32) {
self.send(XwmCommand::SetGeometry { window, x, y, w, h });
}
pub fn set_active(&self, window: Option<Window>) {
self.send(XwmCommand::SetActive(window));
}
pub fn set_maximized(&self, window: Window, on: bool) {
self.send(XwmCommand::SetMaximized { window, on });
}
pub fn set_frame_extents(&self, window: Window, top: u32) {
self.send(XwmCommand::SetFrameExtents { window, top });
}
pub fn set_fullscreen(&self, window: Window, on: bool) {
self.send(XwmCommand::SetFullscreen { window, on });
}
/// Odbiera zmiany stanu zgłoszone przez klientów X11 (woła kompozytor).
pub fn take_changes(&self) -> Vec<(Window, X11Change)> {
self.state.lock_or_recover().take_changes()
}
}
/// Wysokość naszego paska tytułu — ile miejsca oddajemy klientom X11 w `_NET_FRAME_EXTENTS`.
/// Musi być równa [`crate::state::TITLEBAR`], bo dokładnie tyle miejsca zabiera nasz pasek.
pub const FRAME_TOP: u32 = crate::state::TITLEBAR as u32;
/// Uruchamia XWM. **Nie blokuje kompozytora.**
///
/// Xwayland potrzebuje chwili, zanim zacznie przyjmować połączenia, dlatego łączenie
/// ponawiamy w wątku; gdy się uda, XWM rejestruje się jako globalny ([`global`]).
/// Kompozytor nie musi więc czekać i od razu obsługuje klientów Waylanda.
pub fn spawn(display: u32) {
let (tx, rx) = std::sync::mpsc::channel();
let state: Arc<Mutex<XwmState>> = Arc::new(Mutex::new(XwmState::default()));
let st = Arc::clone(&state);
std::thread::spawn(move || {
// 1) Połączenie z X11 — z ponowieniami (Xwayland wstaje asynchronicznie).
let Some((conn, screen)) = connect_retry(display, Duration::from_secs(8)) else {
return;
};
// Dopiero teraz X11 naprawdę działa — sesja może dostać `DISPLAY`
// (wcześniej gniazdo bywało martwe i aplikacje nie mogły się połączyć).
crate::xwayland::write_display_marker(display);
let root = conn.setup().roots[screen].root;
let Ok(mut atoms) = Atoms::new(&conn) else {
return;
};
let is_wm = become_wm(&conn, root, &mut atoms).unwrap_or(false);
st.lock_or_recover().is_wm = is_wm;
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/xwm: X11 gotowy (ekran :{display}), menedżer okien: {}",
if is_wm { "tak" } else { "nie" }
);
}
// 2) Rejestracja globalna — kompozytor czyta `global()` przy każdej operacji,
// więc podłączenie „w locie” jest bezpieczne.
set_global(Xwm {
tx: tx.clone(),
state: Arc::clone(&st),
});
run(conn, root, atoms, rx, st);
});
}
/// Łączy się z X11, ponawiając próbę aż do `timeout`.
fn connect_retry(display: u32, timeout: Duration) -> Option<(RustConnection, usize)> {
let name = format!(":{display}");
let deadline = std::time::Instant::now() + timeout;
loop {
match x11rb::connect(Some(&name)) {
Ok(v) => return Some(v),
Err(e) => {
if std::time::Instant::now() >= deadline {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/xwm: brak połączenia z X11: {e}");
}
return None;
}
}
}
std::thread::sleep(Duration::from_millis(50));
}
}
/// Pętla wątku X11: polecenia z kompozytora + zdarzenia z X11.
fn run(
conn: RustConnection,
root: Window,
atoms: Atoms,
rx: Receiver<XwmCommand>,
state: Arc<Mutex<XwmState>>,
) {
loop {
// 1) Polecenia od kompozytora.
while let Ok(cmd) = rx.try_recv() {
apply_command(&conn, &root, &atoms, &state, cmd);
}
// 2) Zdarzenia X11.
let mut handled = false;
while let Ok(Some(event)) = conn.poll_for_event() {
handled = true;
handle_event(&conn, root, &atoms, &state, event);
}
let _ = conn.flush();
if !handled {
std::thread::sleep(Duration::from_millis(4));
}
}
}
/// Przejmuje rolę menedżera okien X11 (`SubstructureRedirect`).
fn become_wm(conn: &RustConnection, root: Window, atoms: &mut Atoms) -> Option<bool> {
let aux = ChangeWindowAttributesAux::new()
.event_mask(EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY);
// Gdy inny menedżer okien już działa, X zwraca `Access` — nie walczymy z nim.
let ok = conn
.change_window_attributes(root, &aux)
.ok()
.and_then(|c| c.check().ok())
.is_some();
if !ok {
return Some(false);
}
// `_NET_SUPPORTING_WM_CHECK`: dowód dla klientów, że mamy EWMH.
let wm_window = conn.generate_id().ok()?;
conn.create_window(
x11rb::COPY_FROM_PARENT as u8,
wm_window,
root,
0,
0,
1,
1,
0,
x11rb::protocol::xproto::WindowClass::INPUT_OUTPUT,
x11rb::COPY_FROM_PARENT as u32,
&Default::default(),
)
.ok()?;
set_prop32(
conn,
root,
atoms.net_supporting_wm_check,
AtomEnum::WINDOW.into(),
&[wm_window],
);
set_prop32(
conn,
wm_window,
atoms.net_supporting_wm_check,
AtomEnum::WINDOW.into(),
&[wm_window],
);
set_prop_str(conn, wm_window, AtomEnum::WM_NAME.into(), "Luna");
// Lista obsługiwanych rozszerzeń EWMH, których naprawdę używamy.
let supported: Vec<Atom> = vec![
atoms.net_wm_state,
atoms.net_wm_state_maximized_vert,
atoms.net_wm_state_maximized_horz,
atoms.net_wm_state_fullscreen,
atoms.net_close_window,
atoms.net_active_window,
atoms.net_client_list,
atoms.net_wm_name,
atoms.net_wm_window_type,
atoms.net_frame_extents,
];
set_prop32(
conn,
root,
atoms.net_supported,
AtomEnum::ATOM.into(),
&supported,
);
// Composite: w trybie rootless Xwayland tworzy powierzchnię Wayland dla okna dopiero
// wtedy, gdy okno jest przekierowane (`redirectDraw == RedirectDrawManual`). Bez tego
// klient X11 działa w tle, ale kompozytor nie dostaje żadnej powierzchni do rysowania.
// `RedirectSubwindows` obejmuje zarówno okna już istniejące, jak i wszystkie przyszłe
// (X serwera przekierowuje je w hooku `CreateWindow`), więc wystarczy jedno wywołanie.
if !enable_composite(conn, root) {
if std::env::var("SELENE_STATS").is_ok() {
eprintln!("selene/xwm: Composite niedostępny — okna X11 nie będą widoczne");
}
}
let _ = conn.flush();
Some(true)
}
/// Włącza przekierowanie Composite dla okien potomnych roota.
fn enable_composite(conn: &RustConnection, root: Window) -> bool {
let version = conn
.composite_query_version(0, 4)
.ok()
.and_then(|c| c.reply().ok());
let Some(version) = version else {
return false;
};
let ok = conn
.composite_redirect_subwindows(root, Redirect::MANUAL)
.ok()
.and_then(|c| c.check().ok())
.is_some();
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/xwm: Composite {}.{} → przekierowanie okien: {}",
version.major_version,
version.minor_version,
if ok { "tak" } else { "nie" }
);
}
ok
}
fn handle_event(
conn: &RustConnection,
root: Window,
atoms: &Atoms,
state: &Arc<Mutex<XwmState>>,
event: Event,
) {
match event {
Event::CreateNotify(e) => {
// Zmiany własności (`_NET_WM_NAME`, `_NET_WM_WINDOW_TYPE`, `_MOTIF_WM_HINTS`)
// trafiają **do okna**, nie do roota — musimy wybrać maskę na samym oknie.
// Każdy klient ma własną maskę, więc nie nadpisujemy wyboru aplikacji.
if e.parent == root {
let aux = ChangeWindowAttributesAux::new()
.event_mask(EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY);
let _ = conn.change_window_attributes(e.window, &aux);
// Zapamiętaj pozycję od razu — menu może się pojawić zanim przyjdzie
// `WL_SURFACE_SERIAL`, a wtedy chcemy je postawić we właściwym miejscu.
{
let mut st = state.lock_or_recover();
let info = st.windows.entry(e.window).or_default();
info.x = i32::from(e.x);
info.y = i32::from(e.y);
info.width = i32::from(e.width);
info.height = i32::from(e.height);
}
}
}
Event::ConfigureNotify(e) => {
let mut st = state.lock_or_recover();
let info = st.windows.entry(e.window).or_default();
info.x = i32::from(e.x);
info.y = i32::from(e.y);
info.width = i32::from(e.width);
info.height = i32::from(e.height);
}
Event::MapRequest(e) => {
let over = is_override_redirect(conn, e.window);
let info = read_info(conn, atoms, e.window);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/xwm: MapRequest 0x{:x} '{}' over={} csd={}",
e.window, info.title, over, info.client_decorations
);
}
{
let mut info = info;
info.override_redirect = over;
insert_info(state, e.window, info);
}
if !over {
let mut st = state.lock_or_recover();
if !st.managed.contains(&e.window) {
st.managed.push(e.window);
}
}
// Menu i podpowiedzi mapujemy od razu; reszta i tak trafia do naszego modelu
// przez `commit` powierzchni Wayland — mapujemy zawsze, żeby klient żył.
let mapped = conn.map_window(e.window);
let _ = conn.flush();
if std::env::var("SELENE_STATS").is_ok() {
let state = conn
.get_window_attributes(e.window)
.ok()
.and_then(|c| c.reply().ok())
.map(|a| a.map_state);
eprintln!(
"selene/xwm: map_window 0x{:x} -> ok={} state={:?}",
e.window,
mapped.is_ok(),
state
);
}
publish_client_list(conn, root, atoms, state);
}
Event::ConfigureRequest(e) => {
// Klient prosi o geometrię — spełniamy (nie mamy własnej polityki kafelkowej).
let aux = ConfigureWindowAux::new()
.x(i32::from(e.x))
.y(i32::from(e.y))
.width(u32::from(e.width))
.height(u32::from(e.height));
let _ = conn.configure_window(e.window, &aux);
let _ = conn.flush();
}
Event::PropertyNotify(e) => {
if e.atom == atoms.net_wm_name
|| e.atom == AtomEnum::WM_NAME.into()
|| e.atom == atoms.net_wm_window_type
|| e.atom == atoms.motif_wm_hints
{
let info = read_info(conn, atoms, e.window);
if std::env::var("SELENE_STATS").is_ok() && e.atom == atoms.motif_wm_hints {
eprintln!(
"selene/xwm: 0x{:x} dekoracje klienta: {}",
e.window, info.client_decorations
);
}
insert_info(state, e.window, info);
}
}
Event::ClientMessage(e) => {
if e.type_ == atoms.wl_surface_serial {
// Powiązanie okno X11 ↔ powierzchnia Wayland (patrz opis modułu).
let serial = serial_from_client_message(&e.data);
if serial != 0 {
let info = read_info(conn, atoms, e.window);
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/xwm: WL_SURFACE_SERIAL 0x{:x} '{}' serial={}",
e.window, info.title, serial
);
}
let mut st = state.lock_or_recover();
st.by_serial.insert(serial, e.window);
drop(st);
insert_info(state, e.window, info);
}
} else if e.type_ == atoms.net_close_window {
request_close(conn, e.window);
} else if e.type_ == atoms.net_wm_state {
// Klient prosi o zmianę stanu (pełny ekran, maksymalizacja).
// `data = [action, atom1, atom2, source, 0]`; akcja 0 = usuń, 1 = dodaj,
// 2 = przełącz. Samo ustawienie robi kompozytor — my tylko przekazujemy
// życzenie (XWM nie zna modelu okien Waylanda).
let data = e.data.as_data32();
let action = data[0];
let mut st = state.lock_or_recover();
let mut flags = *st.ewmh.get(&e.window).unwrap_or(&(false, false));
let mut pending: Vec<X11Change> = Vec::new();
for state_atom in [data[1], data[2]] {
if state_atom == 0 {
continue;
}
let (fullscreen, current) = if state_atom == atoms.net_wm_state_fullscreen {
(true, flags.0)
} else if state_atom == atoms.net_wm_state_maximized_vert
|| state_atom == atoms.net_wm_state_maximized_horz
{
(false, flags.1)
} else {
continue;
};
let on = match action {
0 => false,
1 => true,
2 => !current,
_ => continue,
};
let change = if fullscreen {
flags.0 = on;
X11Change::Fullscreen(on)
} else {
flags.1 = on;
X11Change::Maximized(on)
};
pending.push(change);
}
st.ewmh.insert(e.window, flags);
st.changes
.extend(pending.into_iter().map(|c| (e.window, c)));
} else if e.type_ == atoms.wm_change_state {
// `WM_CHANGE_STATE`: 1 = NormalState, 3 = IconicState (minimalizacja).
let data = e.data.as_data32();
let iconic = data[0] == 3;
state
.lock_or_recover()
.changes
.push((e.window, X11Change::Minimize(iconic)));
} else if e.type_ == atoms.net_wm_moveresize {
// `data = [x_root, y_root, direction, button, source]`.
let data = e.data.as_data32();
let (x, y, direction) = (data[0] as i32, data[1] as i32, data[2]);
let change = match direction {
// 8 = Move, 0..=7 = krawędzie/narożniki, 11 = Cancel.
8 => Some(X11Change::Move { x, y }),
0..=7 => Some(X11Change::Resize {
edges: move_resize_edges(direction),
x,
y,
}),
_ => None,
};
if let Some(change) = change {
state.lock_or_recover().changes.push((e.window, change));
}
}
}
Event::UnmapNotify(e) => {
let mut st = state.lock_or_recover();
st.managed.retain(|w| *w != e.window);
drop(st);
publish_client_list(conn, root, atoms, state);
}
Event::DestroyNotify(e) => {
let mut st = state.lock_or_recover();
st.windows.remove(&e.window);
st.managed.retain(|w| *w != e.window);
st.by_serial.retain(|_, w| *w != e.window);
st.ewmh.remove(&e.window);
}
_ => {}
}
}
fn apply_command(
conn: &RustConnection,
root: &Window,
atoms: &Atoms,
state: &Arc<Mutex<XwmState>>,
cmd: XwmCommand,
) {
match cmd {
XwmCommand::Close(win) => request_close(conn, win),
XwmCommand::SetGeometry { window, x, y, w, h } => {
let aux = ConfigureWindowAux::new().x(x).y(y).width(w).height(h);
let _ = conn.configure_window(window, &aux);
}
XwmCommand::SetActive(window) => {
let list: Vec<u32> = window.into_iter().collect();
set_prop32(
conn,
*root,
atoms.net_active_window,
AtomEnum::WINDOW.into(),
&list,
);
if let Some(w) = window {
let _ = conn.set_input_focus(InputFocus::POINTER_ROOT, w, x11rb::CURRENT_TIME);
}
}
XwmCommand::SetMaximized { window, on } => {
// Prosimy klienta o zmianę stanu (EWMH) — tak działają wszystkie pulpity.
let action = if on { 1u32 } else { 0u32 }; // _NET_WM_STATE_ADD / _REMOVE
for state_atom in [
atoms.net_wm_state_maximized_vert,
atoms.net_wm_state_maximized_horz,
] {
send_wm_state(conn, atoms, window, action, state_atom);
}
}
XwmCommand::SetFrameExtents { window, top } => {
set_prop32(
conn,
window,
atoms.net_frame_extents,
AtomEnum::CARDINAL.into(),
&[0, 0, top, 0],
);
}
XwmCommand::SetFullscreen { window, on } => {
let action = if on { 1u32 } else { 0u32 }; // _NET_WM_STATE_ADD / _REMOVE
send_wm_state(conn, atoms, window, action, atoms.net_wm_state_fullscreen);
state
.lock_or_recover()
.ewmh
.entry(window)
.or_insert((false, false))
.0 = on;
}
}
let _ = conn.flush();
let _ = state;
}
/// Odczytuje serial z komunikatu `WL_SURFACE_SERIAL` (dwa razy 32 bity: `lo`, `hi`).
/// Bitmaska krawędzi dla kierunku `_NET_WM_MOVERESIZE` (1=góra, 2=dół, 4=lewo, 8=prawo).
fn move_resize_edges(direction: u32) -> u8 {
match direction {
0 => 1 | 4, // lewy-górny
1 => 1, // góra
2 => 1 | 8, // prawy-górny
3 => 8, // prawo
4 => 2 | 8, // prawy-dolny
5 => 2, // dół
6 => 2 | 4, // lewy-dolny
7 => 4, // lewo
_ => 0,
}
}
fn serial_from_client_message(data: &x11rb::protocol::xproto::ClientMessageData) -> u64 {
let raw = data.serialize();
if raw.len() < 8 {
return 0;
}
let lo = u32::from_ne_bytes([raw[0], raw[1], raw[2], raw[3]]);
let hi = u32::from_ne_bytes([raw[4], raw[5], raw[6], raw[7]]);
(u64::from(hi) << 32) | u64::from(lo)
}
/// Prosi klienta o zamknięcie okna (`WM_DELETE_WINDOW`), a gdy nie umie — ubija połączenie.
fn request_close(conn: &RustConnection, window: Window) {
// `WM_PROTOCOLS` to lista protokołów obsługiwanych przez klienta; zamykanie
// odbywa się przez wysłanie `ClientMessage` typu `WM_PROTOCOLS` z `WM_DELETE_WINDOW`
// w pierwszym słowie. Wcześniej wysyłaliśmy typ = pierwszy atom z listy i dane
// `[16, 0, …]`, co żaden klient nie rozpoznawał — „X” na pasku nic nie robiło.
let Ok(wm_protocols) = atom(conn, "WM_PROTOCOLS") else {
return;
};
let Ok(wm_delete) = atom(conn, "WM_DELETE_WINDOW") else {
return;
};
let protocols = get_atoms_prop(conn, window, wm_protocols);
if protocols.contains(&wm_delete) {
send_client_message(conn, window, wm_protocols, &[wm_delete, 0, 0, 0, 0]);
}
}
fn send_wm_state(conn: &RustConnection, atoms: &Atoms, window: Window, action: u32, state: Atom) {
send_client_message(conn, window, atoms.net_wm_state, &[action, state, 0, 1, 0]);
}
/// Wysyła `ClientMessage` (`WM_PROTOCOLS`-style) do okna.
fn send_client_message(conn: &RustConnection, window: Window, type_: Atom, data: &[u32; 5]) {
let event = ClientMessageEvent::new(32, window, type_, *data);
let _ = conn.send_event(false, window, EventMask::NO_EVENT, event);
let _ = conn.flush();
}
/// Ustawia listę okien klientów (`_NET_CLIENT_LIST`) — używają jej panele i aplikacje.
fn publish_client_list(
conn: &RustConnection,
root: Window,
atoms: &Atoms,
state: &Arc<Mutex<XwmState>>,
) {
let list = state.lock_or_recover().managed.clone();
set_prop32(
conn,
root,
atoms.net_client_list,
AtomEnum::WINDOW.into(),
&list,
);
let _ = conn.flush();
}
/// Czy okno ma `override_redirect` (menu, podpowiedź, okna „niezarządzane”).
fn is_override_redirect(conn: &RustConnection, window: Window) -> bool {
conn.get_window_attributes(window)
.ok()
.and_then(|c| c.reply().ok())
.map(|a| a.override_redirect)
.unwrap_or(false)
}
/// Zapisuje informacje o oknie, **zachowując geometrię** z `CreateNotify`/
/// `ConfigureNotify` (odczyt własności jej nie zawiera).
fn insert_info(state: &Arc<Mutex<XwmState>>, window: Window, mut info: X11Info) {
let mut st = state.lock_or_recover();
if let Some(prev) = st.windows.get(&window) {
info.x = prev.x;
info.y = prev.y;
info.width = prev.width;
info.height = prev.height;
}
st.windows.insert(window, info);
}
/// Czyta tytuł i typ okna X11.
fn read_info(conn: &RustConnection, atoms: &Atoms, window: Window) -> X11Info {
let title = read_title(conn, atoms, window);
let kind = read_kind(conn, atoms, window);
X11Info {
title,
override_redirect: is_override_redirect(conn, window),
kind,
client_decorations: has_client_decorations(conn, atoms, window),
..X11Info::default()
}
}
/// Czy klient chce sam rysować dekoracje (`_MOTIF_WM_HINTS` z wyłączonymi dekoracjami).
///
/// To standardowy sposób, w jaki GTK/Qt/Chrome mówią menedżerowi okien „nie dodawaj ramki”.
/// Flagi: bit 2 (`1 << 1`) mówi, że pole `decorations` jest istotne; `decorations == 0`
/// oznacza „bez ramki” (dekoracje po stronie klienta).
fn has_client_decorations(conn: &RustConnection, atoms: &Atoms, window: Window) -> bool {
let hints = get_atoms_prop(conn, window, atoms.motif_wm_hints);
hints.len() >= 3 && hints[0] & 0b10 != 0 && hints[2] == 0
}
/// Tytuł: najpierw `_NET_WM_NAME` (UTF-8), potem klasyczny `WM_NAME`.
fn read_title(conn: &RustConnection, atoms: &Atoms, window: Window) -> String {
if let Some(utf8) = get_prop8(conn, window, atoms.net_wm_name) {
if let Ok(s) = String::from_utf8(utf8) {
let s = s.trim_end_matches('\0').trim();
if !s.is_empty() {
return s.to_owned();
}
}
}
get_prop8(conn, window, AtomEnum::WM_NAME.into())
.and_then(|b| String::from_utf8(b).ok())
.map(|s| s.trim_end_matches('\0').trim().to_owned())
.unwrap_or_default()
}
/// Typ okna: menu/podpowiedzi nie dostają dekoracji.
fn read_kind(conn: &RustConnection, atoms: &Atoms, window: Window) -> X11Kind {
let types = get_atoms_prop(conn, window, atoms.net_wm_window_type);
for t in types {
if t == atoms.net_wm_window_type_menu
|| t == atoms.net_wm_window_type_dropdown
|| t == atoms.net_wm_window_type_tooltip
|| t == atoms.net_wm_window_type_popup
|| t == atoms.net_wm_window_type_notification
{
return X11Kind::Transient;
}
if t == atoms.net_wm_window_type_dialog
|| t == atoms.net_wm_window_type_utility
|| t == atoms.net_wm_window_type_splash
{
return X11Kind::Dialog;
}
}
X11Kind::Normal
}
/// Wartość własności jako lista 32-bitowa.
fn get_atoms_prop(conn: &RustConnection, window: Window, property: Atom) -> Vec<u32> {
conn.get_property(false, window, property, AtomEnum::ANY, 0, 1024)
.ok()
.and_then(|c| c.reply().ok())
.and_then(|r| r.value32().map(|it| it.collect()))
.unwrap_or_default()
}
/// Wartość własności jako bajty (tekst UTF-8 / Latin-1).
fn get_prop8(conn: &RustConnection, window: Window, property: Atom) -> Option<Vec<u8>> {
let reply = conn
.get_property(false, window, property, AtomEnum::ANY, 0, 1024)
.ok()?
.reply()
.ok()?;
if reply.value.is_empty() {
return None;
}
Some(reply.value)
}
fn set_prop32(conn: &RustConnection, window: Window, property: Atom, type_: Atom, data: &[u32]) {
// x11rb 0.14 nie ma `change_property32` — serializujemy liczby sami.
let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
let _ = conn.change_property(
PropMode::REPLACE,
window,
property,
type_,
32,
data.len() as u32,
&bytes,
);
}
fn set_prop_str(conn: &RustConnection, window: Window, property: Atom, text: &str) {
let _ = conn.change_property(
PropMode::REPLACE,
window,
property,
AtomEnum::STRING,
8,
text.len() as u32,
text.as_bytes(),
);
}
/// Internowane atomy, których używamy (internowanie to jedno zapytanie do X).
struct Atoms {
net_supported: Atom,
net_supporting_wm_check: Atom,
net_client_list: Atom,
net_active_window: Atom,
net_close_window: Atom,
net_wm_state: Atom,
net_wm_state_maximized_vert: Atom,
net_wm_state_maximized_horz: Atom,
net_wm_state_fullscreen: Atom,
motif_wm_hints: Atom,
net_wm_name: Atom,
net_wm_window_type: Atom,
net_wm_window_type_dialog: Atom,
net_wm_window_type_utility: Atom,
net_wm_window_type_splash: Atom,
net_wm_window_type_menu: Atom,
net_wm_window_type_dropdown: Atom,
net_wm_window_type_tooltip: Atom,
net_wm_window_type_popup: Atom,
net_wm_window_type_notification: Atom,
net_frame_extents: Atom,
wl_surface_serial: Atom,
wm_change_state: Atom,
net_wm_moveresize: Atom,
}
impl Atoms {
fn new(conn: &RustConnection) -> Result<Self, x11rb::errors::ReplyError> {
Ok(Self {
net_supported: atom(conn, "_NET_SUPPORTED")?,
net_supporting_wm_check: atom(conn, "_NET_SUPPORTING_WM_CHECK")?,
net_client_list: atom(conn, "_NET_CLIENT_LIST")?,
net_active_window: atom(conn, "_NET_ACTIVE_WINDOW")?,
net_close_window: atom(conn, "_NET_CLOSE_WINDOW")?,
net_wm_state: atom(conn, "_NET_WM_STATE")?,
net_wm_state_maximized_vert: atom(conn, "_NET_WM_STATE_MAXIMIZED_VERT")?,
net_wm_state_maximized_horz: atom(conn, "_NET_WM_STATE_MAXIMIZED_HORZ")?,
net_wm_state_fullscreen: atom(conn, "_NET_WM_STATE_FULLSCREEN")?,
motif_wm_hints: atom(conn, "_MOTIF_WM_HINTS")?,
net_wm_name: atom(conn, "_NET_WM_NAME")?,
net_wm_window_type: atom(conn, "_NET_WM_WINDOW_TYPE")?,
net_wm_window_type_dialog: atom(conn, "_NET_WM_WINDOW_TYPE_DIALOG")?,
net_wm_window_type_utility: atom(conn, "_NET_WM_WINDOW_TYPE_UTILITY")?,
net_wm_window_type_splash: atom(conn, "_NET_WM_WINDOW_TYPE_SPLASH")?,
net_wm_window_type_menu: atom(conn, "_NET_WM_WINDOW_TYPE_MENU")?,
net_wm_window_type_dropdown: atom(conn, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU")?,
net_wm_window_type_tooltip: atom(conn, "_NET_WM_WINDOW_TYPE_TOOLTIP")?,
net_wm_window_type_popup: atom(conn, "_NET_WM_WINDOW_TYPE_POPUP_MENU")?,
net_wm_window_type_notification: atom(conn, "_NET_WM_WINDOW_TYPE_NOTIFICATION")?,
net_frame_extents: atom(conn, "_NET_FRAME_EXTENTS")?,
wl_surface_serial: atom(conn, "WL_SURFACE_SERIAL")?,
wm_change_state: atom(conn, "WM_CHANGE_STATE")?,
net_wm_moveresize: atom(conn, "_NET_WM_MOVERESIZE")?,
})
}
}
fn atom(conn: &RustConnection, name: &str) -> Result<Atom, x11rb::errors::ReplyError> {
conn.intern_atom(false, name.as_bytes())?
.reply()
.map(|r| r.atom)
}
/// Synchronizuje model kompozytora z XWM (wołane raz na iterację pętli głównej).
///
/// XWM działa w osobnym wątku i nie ma dostępu do obiektów Waylanda, więc tytuły oraz
/// życzenia stanu (pełny ekran, maksymalizacja) od klientów X11 odbieramy tutaj.
pub fn sync(state: &mut State) {
sync_window_info(state);
let Some(xwm) = global() else {
return;
};
for (window, change) in xwm.take_changes() {
let Some(serial) = xwm.serial_for_window(window) else {
continue;
};
let Some(slot) = slot_for_serial(state, serial) else {
continue;
};
match change {
X11Change::Fullscreen(on) => crate::seat::set_fullscreen(state, slot, on),
X11Change::Maximized(on) => crate::seat::set_maximized(state, slot, on),
X11Change::Minimize(on) => crate::seat::set_minimized(state, slot, on),
X11Change::Move { x, y } => crate::seat::interactive_move_at(
state,
slot,
libluna_core::Point::new(x as f64, y as f64),
),
X11Change::Resize { edges, x, y } => crate::seat::interactive_resize_at(
state,
slot,
edges,
libluna_core::Point::new(x as f64, y as f64),
),
}
}
}
/// Aktualizuje tytuł i tryb dekoracji okien X11.
///
/// Xwayland wysyła `WL_SURFACE_SERIAL` **po** pierwszym `commit`, więc informacje z XWM
/// docierają chwilę po utworzeniu okna. Bez tego kroku tytuł zostałby pusty, a klient
/// z własnymi dekoracjami (CSD) dostałby od nas drugi pasek tytułu.
fn sync_window_info(state: &mut State) {
let Some(xwm) = global() else {
return;
};
let updates: Vec<(usize, X11Info)> = state
.slots
.iter()
.enumerate()
.filter_map(|(slot, surface)| {
let data = surface.data::<SurfaceData>()?;
if !*data.xwayland.lock_or_recover() {
return None;
}
let serial = *data.x11_serial.lock_or_recover();
if serial == 0 {
return None;
}
let info = xwm.info_for_serial(serial)?;
let is_popup = info.override_redirect || info.kind == X11Kind::Transient;
let changed = {
let windows = state.windows.lock_or_recover();
let w = windows.get(slot)?;
let moved =
is_popup && info.width > 0 && (w.x != info.x as f64 || w.y != info.y as f64);
w.title != info.title || w.ssd != info.decorated() || w.popup != is_popup || moved
};
changed.then_some((slot, info))
})
.collect();
for (slot, info) in updates {
let is_popup = info.override_redirect || info.kind == X11Kind::Transient;
if std::env::var("SELENE_STATS").is_ok() {
eprintln!(
"selene/win: X11 zaktualizowane '{}' ssd={} popup={}",
info.title,
info.decorated(),
is_popup
);
}
{
let mut windows = state.windows.lock_or_recover();
if let Some(w) = windows.get_mut(slot) {
w.title = info.title.clone();
w.ssd = info.decorated();
w.popup = is_popup;
// Menu X11 ma współrzędne absolutne — ustawiamy je dokładnie tam,
// gdzie klient je otworzył (klik w menu wtedy trafia w menu).
if is_popup && info.width > 0 && info.height > 0 {
w.x = info.x as f64;
w.y = info.y as f64;
}
}
}
if let Some(surface) = state.slots.get(slot).cloned() {
crate::foreign_toplevel::broadcast_title(state, &surface, &info.title);
}
}
}
/// Slot okna odpowiadający serialowi powierzchni X11.
fn slot_for_serial(state: &State, serial: u64) -> Option<usize> {
state.slots.iter().position(|surface| {
surface
.data::<SurfaceData>()
.map(|d| *d.xwayland.lock_or_recover() && *d.x11_serial.lock_or_recover() == serial)
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moveresize_directions_map_to_edges() {
assert_eq!(move_resize_edges(0), 1 | 4); // lewy-górny
assert_eq!(move_resize_edges(1), 1); // góra
assert_eq!(move_resize_edges(3), 8); // prawo
assert_eq!(move_resize_edges(4), 2 | 8); // prawy-dolny
assert_eq!(move_resize_edges(5), 2); // dół
assert_eq!(move_resize_edges(7), 4); // lewo
assert_eq!(move_resize_edges(8), 0); // „move” nie jest zmianą rozmiaru
assert_eq!(move_resize_edges(99), 0);
}
#[test]
fn normal_windows_are_decorated_but_menus_are_not() {
let normal = X11Info {
kind: X11Kind::Normal,
..Default::default()
};
assert!(normal.decorated());
let menu = X11Info {
kind: X11Kind::Transient,
..Default::default()
};
assert!(!menu.decorated());
// Override-redirect (np. podpowiedź bez typu) też bez dekoracji.
let over = X11Info {
override_redirect: true,
..Default::default()
};
assert!(!over.decorated());
// Klient z własnymi dekoracjami (CSD) nie dostaje drugiego paska.
let csd = X11Info {
client_decorations: true,
..Default::default()
};
assert!(!csd.decorated());
}
}