From 6f31cf779b4ae5f7a3ab82c6123565bd5dc415c2 Mon Sep 17 00:00:00 2001 From: jess Date: Sat, 25 Apr 2026 13:38:48 -0700 Subject: [PATCH] Wayland --- Cargo.toml | 2 + src/bin/layers_shell.rs | 12 + src/lib.rs | 3 + src/wayland_shell.rs | 523 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 540 insertions(+) create mode 100644 src/wayland_shell.rs diff --git a/Cargo.toml b/Cargo.toml index 3bc5f08..0bab2c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,3 +81,5 @@ windows = { version = "0.58", features = [ [target.'cfg(target_os = "linux")'.dependencies] x11-dl = "2" +smithay-client-toolkit = "0.20" +wayland-client = "0.31" diff --git a/src/bin/layers_shell.rs b/src/bin/layers_shell.rs index 2445b7b..d8325f4 100644 --- a/src/bin/layers_shell.rs +++ b/src/bin/layers_shell.rs @@ -30,6 +30,18 @@ fn main() { "layers shell: starting native event loop", ); + #[cfg(target_os = "linux")] + { + if std::env::var_os("WAYLAND_DISPLAY").is_some() { + if layers::wayland_shell::try_run() { + return; + } + // Wayland up but no layer-shell (mutter/GNOME): use XWayland via winit's + // X11 backend so the existing X11 always-on-top path applies. + std::env::set_var("WINIT_UNIX_BACKEND", "x11"); + } + } + if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)) { tracing::error!("layers shell panicked: {e:?}"); std::process::exit(1); diff --git a/src/lib.rs b/src/lib.rs index 7f9dd74..35cce6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,6 @@ pub mod single_instance; pub mod snapshot; pub mod ui; pub mod version; + +#[cfg(target_os = "linux")] +pub mod wayland_shell; diff --git a/src/wayland_shell.rs b/src/wayland_shell.rs new file mode 100644 index 0000000..82515aa --- /dev/null +++ b/src/wayland_shell.rs @@ -0,0 +1,523 @@ +use crate::ffi::ViewportHandle; +use raw_window_handle::{ + RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, +}; +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer, + delegate_registry, delegate_seat, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers}, + pointer::{PointerEvent, PointerEventKind, PointerHandler}, + Capability, SeatHandler, SeatState, + }, + shell::{ + wlr_layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface, + LayerSurfaceConfigure, + }, + WaylandSurface, + }, +}; +use std::ptr::NonNull; +use wayland_client::{ + globals::registry_queue_init, + protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface}, + Connection, Proxy, QueueHandle, +}; + +const DEFAULT_LOGICAL_SIZE: (u32, u32) = (480, 640); +const MIN_LOGICAL_SIZE: (u32, u32) = (380, 220); +const ANCHOR_MARGIN: i32 = 24; + +/// Try to run the Wayland (wlr-layer-shell) shell. Returns `true` if Wayland was used +/// — successfully started or not. Returns `false` only when no Wayland session was +/// available or the compositor doesn't advertise wlr-layer-shell, so the caller can +/// fall through to the winit/X11 path. +pub fn try_run() -> bool { + let conn = match Connection::connect_to_env() { + Ok(c) => c, + Err(_) => { + tracing::info!("wayland: no WAYLAND_DISPLAY, falling through to X11"); + return false; + } + }; + + let (globals, mut event_queue) = match registry_queue_init::(&conn) { + Ok(pair) => pair, + Err(e) => { + tracing::warn!("wayland: registry init failed ({e}); falling through to X11"); + return false; + } + }; + let qh = event_queue.handle(); + + let compositor = match CompositorState::bind(&globals, &qh) { + Ok(c) => c, + Err(_) => { + tracing::warn!("wayland: wl_compositor missing; falling through to X11"); + return false; + } + }; + + let layer_shell = match LayerShell::bind(&globals, &qh) { + Ok(l) => l, + Err(_) => { + tracing::warn!( + "wayland: zwlr_layer_shell_v1 not advertised (likely GNOME/mutter); falling through to X11" + ); + return false; + } + }; + + let surface = compositor.create_surface(&qh); + let layer = layer_shell.create_layer_surface( + &qh, + surface, + Layer::Top, + Some("layers"), + None, + ); + layer.set_anchor(Anchor::TOP | Anchor::RIGHT); + layer.set_size(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1); + layer.set_margin(ANCHOR_MARGIN, ANCHOR_MARGIN, 0, 0); + layer.set_keyboard_interactivity(KeyboardInteractivity::OnDemand); + layer.commit(); + + let raw_display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new( + match NonNull::new(conn.backend().display_ptr() as *mut _) { + Some(p) => p, + None => { + tracing::error!("wayland: null display ptr"); + return true; + } + }, + )); + let raw_window = RawWindowHandle::Wayland(WaylandWindowHandle::new( + match NonNull::new(layer.wl_surface().id().as_ptr() as *mut _) { + Some(p) => p, + None => { + tracing::error!("wayland: null surface ptr"); + return true; + } + }, + )); + + let mut state = State { + registry_state: RegistryState::new(&globals), + seat_state: SeatState::new(&globals, &qh), + output_state: OutputState::new(&globals, &qh), + viewport: None, + layer, + keyboard: None, + pointer: None, + modifiers: Modifiers::default(), + last_pointer: (0.0, 0.0), + width: DEFAULT_LOGICAL_SIZE.0, + height: DEFAULT_LOGICAL_SIZE.1, + scale: 1, + first_configure: true, + raw_display, + raw_window, + exit: false, + }; + + tracing::info!("wayland: layer-shell up, entering dispatch loop"); + while !state.exit { + if let Err(e) = event_queue.blocking_dispatch(&mut state) { + tracing::error!("wayland dispatch: {e}"); + break; + } + } + + true +} + +struct State { + registry_state: RegistryState, + seat_state: SeatState, + output_state: OutputState, + /// Held above `layer` so the wgpu surface drops before the wl_surface. + viewport: Option, + layer: LayerSurface, + keyboard: Option, + pointer: Option, + modifiers: Modifiers, + last_pointer: (f32, f32), + width: u32, + height: u32, + scale: i32, + first_configure: bool, + raw_display: RawDisplayHandle, + raw_window: RawWindowHandle, + exit: bool, +} + +impl State { + fn ensure_viewport(&mut self) { + if self.viewport.is_some() { + return; + } + let scale = self.scale.max(1) as f32; + let logical_w = self.width.max(MIN_LOGICAL_SIZE.0) as f32; + let logical_h = self.height.max(MIN_LOGICAL_SIZE.1) as f32; + match ViewportHandle::new_from_raw(self.raw_window, self.raw_display, logical_w, logical_h, scale) { + Some(h) => { + tracing::info!( + "wayland: viewport ready ({}x{} @ {})", + logical_w, + logical_h, + scale + ); + self.viewport = Some(h); + } + None => tracing::error!("wayland: ViewportHandle::new_from_raw returned None"), + } + } + + fn render(&mut self, qh: &QueueHandle) { + if let Some(vp) = self.viewport.as_mut() { + vp.render_frame(); + } + let surface = self.layer.wl_surface(); + surface.frame(qh, surface.clone()); + self.layer.commit(); + } +} + +impl CompositorHandler for State { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + new_factor: i32, + ) { + if self.scale == new_factor { + return; + } + self.scale = new_factor; + self.layer.wl_surface().set_buffer_scale(new_factor); + if let Some(vp) = self.viewport.as_mut() { + vp.resize_px(self.width as f32, self.height as f32, new_factor as f32); + } + } + + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: wl_output::Transform, + ) { + } + + fn frame( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + _: &wl_surface::WlSurface, + _time: u32, + ) { + self.render(qh); + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } +} + +impl OutputHandler for State { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} +} + +impl LayerShellHandler for State { + fn closed(&mut self, _: &Connection, _: &QueueHandle, _: &LayerSurface) { + self.exit = true; + } + + fn configure( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + _layer: &LayerSurface, + configure: LayerSurfaceConfigure, + _serial: u32, + ) { + let (w, h) = configure.new_size; + let new_w = if w == 0 { self.width } else { w }; + let new_h = if h == 0 { self.height } else { h }; + let resized = new_w != self.width || new_h != self.height; + self.width = new_w; + self.height = new_h; + + self.ensure_viewport(); + if resized { + if let Some(vp) = self.viewport.as_mut() { + vp.resize_px(self.width as f32, self.height as f32, self.scale.max(1) as f32); + } + } + + if self.first_configure { + self.first_configure = false; + self.render(qh); + } + } +} + +impl SeatHandler for State { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_none() { + if let Ok(kb) = self.seat_state.get_keyboard(qh, &seat, None) { + self.keyboard = Some(kb); + } + } + if capability == Capability::Pointer && self.pointer.is_none() { + if let Ok(p) = self.seat_state.get_pointer(qh, &seat) { + self.pointer = Some(p); + } + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _: &QueueHandle, + _: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard { + if let Some(k) = self.keyboard.take() { + k.release(); + } + } + if capability == Capability::Pointer { + if let Some(p) = self.pointer.take() { + p.release(); + } + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +} + +impl KeyboardHandler for State { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _surface: &wl_surface::WlSurface, + _: u32, + _: &[u32], + _keysyms: &[Keysym], + ) { + } + + fn leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _surface: &wl_surface::WlSurface, + _: u32, + ) { + } + + fn press_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + event: KeyEvent, + ) { + if let Some(vp) = self.viewport.as_mut() { + let (named, utf8) = map_key_event(&event); + vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), true); + } + } + + fn release_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + event: KeyEvent, + ) { + if let Some(vp) = self.viewport.as_mut() { + let (named, utf8) = map_key_event(&event); + vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), false); + } + } + + fn repeat_key( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _: u32, + _event: KeyEvent, + ) { + } + + fn update_modifiers( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_keyboard::WlKeyboard, + _serial: u32, + modifiers: Modifiers, + _raw: RawModifiers, + _layout: u32, + ) { + self.modifiers = modifiers; + } +} + +impl PointerHandler for State { + fn pointer_frame( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_pointer::WlPointer, + events: &[PointerEvent], + ) { + let Some(vp) = self.viewport.as_mut() else { + return; + }; + for event in events { + if &event.surface != self.layer.wl_surface() { + continue; + } + let (px, py) = event.position; + let lx = px as f32; + let ly = py as f32; + self.last_pointer = (lx, ly); + match event.kind { + PointerEventKind::Enter { .. } => { + vp.push_mouse_move(lx, ly); + } + PointerEventKind::Leave { .. } => { + vp.push_mouse_left(); + } + PointerEventKind::Motion { .. } => { + vp.push_mouse_move(lx, ly); + } + PointerEventKind::Press { button, .. } => { + if let Some(code) = map_button(button) { + vp.push_mouse_button(lx, ly, code, true); + } + } + PointerEventKind::Release { button, .. } => { + if let Some(code) = map_button(button) { + vp.push_mouse_button(lx, ly, code, false); + } + } + PointerEventKind::Axis { + horizontal, + vertical, + .. + } => { + let dx = -(horizontal.absolute as f32); + let dy = -(vertical.absolute as f32); + vp.push_mouse_scroll(lx, ly, dx, dy); + } + } + } + } +} + +fn map_button(button: u32) -> Option { + match button { + 0x110 => Some(0), // BTN_LEFT + 0x111 => Some(1), // BTN_RIGHT + 0x112 => Some(2), // BTN_MIDDLE + _ => None, + } +} + +fn map_key_event(event: &KeyEvent) -> (u32, Option) { + let named = match event.keysym { + Keysym::Return => 1, + Keysym::Escape => 2, + Keysym::BackSpace => 3, + Keysym::Tab => 4, + Keysym::Left => 5, + Keysym::Right => 6, + Keysym::Up => 7, + Keysym::Down => 8, + Keysym::Delete => 9, + Keysym::Home => 10, + Keysym::End => 11, + _ => 0, + }; + let utf8 = if named == 0 { event.utf8.clone() } else { None }; + (named, utf8) +} + +fn encode_modifiers(mods: &Modifiers) -> u32 { + let mut bits = 0u32; + if mods.shift { + bits |= 1; + } + if mods.ctrl { + bits |= 2; + } + if mods.alt { + bits |= 4; + } + if mods.logo { + bits |= 8; + } + bits +} + +delegate_compositor!(State); +delegate_output!(State); +delegate_seat!(State); +delegate_keyboard!(State); +delegate_pointer!(State); +delegate_layer!(State); +delegate_registry!(State); + +impl ProvidesRegistryState for State { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState, SeatState]; +}