From 0deee83178722d57b0d35a5a7f97e4001b7884d9 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 5 Jul 2026 11:44:08 -0700 Subject: [PATCH] feat: add x11 initial support --- CONTRIBUTING.md | 13 +- Cargo.lock | 12 + crates/hearthspace-compositor/Cargo.toml | 3 +- .../src/compositor/handlers.rs | 54 ++- .../src/compositor/input.rs | 15 +- .../src/compositor/input/pointer.rs | 15 +- .../src/compositor/mod.rs | 83 +++- .../src/compositor/rendering.rs | 17 +- .../src/compositor/runtime.rs | 18 +- .../src/compositor/shell_integration.rs | 19 +- .../src/compositor/windows.rs | 125 ++++-- .../src/compositor/windows/hit_testing.rs | 22 +- .../src/compositor/xwayland/handlers.rs | 381 ++++++++++++++++++ .../src/compositor/xwayland/mod.rs | 173 ++++++++ todos/NATIVE_BACKEND.md | 2 +- todos/ROADMAP.md | 7 +- 16 files changed, 864 insertions(+), 95 deletions(-) create mode 100644 crates/hearthspace-compositor/src/compositor/xwayland/handlers.rs create mode 100644 crates/hearthspace-compositor/src/compositor/xwayland/mod.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5847e44..4e90411 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,11 @@ As this project grows, you can find technical documentation in the [docs/](./doc ## Setup -This project targets modern Linux systems with Wayland only. Hearthspace is a Rust workspace with separate crates for the session supervisor, compositor, shell, settings daemon, settings app, and shared IPC/common code. +This project targets modern Linux systems with Wayland as the native display +protocol. X11 applications are supported through XWayland when the `Xwayland` +runtime is installed. Hearthspace is a Rust workspace with separate crates for +the session supervisor, compositor, shell, settings daemon, settings app, and +shared IPC/common code. Ubuntu 26.04 LTS is our development and runtime baseline, and it is expected to have the oldest supported version of most packages. @@ -68,6 +72,13 @@ Hearthspace compositor. If we later provide a Hearthspace-owned portal session, the compositor still needs to support the dialog window created by the portal backend. +X11 app support is provided by Smithay's XWayland integration. Install the +runtime when testing X11-only applications: + +```sh +sudo apt-get install -y xwayland +``` + For testing, `foot` is installed as a small Wayland-native terminal for server-side decoration testing. ``` diff --git a/Cargo.lock b/Cargo.lock index 12f412f..bd59e68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -4807,6 +4816,7 @@ dependencies = [ "drm", "drm-ffi", "drm-fourcc", + "encoding_rs", "errno", "gbm", "gl_generator", @@ -4820,6 +4830,7 @@ dependencies = [ "rand", "reis", "rustix 1.1.4", + "scopeguard", "sha2", "smallvec", "tempfile", @@ -4834,6 +4845,7 @@ dependencies = [ "wayland-protocols-wlr", "wayland-server", "winit 0.31.0-beta.2", + "x11rb", "xkbcommon", ] diff --git a/crates/hearthspace-compositor/Cargo.toml b/crates/hearthspace-compositor/Cargo.toml index 6afabd9..c22a1b3 100644 --- a/crates/hearthspace-compositor/Cargo.toml +++ b/crates/hearthspace-compositor/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" default-run = "hearthspace-compositor" [features] -default = ["winit"] +default = ["winit", "xwayland"] e2e = ["dep:waydriver", "dep:waydriver-hearthspace"] test-apps = ["dep:gtk"] udev = [ @@ -21,6 +21,7 @@ udev = [ "smithay/renderer_multi", ] winit = ["smithay/backend_winit"] +xwayland = ["smithay/xwayland", "smithay/x11rb_event_source"] [[bin]] name = "hearthspace" diff --git a/crates/hearthspace-compositor/src/compositor/handlers.rs b/crates/hearthspace-compositor/src/compositor/handlers.rs index 62c53cc..8e9ad9b 100644 --- a/crates/hearthspace-compositor/src/compositor/handlers.rs +++ b/crates/hearthspace-compositor/src/compositor/handlers.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "xwayland")] +use smithay::xwayland::XWaylandClientData; use smithay::{ backend::allocator::dmabuf::Dmabuf, backend::renderer::utils::on_commit_buffer_handler, @@ -43,7 +45,8 @@ use wayland_server::{ }; use super::{ - App, ClientState, DragState, ManagedWindow, ManagedWindowKind, WindowDecoration, + App, ClientState, DragState, ManagedSurface, ManagedWindow, ManagedWindowKind, + WindowDecoration, idle::ActivityReason, windows::{ ResizeEdges, decoration_for_new_window, position_for_new_window, @@ -76,7 +79,7 @@ impl XdgShellHandler for App { ); self.windows.push(ManagedWindow { id, - surface: surface.clone(), + surface: ManagedSurface::Xdg(surface.clone()), position: position_for_new_window(kind, self.next_spawn_position), kind, shell_output_name: shell_output_name.clone(), @@ -117,7 +120,7 @@ impl XdgShellHandler for App { let Some(window_index) = self .windows .iter() - .position(|window| window.surface == surface) + .position(|window| window.surface.same_xdg(&surface)) else { return; }; @@ -127,8 +130,9 @@ impl XdgShellHandler for App { } let window_index = self.raise_window(window_index); - let surface = self.windows[window_index].surface.wl_surface().clone(); - self.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } debug!( window_id = self.windows[window_index].id, "client requested window move" @@ -155,7 +159,7 @@ impl XdgShellHandler for App { let Some(window_index) = self .windows .iter() - .position(|window| window.surface == surface) + .position(|window| window.surface.same_xdg(&surface)) else { return; }; @@ -165,8 +169,9 @@ impl XdgShellHandler for App { } let window_index = self.raise_window(window_index); - let surface = self.windows[window_index].surface.wl_surface().clone(); - self.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } debug!( window_id = self.windows[window_index].id, ?edges, @@ -228,14 +233,19 @@ impl XdgShellHandler for App { } fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { - if let Some(window) = self.windows.iter().find(|window| window.surface == surface) { + if let Some(window) = self + .windows + .iter() + .find(|window| window.surface.same_xdg(&surface)) + { info!(window_id = window.id, kind = ?window.kind, "xdg toplevel destroyed"); self.idle_daemon.unregister_window(window.id); if self.focused_normal_window_id == Some(window.id) { self.focused_normal_window_id = None; } } - self.windows.retain(|window| window.surface != surface); + self.windows + .retain(|window| !window.surface.same_xdg(&surface)); self.drag = None; self.resize = None; self.request_redraw(); @@ -247,7 +257,7 @@ impl XdgShellHandler for App { if let Some(window_index) = self .windows .iter() - .position(|window| window.surface == surface) + .position(|window| window.surface.same_xdg(&surface)) { let mut window = self.windows.remove(window_index); let old_kind = window.kind; @@ -280,11 +290,14 @@ impl XdgShellHandler for App { | ManagedWindowKind::Launcher | ManagedWindowKind::ShellWatcher => self.windows.len(), }; + let wl_surface = window.surface.wl_surface(); self.windows.insert(insert_index, window); if kind.is_shell_chrome() { - self.enter_shell_output(surface.wl_surface(), shell_output_name.as_deref()); - } else { - self.enter_output(surface.wl_surface(), None); + if let Some(surface) = &wl_surface { + self.enter_shell_output(surface, shell_output_name.as_deref()); + } + } else if let Some(surface) = &wl_surface { + self.enter_output(surface, None); } self.configure_toplevel(&surface, kind, shell_output_name.as_deref()); self.request_redraw(); @@ -360,7 +373,14 @@ impl CompositorHandler for App { } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { - &client.get_data::().unwrap().compositor_state + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + #[cfg(feature = "xwayland")] + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + panic!("unknown compositor client data type") } fn new_surface(&mut self, surface: &WlSurface) { @@ -524,7 +544,9 @@ impl App { continue; } self.windows[index].advertised_fractional_scale = scale; - let root = self.windows[index].surface.wl_surface().clone(); + let Some(root) = self.windows[index].surface.wl_surface() else { + continue; + }; with_surface_tree_downward( &root, (), diff --git a/crates/hearthspace-compositor/src/compositor/input.rs b/crates/hearthspace-compositor/src/compositor/input.rs index 077ad24..ec5a787 100644 --- a/crates/hearthspace-compositor/src/compositor/input.rs +++ b/crates/hearthspace-compositor/src/compositor/input.rs @@ -107,8 +107,9 @@ pub(in crate::compositor) fn handle_input_event( } Some(HitTarget::TitleBar { window_index }) => { let window_index = state.raise_window(window_index); - let surface = state.windows[window_index].surface.wl_surface().clone(); - state.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = state.windows[window_index].surface.wl_surface() { + state.set_keyboard_focus_to_window(window_index, surface); + } state.drag = Some(DragState { window_id: state.windows[window_index].id, pointer_start: state.raw_pointer_location, @@ -126,8 +127,9 @@ pub(in crate::compositor) fn handle_input_event( edges, }) => { let window_index = state.raise_window(window_index); - let surface = state.windows[window_index].surface.wl_surface().clone(); - state.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = state.windows[window_index].surface.wl_surface() { + state.set_keyboard_focus_to_window(window_index, surface); + } debug!( window_id = state.windows[window_index].id, ?edges, @@ -138,8 +140,9 @@ pub(in crate::compositor) fn handle_input_event( } Some(HitTarget::Client { window_index, .. }) => { let window_index = state.raise_window(window_index); - let surface = state.windows[window_index].surface.wl_surface().clone(); - state.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = state.windows[window_index].surface.wl_surface() { + state.set_keyboard_focus_to_window(window_index, surface); + } } None => { debug!("clearing keyboard focus from pointer press on canvas"); diff --git a/crates/hearthspace-compositor/src/compositor/input/pointer.rs b/crates/hearthspace-compositor/src/compositor/input/pointer.rs index 3992f63..db32d66 100644 --- a/crates/hearthspace-compositor/src/compositor/input/pointer.rs +++ b/crates/hearthspace-compositor/src/compositor/input/pointer.rs @@ -105,8 +105,9 @@ impl App { } Some(HitTarget::TitleBar { window_index }) => { let window_index = self.raise_window(window_index); - let surface = self.windows[window_index].surface.wl_surface().clone(); - self.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } self.drag = Some(DragState { window_id: self.windows[window_index].id, pointer_start: self.raw_pointer_location, @@ -124,8 +125,9 @@ impl App { edges, }) => { let window_index = self.raise_window(window_index); - let surface = self.windows[window_index].surface.wl_surface().clone(); - self.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } debug!( window_id = self.windows[window_index].id, ?edges, @@ -136,8 +138,9 @@ impl App { } Some(HitTarget::Client { window_index, .. }) => { let window_index = self.raise_window(window_index); - let surface = self.windows[window_index].surface.wl_surface().clone(); - self.set_keyboard_focus_to_window(window_index, surface); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } } None => { debug!("clearing keyboard focus from synthetic pointer press on canvas"); diff --git a/crates/hearthspace-compositor/src/compositor/mod.rs b/crates/hearthspace-compositor/src/compositor/mod.rs index 4fd7a96..0045a39 100644 --- a/crates/hearthspace-compositor/src/compositor/mod.rs +++ b/crates/hearthspace-compositor/src/compositor/mod.rs @@ -23,6 +23,8 @@ mod shell_integration; mod udev; mod viewport; mod windows; +#[cfg(feature = "xwayland")] +mod xwayland; use idle::{IdleTransition, WindowIdleDaemon}; #[cfg(any(feature = "winit", feature = "udev"))] @@ -80,13 +82,18 @@ use smithay::{ viewporter::ViewporterState, }, }; +#[cfg(feature = "xwayland")] +use smithay::{ + wayland::xwayland_shell::XWaylandShellState, + xwayland::{X11Surface, X11Wm}, +}; use tracing::{debug, error, info}; use wayland_protocols::xdg::shell::server::xdg_toplevel; use wayland_server::{DisplayHandle, protocol::wl_surface::WlSurface}; struct ManagedWindow { id: u64, - surface: ToplevelSurface, + surface: ManagedSurface, position: CanvasPoint, kind: ManagedWindowKind, shell_output_name: Option, @@ -104,6 +111,59 @@ struct ManagedWindow { advertised_fractional_scale: f64, } +#[derive(Clone)] +enum ManagedSurface { + Xdg(ToplevelSurface), + #[cfg(feature = "xwayland")] + X11(Box), +} + +impl ManagedSurface { + fn wl_surface(&self) -> Option { + match self { + Self::Xdg(surface) => Some(surface.wl_surface().clone()), + #[cfg(feature = "xwayland")] + Self::X11(surface) => surface.wl_surface(), + } + } + + fn xdg_toplevel(&self) -> Option<&ToplevelSurface> { + match self { + Self::Xdg(surface) => Some(surface), + #[cfg(feature = "xwayland")] + Self::X11(_) => None, + } + } + + fn send_close(&self) { + match self { + Self::Xdg(surface) => surface.send_close(), + #[cfg(feature = "xwayland")] + Self::X11(surface) => { + if let Err(error) = surface.close() { + tracing::warn!(%error, "failed to close X11 window"); + } + } + } + } + + fn same_xdg(&self, other: &ToplevelSurface) -> bool { + match self { + Self::Xdg(surface) => surface == other, + #[cfg(feature = "xwayland")] + Self::X11(_) => false, + } + } + + #[cfg(feature = "xwayland")] + fn same_x11(&self, other: &X11Surface) -> bool { + match self { + Self::Xdg(_) => false, + Self::X11(surface) => surface.as_ref() == other, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ManagedWindowKind { Normal, @@ -175,6 +235,8 @@ struct App { /// they render buffers at the monitor scale instead of falling back to a /// mismatched integer buffer scale. _fractional_scale_manager_state: FractionalScaleManagerState, + #[cfg(feature = "xwayland")] + _xwayland_shell_state: XWaylandShellState, shm_state: ShmState, seat_state: SeatState, data_device_state: DataDeviceState, @@ -226,6 +288,10 @@ struct App { background_tile_ids: Vec, software_cursor_visible: bool, software_cursor: SoftwareCursor, + #[cfg(feature = "xwayland")] + xwm: Option, + #[cfg(feature = "xwayland")] + xdisplay: Option, } #[derive(Debug, Default)] @@ -358,6 +424,8 @@ pub fn run_winit(options: RunOptions) -> Result<(), Box> full_redraw: 1, applied_cursor: CursorIcon::Default, applied_cursor_visible: true, + #[cfg(feature = "xwayland")] + xwayland_seat_state: SeatState::new(), }; run_event_loop(event_loop, &mut data) @@ -456,6 +524,8 @@ pub(in crate::compositor) fn initialize_app( let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); let viewporter_state = ViewporterState::new::(&dh); let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); + #[cfg(feature = "xwayland")] + let xwayland_shell_state = XWaylandShellState::new::(&dh); let shm_state = ShmState::new::(&dh, vec![]); let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&dh, "hearthspace"); @@ -495,6 +565,8 @@ pub(in crate::compositor) fn initialize_app( _output_manager_state: output_manager_state, _viewporter_state: viewporter_state, _fractional_scale_manager_state: fractional_scale_manager_state, + #[cfg(feature = "xwayland")] + _xwayland_shell_state: xwayland_shell_state, shm_state, seat_state, data_device_state: DataDeviceState::new::(&dh), @@ -535,8 +607,17 @@ pub(in crate::compositor) fn initialize_app( background_tile_ids: Vec::new(), software_cursor_visible: false, software_cursor: cursor::standard_software_cursor(), + #[cfg(feature = "xwayland")] + xwm: None, + #[cfg(feature = "xwayland")] + xdisplay: None, }; + #[cfg(feature = "xwayland")] + let mut app = app; + #[cfg(feature = "xwayland")] + app.start_xwayland(&dh); + if options.start_shell { info!(path = %command_socket_path.display(), "spawning shell client"); spawn_shell(&command_socket_path); diff --git a/crates/hearthspace-compositor/src/compositor/rendering.rs b/crates/hearthspace-compositor/src/compositor/rendering.rs index d545559..2c10490 100644 --- a/crates/hearthspace-compositor/src/compositor/rendering.rs +++ b/crates/hearthspace-compositor/src/compositor/rendering.rs @@ -23,7 +23,7 @@ use smithay::{ use std::sync::Mutex; use wayland_server::protocol::wl_surface; -use super::{App, ManagedWindowKind, masonry_titlebar, windows::toplevel_title}; +use super::{App, ManagedWindowKind, masonry_titlebar, windows::window_title}; mod background; @@ -284,13 +284,15 @@ impl App { window_index: usize, ) -> Vec { let window = &self.windows[window_index]; - let parent = window.surface.wl_surface(); + let Some(parent) = window.surface.wl_surface() else { + return Vec::new(); + }; let base = self.surface_screen_origin(window_index); let scale = self.window_render_scale(window_index); - let geometry_loc = toplevel_geometry_loc(parent); + let geometry_loc = self.window_surface_geometry_loc(window_index); let mut elements = Vec::new(); - for (popup, popup_offset) in PopupManager::popups_for_surface(parent) { + for (popup, popup_offset) in PopupManager::popups_for_surface(&parent) { // Offset of the popup surface origin from the parent surface origin, // in the parent's native (unscaled) logical coordinates. let offset = geometry_loc + popup_offset - popup.geometry().loc; @@ -325,9 +327,12 @@ impl App { origin: Point, ) -> Vec> { let window = &self.windows[window_index]; + let Some(root) = window.surface.wl_surface() else { + return Vec::new(); + }; render_elements_from_surface_tree( renderer, - window.surface.wl_surface(), + &root, origin, // Built at native scale; the viewport zoom is applied by wrapping // these elements in a `RescaleRenderElement` in the caller. @@ -374,7 +379,7 @@ impl App { let native_size = self.title_bar_canvas_rect(window_index).size; let native_w = native_size.w.max(1); - let title = toplevel_title(&self.windows[window_index].surface) + let title = window_title(&self.windows[window_index].surface) .filter(|title| !title.trim().is_empty()) .unwrap_or_else(|| "Hearthspace".to_string()); let active = Some(window_index) diff --git a/crates/hearthspace-compositor/src/compositor/runtime.rs b/crates/hearthspace-compositor/src/compositor/runtime.rs index 9373482..6396aae 100644 --- a/crates/hearthspace-compositor/src/compositor/runtime.rs +++ b/crates/hearthspace-compositor/src/compositor/runtime.rs @@ -1,5 +1,7 @@ use std::time::Instant; +#[cfg(feature = "xwayland")] +use smithay::input::SeatState; use smithay::{ backend::{ allocator::Fourcc, @@ -50,6 +52,8 @@ pub(in crate::compositor) struct CalloopData { // (`state.cursor_icon`) is only pushed to the backend when it changes. pub(in crate::compositor) applied_cursor: CursorIcon, pub(in crate::compositor) applied_cursor_visible: bool, + #[cfg(feature = "xwayland")] + pub(in crate::compositor) xwayland_seat_state: SeatState, } pub(in crate::compositor) fn create_headless_calloop_data( @@ -76,6 +80,8 @@ pub(in crate::compositor) fn create_headless_calloop_data( full_redraw: 1, applied_cursor: CursorIcon::Default, applied_cursor_visible: true, + #[cfg(feature = "xwayland")] + xwayland_seat_state: SeatState::new(), } } @@ -104,6 +110,8 @@ pub(in crate::compositor) fn create_calloop_data( full_redraw: 1, applied_cursor: CursorIcon::Default, applied_cursor_visible: true, + #[cfg(feature = "xwayland")] + xwayland_seat_state: SeatState::new(), } } @@ -346,16 +354,16 @@ impl CalloopData { &mut self, ) -> Result<(), Box> { for window in &self.state.windows { - send_frames_surface_tree( - window.surface.wl_surface(), - self.start_time.elapsed().as_millis() as u32, - ); + let Some(root) = window.surface.wl_surface() else { + continue; + }; + send_frames_surface_tree(&root, self.start_time.elapsed().as_millis() as u32); // Popups (e.g. client menus) are tracked separately from the window // surface tree, so they need their own frame callbacks. Without // these the client (e.g. GTK4) throttles and never repaints the // popup after its first frame, so keyboard navigation highlights // never appear. - for (popup, _) in PopupManager::popups_for_surface(window.surface.wl_surface()) { + for (popup, _) in PopupManager::popups_for_surface(&root) { send_frames_surface_tree( popup.wl_surface(), self.start_time.elapsed().as_millis() as u32, diff --git a/crates/hearthspace-compositor/src/compositor/shell_integration.rs b/crates/hearthspace-compositor/src/compositor/shell_integration.rs index a6a26f2..0c853f7 100644 --- a/crates/hearthspace-compositor/src/compositor/shell_integration.rs +++ b/crates/hearthspace-compositor/src/compositor/shell_integration.rs @@ -359,7 +359,12 @@ impl App { }; let launch_env = match launch_environment_for_app(&app) { - Ok(env) => env, + Ok(mut env) => { + if let Some(display) = self.xwayland_display_name() { + env.push(("DISPLAY".to_string(), display)); + } + env + } Err(error) => { error!(app_id, %error, "failed to prepare app environment"); return; @@ -378,6 +383,18 @@ impl App { } } + fn xwayland_display_name(&self) -> Option { + #[cfg(feature = "xwayland")] + { + self.xdisplay + .map(|display_number| format!(":{display_number}")) + } + #[cfg(not(feature = "xwayland"))] + { + None + } + } + fn ensure_snap_wayland_listener(&mut self, instance_name: &str) -> io::Result { ensure_snap_instance_name(instance_name)?; diff --git a/crates/hearthspace-compositor/src/compositor/windows.rs b/crates/hearthspace-compositor/src/compositor/windows.rs index 2b5dc7f..4417d65 100644 --- a/crates/hearthspace-compositor/src/compositor/windows.rs +++ b/crates/hearthspace-compositor/src/compositor/windows.rs @@ -29,7 +29,7 @@ use geometry::{ }; use super::{ - App, ManagedWindow, ManagedWindowKind, ResizeState, WindowDecoration, + App, ManagedSurface, ManagedWindow, ManagedWindowKind, ResizeState, WindowDecoration, idle::ActivityReason, rendering::{toplevel_geometry_loc, toplevel_geometry_size}, }; @@ -99,6 +99,17 @@ pub(super) fn toplevel_title(surface: &ToplevelSurface) -> Option { }) } +pub(super) fn window_title(surface: &ManagedSurface) -> Option { + match surface { + ManagedSurface::Xdg(surface) => toplevel_title(surface), + #[cfg(feature = "xwayland")] + ManagedSurface::X11(surface) => { + let title = surface.title(); + (!title.trim().is_empty()).then_some(title) + } + } +} + pub(super) fn position_for_new_window( kind: ManagedWindowKind, fallback: CanvasPoint, @@ -164,9 +175,13 @@ impl App { } pub(super) fn window_index_for_surface(&self, surface: &WlSurface) -> Option { - self.windows - .iter() - .position(|window| surface_tree_contains(window.surface.wl_surface(), surface)) + self.windows.iter().position(|window| { + window + .surface + .wl_surface() + .as_ref() + .is_some_and(|root| surface_tree_contains(root, surface)) + }) } pub(super) fn window_mut_by_id(&mut self, window_id: u64) -> Option<&mut ManagedWindow> { @@ -178,7 +193,11 @@ impl App { pub(super) fn managed_normal_window_id_for_surface(&self, surface: &WlSurface) -> Option { self.windows.iter().find_map(|window| { (window.kind == ManagedWindowKind::Normal - && surface_tree_contains(window.surface.wl_surface(), surface)) + && window + .surface + .wl_surface() + .as_ref() + .is_some_and(|root| surface_tree_contains(root, surface))) .then_some(window.id) }) } @@ -191,11 +210,10 @@ impl App { let Some(window_index) = self.window_index_for_surface(surface) else { return; }; - let bbox = bbox_from_surface_tree( - self.windows[window_index].surface.wl_surface(), - Point::::from((0, 0)), - ); - self.windows[window_index].content_bbox_size = bbox.size; + if let Some(root) = self.windows[window_index].surface.wl_surface() { + let bbox = bbox_from_surface_tree(&root, Point::::from((0, 0))); + self.windows[window_index].content_bbox_size = bbox.size; + } } pub(super) fn refresh_normal_window_outputs(&self) { @@ -206,7 +224,9 @@ impl App { let Some(rect) = self.normal_window_screen_rect(window_index) else { continue; }; - self.enter_outputs_intersecting(window.surface.wl_surface(), rect); + if let Some(surface) = window.surface.wl_surface() { + self.enter_outputs_intersecting(&surface, rect); + } } } @@ -222,8 +242,8 @@ impl App { .filter(|window| window.kind == ManagedWindowKind::Normal) .map(|window| ManagedWindowAccessibilityInfo { id: window.id, - app_id: toplevel_app_id(&window.surface), - title: toplevel_title(&window.surface), + app_id: window_app_id(&window.surface), + title: window_title(&window.surface), }) .collect() } @@ -299,18 +319,15 @@ impl App { #[cfg_attr(not(feature = "winit"), allow(dead_code))] pub(super) fn configure_shell_bars(&self) { for window in &self.windows { - if window.kind.is_shell_chrome() { - self.enter_shell_output( - window.surface.wl_surface(), - window.shell_output_name.as_deref(), - ); + if window.kind.is_shell_chrome() + && let Some(surface) = window.surface.wl_surface() + { + self.enter_shell_output(&surface, window.shell_output_name.as_deref()); } - if window.kind == ManagedWindowKind::ShellBar { - self.configure_toplevel( - &window.surface, - window.kind, - window.shell_output_name.as_deref(), - ); + if window.kind == ManagedWindowKind::ShellBar + && let Some(surface) = window.surface.xdg_toplevel() + { + self.configure_toplevel(surface, window.kind, window.shell_output_name.as_deref()); } } } @@ -332,7 +349,7 @@ impl App { if let Some(window) = self .windows .iter_mut() - .find(|window| window.surface == *toplevel) + .find(|window| window.surface.same_xdg(toplevel)) { window.decoration = decoration; } @@ -386,7 +403,7 @@ impl App { /// surface-tree bounding box otherwise. fn window_geometry_size(&self, window_index: usize) -> Size { let window = &self.windows[window_index]; - toplevel_geometry_size(window.surface.wl_surface()) + self.window_surface_geometry_size(window_index) .filter(|size| size.w > 0 && size.h > 0) .unwrap_or(window.content_bbox_size) } @@ -401,7 +418,7 @@ impl App { return window_canvas_rect_for(window.position, window.content_bbox_size, true); } let surface_origin = self.content_canvas_origin(window_index); - let geometry_loc = toplevel_geometry_loc(window.surface.wl_surface()); + let geometry_loc = self.window_surface_geometry_loc(window_index); Rectangle::new( surface_origin + geometry_loc, self.window_geometry_size(window_index), @@ -477,11 +494,23 @@ impl App { let Some(window) = self.windows.iter().find(|window| window.id == window_id) else { return; }; - window.surface.with_pending_state(|state| { - state.states.set(xdg_toplevel::State::Resizing); - state.size = Some(size); - }); - window.surface.send_configure(); + match &window.surface { + ManagedSurface::Xdg(surface) => { + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(size); + }); + surface.send_configure(); + } + #[cfg(feature = "xwayland")] + ManagedSurface::X11(surface) => { + let rect = + Rectangle::new(Point::from((window.position.x, window.position.y)), size); + if let Err(error) = surface.configure(Some(rect)) { + tracing::warn!(%error, "failed to configure X11 resize"); + } + } + } } /// End the active interactive resize, clearing the `Resizing` state on the @@ -495,11 +524,12 @@ impl App { .windows .iter() .find(|window| window.id == resize.window_id) + && let Some(surface) = window.surface.xdg_toplevel() { - window.surface.with_pending_state(|state| { + surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Resizing); }); - window.surface.send_configure(); + surface.send_configure(); } } @@ -580,6 +610,33 @@ impl App { .to_physical(1), } } + + pub(super) fn window_surface_geometry_loc(&self, window_index: usize) -> Point { + match &self.windows[window_index].surface { + ManagedSurface::Xdg(surface) => toplevel_geometry_loc(surface.wl_surface()), + #[cfg(feature = "xwayland")] + ManagedSurface::X11(surface) => surface.geometry().loc, + } + } + + fn window_surface_geometry_size(&self, window_index: usize) -> Option> { + match &self.windows[window_index].surface { + ManagedSurface::Xdg(surface) => toplevel_geometry_size(surface.wl_surface()), + #[cfg(feature = "xwayland")] + ManagedSurface::X11(surface) => Some(surface.geometry().size), + } + } +} + +fn window_app_id(surface: &ManagedSurface) -> Option { + match surface { + ManagedSurface::Xdg(surface) => toplevel_app_id(surface), + #[cfg(feature = "xwayland")] + ManagedSurface::X11(surface) => { + let class = surface.class(); + (!class.trim().is_empty()).then_some(class) + } + } } fn launcher_logical_origin_for_shell_bar(origin: Point) -> Point { diff --git a/crates/hearthspace-compositor/src/compositor/windows/hit_testing.rs b/crates/hearthspace-compositor/src/compositor/windows/hit_testing.rs index ab51d03..7765261 100644 --- a/crates/hearthspace-compositor/src/compositor/windows/hit_testing.rs +++ b/crates/hearthspace-compositor/src/compositor/windows/hit_testing.rs @@ -9,7 +9,7 @@ use crate::{ }; use super::geometry::resize_edges_at; -use crate::compositor::{App, HitTarget, ManagedWindowKind, rendering::toplevel_geometry_loc}; +use crate::compositor::{App, HitTarget, ManagedWindowKind}; impl App { pub(in crate::compositor) fn hit_test( @@ -60,10 +60,11 @@ impl App { // Popups (menus) sit above this window's content and chrome, so // they are hit-tested first. Their location comes from the popup's // configured offset relative to the parent surface origin. - let geometry_loc = toplevel_geometry_loc(window.surface.wl_surface()); - for (popup, popup_offset) in - PopupManager::popups_for_surface(window.surface.wl_surface()) - { + let Some(root) = window.surface.wl_surface() else { + continue; + }; + let geometry_loc = self.window_surface_geometry_loc(window_index); + for (popup, popup_offset) in PopupManager::popups_for_surface(&root) { let popup_origin = content_origin + geometry_loc + popup_offset - popup.geometry().loc; let hit = under_from_surface_tree( @@ -112,7 +113,7 @@ impl App { } if let Some((surface, surface_location)) = under_from_surface_tree( - window.surface.wl_surface(), + &root, canvas_location, content_origin, WindowSurfaceType::ALL, @@ -138,18 +139,15 @@ impl App { scale: f64, ) -> Option { let window = &self.windows[window_index]; + let root = window.surface.wl_surface()?; let scale = scale.max(1.0); let origin_f64 = origin.to_f64(); let unscaled_location = Point::from(( origin_f64.x + (location.x - origin_f64.x) / scale, origin_f64.y + (location.y - origin_f64.y) / scale, )); - let (surface, surface_location) = under_from_surface_tree( - window.surface.wl_surface(), - unscaled_location, - origin, - WindowSurfaceType::ALL, - )?; + let (surface, surface_location) = + under_from_surface_tree(&root, unscaled_location, origin, WindowSurfaceType::ALL)?; let relative_surface_location = unscaled_location - surface_location.to_f64(); let pointer_focus_origin = location - relative_surface_location; Some(HitTarget::Client { diff --git a/crates/hearthspace-compositor/src/compositor/xwayland/handlers.rs b/crates/hearthspace-compositor/src/compositor/xwayland/handlers.rs new file mode 100644 index 0000000..e19051d --- /dev/null +++ b/crates/hearthspace-compositor/src/compositor/xwayland/handlers.rs @@ -0,0 +1,381 @@ +use smithay::{ + input::{Seat, SeatHandler, SeatState, dnd::DndGrabHandler, pointer::CursorImageStatus}, + utils::{Logical, Rectangle}, + wayland::{ + compositor::{CompositorClientState, CompositorHandler, CompositorState}, + selection::{ + SelectionHandler, SelectionTarget, + data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler}, + }, + xwayland_shell::{XWaylandShellHandler, XWaylandShellState}, + }, + xwayland::{ + X11Surface, X11Wm, XwmHandler, + xwm::{Reorder, ResizeEdge as X11ResizeEdge, WmWindowProperty, XwmId}, + }, +}; +use tracing::{debug, warn}; +use wayland_server::{Client, Resource, protocol::wl_surface::WlSurface}; + +use crate::compositor::{App, CalloopData, DragState, WindowDecoration, windows::ResizeEdges}; + +impl XWaylandShellHandler for App { + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { + &mut self._xwayland_shell_state + } + + fn surface_associated(&mut self, _xwm: XwmId, wl_surface: WlSurface, surface: X11Surface) { + debug!( + x11_window = surface.window_id(), + wl_surface = ?wl_surface.id().protocol_id(), + "X11 window associated with wl_surface" + ); + if surface.is_mapped() { + self.map_x11_window(surface, false); + } + } +} + +impl DndGrabHandler for App {} + +impl SelectionHandler for CalloopData { + type SelectionUserData = (); +} + +impl WaylandDndGrabHandler for CalloopData {} + +impl DataDeviceHandler for CalloopData { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.state.data_device_state + } +} + +impl SeatHandler for CalloopData { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.xwayland_seat_state + } + + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&Self::KeyboardFocus>) {} + + fn cursor_image(&mut self, _seat: &Seat, _image: CursorImageStatus) {} +} + +impl DndGrabHandler for CalloopData {} + +impl CompositorHandler for CalloopData { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.state.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + self.state.client_compositor_state(client) + } + + fn commit(&mut self, surface: &WlSurface) { + CompositorHandler::commit(&mut self.state, surface); + } +} + +impl XWaylandShellHandler for CalloopData { + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { + &mut self.state._xwayland_shell_state + } + + fn surface_associated(&mut self, xwm: XwmId, wl_surface: WlSurface, surface: X11Surface) { + XWaylandShellHandler::surface_associated(&mut self.state, xwm, wl_surface, surface); + } +} + +impl XwmHandler for App { + fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm { + self.xwm.as_mut().expect("XWM callback without X11Wm") + } + + fn new_window(&mut self, _xwm: XwmId, window: X11Surface) { + debug!(x11_window = window.window_id(), "new unmapped X11 window"); + } + + fn new_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { + debug!( + x11_window = window.window_id(), + "new unmapped override-redirect X11 window" + ); + } + + fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) { + if let Err(error) = window.set_mapped(true) { + warn!(%error, x11_window = window.window_id(), "failed to map X11 window"); + return; + } + self.map_x11_window(window, false); + } + + fn map_window_notify(&mut self, _xwm: XwmId, window: X11Surface) { + self.map_x11_window(window, false); + } + + fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { + self.map_x11_window(window, true); + } + + fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) { + self.unmap_x11_window(&window); + if !window.is_override_redirect() + && let Err(error) = window.set_mapped(false) + { + warn!(%error, x11_window = window.window_id(), "failed to unmap X11 window"); + } + } + + fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) { + self.unmap_x11_window(&window); + } + + fn configure_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + x: Option, + y: Option, + w: Option, + h: Option, + _reorder: Option, + ) { + let mut geometry = window.last_configure(); + if window.is_override_redirect() { + if let Some(x) = x { + geometry.loc.x = x; + } + if let Some(y) = y { + geometry.loc.y = y; + } + } + if let Some(w) = w { + geometry.size.w = w as i32; + } + if let Some(h) = h { + geometry.size.h = h as i32; + } + if let Err(error) = window.configure(Some(geometry)) { + warn!(%error, x11_window = window.window_id(), "failed to configure X11 window"); + } + } + + fn configure_notify( + &mut self, + _xwm: XwmId, + window: X11Surface, + geometry: Rectangle, + _above: Option, + ) { + if let Some(index) = self.window_index_for_x11(&window) { + self.windows[index].content_bbox_size = geometry.size; + if window.is_override_redirect() { + self.windows[index].position = crate::geometry::CanvasPoint { + x: geometry.loc.x, + y: geometry.loc.y, + }; + } + self.request_redraw(); + } + } + + fn property_notify(&mut self, _xwm: XwmId, window: X11Surface, property: WmWindowProperty) { + if matches!( + property, + WmWindowProperty::Title | WmWindowProperty::Class | WmWindowProperty::MotifHints + ) && let Some(index) = self.window_index_for_x11(&window) + { + self.windows[index].titlebar = None; + self.windows[index].decoration = if window.is_decorated() { + WindowDecoration::ClientSide + } else { + WindowDecoration::ServerSide + }; + self.request_redraw(); + } + } + + fn resize_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + _button: u32, + resize_edge: X11ResizeEdge, + ) { + let Some(index) = self.window_index_for_x11(&window) else { + return; + }; + let index = self.raise_window(index); + self.start_resize(index, ResizeEdges::from(resize_edge)); + } + + fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) { + let Some(index) = self.window_index_for_x11(&window) else { + return; + }; + let index = self.raise_window(index); + self.drag = Some(DragState { + window_id: self.windows[index].id, + pointer_start: self.raw_pointer_location, + window_start: self.windows[index].position, + }); + self.request_redraw(); + } + + fn active_window_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + _timestamp: u32, + _currently_active_window: Option, + ) { + self.focus_x11_window(&window); + } + + fn allow_selection_access(&mut self, _xwm: XwmId, _selection: SelectionTarget) -> bool { + false + } +} + +impl XwmHandler for CalloopData { + fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm { + XwmHandler::xwm_state(&mut self.state, xwm) + } + + fn new_window(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::new_window(&mut self.state, xwm, window); + } + + fn new_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::new_override_redirect_window(&mut self.state, xwm, window); + } + + fn map_window_request(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::map_window_request(&mut self.state, xwm, window); + } + + fn map_window_notify(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::map_window_notify(&mut self.state, xwm, window); + } + + fn mapped_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::mapped_override_redirect_window(&mut self.state, xwm, window); + } + + fn unmapped_window(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::unmapped_window(&mut self.state, xwm, window); + } + + fn destroyed_window(&mut self, xwm: XwmId, window: X11Surface) { + XwmHandler::destroyed_window(&mut self.state, xwm, window); + } + + fn configure_request( + &mut self, + xwm: XwmId, + window: X11Surface, + x: Option, + y: Option, + w: Option, + h: Option, + reorder: Option, + ) { + XwmHandler::configure_request(&mut self.state, xwm, window, x, y, w, h, reorder); + } + + fn configure_notify( + &mut self, + xwm: XwmId, + window: X11Surface, + geometry: Rectangle, + above: Option, + ) { + XwmHandler::configure_notify(&mut self.state, xwm, window, geometry, above); + } + + fn property_notify(&mut self, xwm: XwmId, window: X11Surface, property: WmWindowProperty) { + XwmHandler::property_notify(&mut self.state, xwm, window, property); + } + + fn resize_request( + &mut self, + xwm: XwmId, + window: X11Surface, + button: u32, + resize_edge: X11ResizeEdge, + ) { + XwmHandler::resize_request(&mut self.state, xwm, window, button, resize_edge); + } + + fn move_request(&mut self, xwm: XwmId, window: X11Surface, button: u32) { + XwmHandler::move_request(&mut self.state, xwm, window, button); + } + + fn active_window_request( + &mut self, + xwm: XwmId, + window: X11Surface, + timestamp: u32, + currently_active_window: Option, + ) { + XwmHandler::active_window_request( + &mut self.state, + xwm, + window, + timestamp, + currently_active_window, + ); + } + + fn allow_selection_access(&mut self, xwm: XwmId, selection: SelectionTarget) -> bool { + XwmHandler::allow_selection_access(&mut self.state, xwm, selection) + } +} + +impl From for ResizeEdges { + fn from(edge: X11ResizeEdge) -> Self { + match edge { + X11ResizeEdge::Top => Self { + top: true, + ..Self::default() + }, + X11ResizeEdge::Bottom => Self { + bottom: true, + ..Self::default() + }, + X11ResizeEdge::Left => Self { + left: true, + ..Self::default() + }, + X11ResizeEdge::Right => Self { + right: true, + ..Self::default() + }, + X11ResizeEdge::TopLeft => Self { + top: true, + left: true, + ..Self::default() + }, + X11ResizeEdge::TopRight => Self { + top: true, + right: true, + ..Self::default() + }, + X11ResizeEdge::BottomLeft => Self { + bottom: true, + left: true, + ..Self::default() + }, + X11ResizeEdge::BottomRight => Self { + bottom: true, + right: true, + ..Self::default() + }, + } + } +} diff --git a/crates/hearthspace-compositor/src/compositor/xwayland/mod.rs b/crates/hearthspace-compositor/src/compositor/xwayland/mod.rs new file mode 100644 index 0000000..81719d6 --- /dev/null +++ b/crates/hearthspace-compositor/src/compositor/xwayland/mod.rs @@ -0,0 +1,173 @@ +use std::process::Stdio; + +use smithay::{ + wayland::compositor::CompositorHandler, + xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent}, +}; +use tracing::{error, info, trace, warn}; +use wayland_server::DisplayHandle; + +use super::{ + App, ManagedSurface, ManagedWindow, ManagedWindowKind, WindowDecoration, + windows::position_for_new_window, +}; + +mod handlers; + +impl App { + pub(super) fn start_xwayland(&mut self, dh: &DisplayHandle) { + let Ok((xwayland, client)) = XWayland::spawn( + dh, + None, + std::iter::empty::<(String, String)>(), + std::iter::empty::(), + true, + Stdio::null(), + Stdio::null(), + |_| (), + ) else { + warn!("XWayland is unavailable; X11 applications will not be supported"); + return; + }; + + let display_handle = dh.clone(); + let ret = self + .loop_handle + .insert_source(xwayland, move |event, _, data| match event { + XWaylandEvent::Ready { + x11_socket, + display_number, + } => { + data.state + .client_compositor_state(&client) + .set_client_scale(1.0); + match X11Wm::start_wm( + data.state.loop_handle.clone(), + &display_handle, + x11_socket, + client.clone(), + ) { + Ok(wm) => { + data.state.xwm = Some(wm); + data.state.xdisplay = Some(display_number); + info!( + display = format_args!(":{display_number}"), + "XWayland is ready" + ); + } + Err(error) => { + error!(%error, "failed to start XWayland window manager"); + } + } + } + XWaylandEvent::Error => { + warn!("XWayland exited during startup"); + } + }); + if let Err(error) = ret { + error!(%error, "failed to register XWayland event source"); + } + } + + fn map_x11_window(&mut self, window: X11Surface, override_redirect: bool) { + if self + .windows + .iter() + .any(|managed| managed.surface.same_x11(&window)) + { + return; + } + + let Some(wl_surface) = window.wl_surface() else { + trace!( + window = window.window_id(), + "X11 window has no wl_surface yet" + ); + return; + }; + + let id = self.next_window_id; + self.next_window_id += 1; + let geometry = window.geometry(); + let position = if override_redirect { + crate::geometry::CanvasPoint { + x: geometry.loc.x, + y: geometry.loc.y, + } + } else { + position_for_new_window(ManagedWindowKind::Normal, self.next_spawn_position) + }; + let decoration = if window.is_decorated() || override_redirect { + WindowDecoration::ClientSide + } else { + WindowDecoration::ServerSide + }; + + info!( + window_id = id, + x11_window = window.window_id(), + title = %window.title(), + class = %window.class(), + override_redirect, + "new X11 window" + ); + self.windows.push(ManagedWindow { + id, + surface: ManagedSurface::X11(Box::new(window.clone())), + position, + kind: ManagedWindowKind::Normal, + shell_output_name: None, + decoration, + titlebar: None, + content_bbox_size: geometry.size, + advertised_fractional_scale: 0.0, + }); + self.idle_daemon.register_window(id); + let spawn_output_name = self.next_spawn_output_name.take(); + self.enter_output(&wl_surface, spawn_output_name.as_deref()); + self.refresh_window_content_bbox(&wl_surface); + self.request_redraw(); + } + + fn unmap_x11_window(&mut self, window: &X11Surface) { + if let Some(managed) = self + .windows + .iter() + .find(|managed| managed.surface.same_x11(window)) + { + info!( + window_id = managed.id, + x11_window = window.window_id(), + "X11 window unmapped" + ); + self.idle_daemon.unregister_window(managed.id); + if self.focused_normal_window_id == Some(managed.id) { + self.focused_normal_window_id = None; + } + } + self.windows + .retain(|managed| !managed.surface.same_x11(window)); + self.drag = None; + self.resize = None; + self.request_redraw(); + } + + fn window_index_for_x11(&self, window: &X11Surface) -> Option { + self.windows + .iter() + .position(|managed| managed.surface.same_x11(window)) + } + + fn focus_x11_window(&mut self, window: &X11Surface) { + let Some(window_index) = self.window_index_for_x11(window) else { + return; + }; + let window_index = self.raise_window(window_index); + if let Some(surface) = self.windows[window_index].surface.wl_surface() { + self.set_keyboard_focus_to_window(window_index, surface); + } + if let Err(error) = window.set_activated(true) { + warn!(%error, "failed to activate X11 window"); + } + } +} diff --git a/todos/NATIVE_BACKEND.md b/todos/NATIVE_BACKEND.md index 78f0a3d..aab3e8c 100644 --- a/todos/NATIVE_BACKEND.md +++ b/todos/NATIVE_BACKEND.md @@ -122,7 +122,7 @@ them reliably. ## Non-Goals For The First Native Milestone -- [ ] X11/Xwayland support. +- [ ] Full X11/XWayland polish beyond first-pass window mapping. - [ ] Direct scanout. - [ ] VRR/HDR/color management. - [ ] Complex multi-monitor layout policy. diff --git a/todos/ROADMAP.md b/todos/ROADMAP.md index ad27af2..6699655 100644 --- a/todos/ROADMAP.md +++ b/todos/ROADMAP.md @@ -12,6 +12,8 @@ edges) or intentionally deferred. Native backend work lives in `Super`-modified scroll, but there is no pinch gesture zoom yet. - [ ] **Optional desktop protocols** — several optional protocols are not implemented yet, so clients may print warnings. +- [ ] **XWayland polish** — X11 apps can map through XWayland, but clipboard, + drag-and-drop, and full EWMH state handling still need follow-up work. ## Deferred Scope @@ -22,8 +24,3 @@ edges) or intentionally deferred. Native backend work lives in - [ ] **Theming** beyond the current proof-of-concept shell UI. - [ ] **DRM/KMS backend and libinput device management** — tracked in [NATIVE_BACKEND.md](./NATIVE_BACKEND.md). - -## Out of Scope - -- **X11/Xwayland support** — intentionally out of scope unless a concrete need - appears. -- 2.51.2