diff options
Diffstat (limited to 'crates/wayland/src/xwayland.rs')
| -rw-r--r-- | crates/wayland/src/xwayland.rs | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/crates/wayland/src/xwayland.rs b/crates/wayland/src/xwayland.rs new file mode 100644 index 0000000..dc7a88b --- /dev/null +++ b/crates/wayland/src/xwayland.rs @@ -0,0 +1,240 @@ +//! 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<i32>, _y: Option<i32>, _w: Option<u32>, _h: Option<u32>, _reorder: Option<Reorder>) { + // 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<i32, Logical>, _above: Option<X11Window>) { + // 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); + } +} |