//! XWayland integration: lets legacy X11-only clients (anything that can't //! speak the Wayland protocol natively) run inside the Wayland session, //! bridged into the same `srdwm_core::WindowManager`/`Space` pipeline as //! native `xdg-shell` windows. //! //! Only wired up for the udev/DRM backend (`udev.rs`) for now: XWayland's //! window-manager side (`X11Wm::start_wm`) is driven entirely through a //! `calloop` event loop, which only the udev backend has - the nested //! winit backend still drives its own manual poll loop (see `lib.rs`'s //! module docs). Adding a second, XWayland-only `calloop::EventLoop` to the //! winit backend too is possible but left as a follow-up. //! //! Scope: regular (server-managed) windows go through the exact same //! `WindowManager::add_window`/decoration/hit-test path as xdg-shell //! windows - an X11 app gets tiled, placed by `SmartPlacement`, and //! decorated with our drawn titlebar exactly like a native Wayland client. //! Override-redirect windows (menus, tooltips, drag images) are //! deliberately *not* run through `WindowManager` at all - matching real //! ICCCM semantics, no WM is ever supposed to manage or decorate them -- //! they're mapped into `Space` at whatever geometry the client itself //! requests. Selections/clipboard, XSETTINGS, and RandR primary-output //! sync are not implemented (all have harmless no-op default trait //! methods in `XwmHandler`). use smithay::reexports::calloop::LoopHandle; use smithay::utils::{Logical, Rectangle}; use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState}; use smithay::xwayland::xwm::{Reorder, ResizeEdge as X11ResizeEdge, XwmId}; use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler}; use smithay::{delegate_xwayland_shell, desktop::Window as DWindow}; use srdwm_core::{Event as CoreEvent, ResizeEdge, Window as CoreWindow, TITLEBAR_HEIGHT}; use crate::CompState; pub(crate) type X11Window = smithay::xwayland::xwm::X11Window; /// Spawns XWayland and registers the calloop sources that drive it: the /// `XWayland` process/readiness source, and (once ready) `X11Wm`'s own /// internal X11-connection source. Both are owned by the event loop after /// `insert_source`, not by any struct here - dropping the loop (or the /// `X11Wm` on disconnect) is what shuts things down. pub(crate) fn spawn(handle: &LoopHandle<'static, CompState>, display_handle: &smithay::reexports::wayland_server::DisplayHandle) -> std::io::Result<()> { let (xwayland, client) = XWayland::spawn(display_handle, None, std::iter::empty::<(String, String)>(), true, std::process::Stdio::null(), std::process::Stdio::null(), |_| ())?; let handle_for_ready = handle.clone(); handle .insert_source(xwayland, move |event, _, data: &mut CompState| match event { XWaylandEvent::Ready { x11_socket, display_number } => { log::info!("XWayland ready on display :{display_number}"); match X11Wm::start_wm(handle_for_ready.clone(), x11_socket, client.clone()) { Ok(wm) => data.xwm = Some(wm), Err(e) => log::error!("failed to start X11 window manager for XWayland: {e}"), } } XWaylandEvent::Error => log::error!("XWayland exited unexpectedly during startup"), }) .map_err(|e| std::io::Error::other(format!("failed to register XWayland source: {e}")))?; Ok(()) } fn to_core_resize_edge(edge: X11ResizeEdge) -> ResizeEdge { match edge { X11ResizeEdge::Top => ResizeEdge::Top, X11ResizeEdge::Bottom => ResizeEdge::Bottom, X11ResizeEdge::Left => ResizeEdge::Left, X11ResizeEdge::Right => ResizeEdge::Right, X11ResizeEdge::TopLeft => ResizeEdge::TopLeft, X11ResizeEdge::TopRight => ResizeEdge::TopRight, X11ResizeEdge::BottomLeft => ResizeEdge::BottomLeft, X11ResizeEdge::BottomRight => ResizeEdge::BottomRight, } } impl CompState { /// Retries `finish_x11_window_setup` for every mapped X11 window still /// waiting on its `wl_surface` association - called on every /// compositor commit, since that association can complete without ever /// invoking `surface_associated` (see the module docs). pub(crate) fn retry_pending_x11_windows(&mut self) { if self.xwayland_pending.is_empty() { return; } let pending = std::mem::take(&mut self.xwayland_pending); for surface in pending { self.finish_x11_window_setup(&surface); let done = self.xwayland_windows.get(&surface.window_id()).is_some_and(|id| self.id_to_window.contains_key(id)); if !done { self.xwayland_pending.push(surface); } } } /// Finishes setting up a *server-managed* (non-override-redirect) X11 /// window once both halves are known: it's been granted its map /// request, and XWayland has associated it with a `wl_surface`. Safe to /// call from either order's callback; idempotent. fn finish_x11_window_setup(&mut self, surface: &X11Surface) { let Some(wl_surface) = surface.wl_surface() else { log::debug!("xwayland: finish_x11_window_setup xid={:?} - no wl_surface yet", surface.window_id()); return; }; let Some(&id) = self.xwayland_windows.get(&surface.window_id()) else { log::debug!("xwayland: finish_x11_window_setup xid={:?} - not in xwayland_windows", surface.window_id()); return; }; if self.id_to_window.contains_key(&id) { log::debug!("xwayland: finish_x11_window_setup xid={:?} id={id} - already set up", surface.window_id()); return; } log::info!("xwayland: finishing setup for xid={:?} id={id}", surface.window_id()); let geom = self.wm.borrow().window(id).map(|w| w.geometry).unwrap_or_default(); let dwindow = DWindow::new_x11_window(surface.clone()); let _ = surface.set_mapped(true); let _ = surface.configure(Rectangle::new((geom.x, geom.y + TITLEBAR_HEIGHT as i32).into(), (geom.width as i32, (geom.height - TITLEBAR_HEIGHT) as i32).into())); self.space.map_element(dwindow.clone(), (geom.x, geom.y + TITLEBAR_HEIGHT as i32), true); self.surface_to_id.insert(wl_surface, id); self.id_to_window.insert(id, dwindow); self.redraw_decoration_buffer(id); self.pending.borrow_mut().push(CoreEvent::WindowCreated(id)); } fn remove_x11_window(&mut self, xid: X11Window) { let Some(id) = self.xwayland_windows.get(&xid).copied() else { return }; if let Some(w) = self.id_to_window.remove(&id) { self.space.unmap_elem(&w); } self.decorations.remove(&id); self.wm.borrow_mut().remove_window(id); self.pending.borrow_mut().push(CoreEvent::WindowDestroyed(id)); } } impl XWaylandShellHandler for CompState { fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { &mut self.xwayland_shell_state } fn surface_associated(&mut self, _xwm: XwmId, _wl_surface: smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, surface: X11Surface) { log::debug!("xwayland: surface_associated xid={:?}", surface.window_id()); self.finish_x11_window_setup(&surface); } } delegate_xwayland_shell!(CompState); impl XwmHandler for CompState { fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm { self.xwm.as_mut().expect("XwmHandler callback fired without an X11Wm") } fn new_window(&mut self, _xwm: XwmId, window: X11Surface) { // Created but not (yet) mapped - nothing to do until a map request. log::debug!("xwayland: new_window xid={:?} title={:?}", window.window_id(), window.title()); } fn new_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { // Not managed until it actually maps - see `mapped_override_redirect_window`. log::debug!("xwayland: new_override_redirect_window xid={:?}", window.window_id()); } fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) { log::debug!("xwayland: map_window_request xid={:?} title={:?} class={:?}", window.window_id(), window.title(), window.class()); let id = { let mut wm = self.wm.borrow_mut(); let id = wm.alloc_window_id(); let mut w = CoreWindow::new(id, window.title()); w.app_id = window.class(); let size = window.geometry().size; w.geometry = srdwm_core::Rect::new(0, 0, size.w.max(1) as u32, size.h.max(1) as u32 + TITLEBAR_HEIGHT); wm.add_window(w); id }; self.xwayland_windows.insert(window.window_id(), id); self.finish_x11_window_setup(&window); if !self.id_to_window.contains_key(&id) { self.xwayland_pending.push(window); } } fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { let Some(wl_surface) = window.wl_surface() else { return }; let geom = window.geometry(); let dwindow = DWindow::new_x11_window(window.clone()); self.space.map_element(dwindow.clone(), (geom.loc.x, geom.loc.y), true); // Allocated only for the surface_to_id/id_to_window bookkeeping // `commit()` needs - deliberately never passed to // `WindowManager::add_window`: override-redirect windows are not // managed, per ICCCM. let id = self.wm.borrow_mut().alloc_window_id(); self.xwayland_windows.insert(window.window_id(), id); self.surface_to_id.insert(wl_surface, id); self.id_to_window.insert(id, dwindow); } fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) { self.remove_x11_window(window.window_id()); } fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) { let xid = window.window_id(); self.remove_x11_window(xid); self.xwayland_windows.remove(&xid); } fn configure_request(&mut self, _xwm: XwmId, _window: X11Surface, _x: Option, _y: Option, _w: Option, _h: Option, _reorder: Option) { // We own layout for managed windows; smithay always sends back a // synthetic configure with the window's actual current geometry // after this callback returns (see `xwayland::xwm`'s `handle_event` // for `ConfigureRequest`), so there is nothing to do here - this // mirrors how `srdwm_x11::X11Platform` acks `ConfigureRequest` with // the client's real geometry rather than whatever it asked for. } fn configure_notify(&mut self, _xwm: XwmId, window: X11Surface, geometry: Rectangle, _above: Option) { // Only override-redirect windows are allowed to reposition // themselves at will; managed windows' geometry is owned by us. if !window.is_override_redirect() { return; } let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return }; if let Some(w) = self.id_to_window.get(&id) { self.space.map_element(w.clone(), (geometry.loc.x, geometry.loc.y), false); } } fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, resize_edge: X11ResizeEdge) { let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return }; let pos = self.seat.get_pointer().map(|p| p.current_location()).unwrap_or_default(); self.wm.borrow_mut().start_resize(id, to_core_resize_edge(resize_edge), pos.x as i32, pos.y as i32); } fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) { let Some(&id) = self.xwayland_windows.get(&window.window_id()) else { return }; let pos = self.seat.get_pointer().map(|p| p.current_location()).unwrap_or_default(); self.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32); } }