//! Wayland backend: layer-shell overlay with keyboard and pointer tracking. //! //! Creates a full-screen layer-shell surface for the ring light overlay. //! Tracks pointer position for cursor avoidance and captures keyboard //! for Escape to exit. use std::collections::VecDeque; use std::io::Write; use std::os::fd::AsFd; use std::time::Duration; use wayland_client::{ delegate_noop, protocol::{ wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, }, Connection, Dispatch, EventQueue, QueueHandle, WEnum, }; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use anyhow::{Context, Result}; // Linux evdev key code for Escape. const KEY_ESCAPE: u32 = 1; /// Events emitted by the Wayland backend to the main loop. pub enum OverlayEvent { /// Escape pressed or surface closed by compositor. Close, /// Pointer moved to (x, y) in surface-local coordinates. CursorMoved(f64, f64), /// Surface (re-)configured with new dimensions. Configure(u32, u32), } // =========================================================================== // WaylandBackend — public API. // =========================================================================== pub struct WaylandBackend { state: WaylandState, event_queue: EventQueue, queue_handle: QueueHandle, } impl WaylandBackend { pub fn new() -> Result { let conn = Connection::connect_to_env().context("connect to Wayland display")?; let mut event_queue = conn.new_event_queue(); let queue_handle = event_queue.handle(); conn.display().get_registry(&queue_handle, ()); let mut state = WaylandState { conn, compositor: None, shm: None, surface: None, layer_shell: None, layer_surface: None, seat: None, screen_w: 0, screen_h: 0, configured: false, pending_events: VecDeque::new(), }; event_queue .roundtrip(&mut state) .context("initial roundtrip")?; if state.compositor.is_none() { anyhow::bail!("compositor does not advertise wl_compositor"); } if state.shm.is_none() { anyhow::bail!("compositor does not advertise wl_shm"); } if state.layer_shell.is_none() { anyhow::bail!( "compositor does not support wlr-layer-shell-unstable-v1. \ Supported compositors: Sway, Hyprland, river, etc." ); } state.init_layer_surface(&queue_handle)?; event_queue .roundtrip(&mut state) .context("roundtrip after layer surface")?; while !state.configured { event_queue .blocking_dispatch(&mut state) .context("waiting for configure")?; } Ok(Self { state, event_queue, queue_handle, }) } pub fn screen_size(&self) -> (u32, u32) { (self.state.screen_w, self.state.screen_h) } /// Wait up to `timeout` for the next event. Returns `None` on timeout /// or if the surface has been torn down. pub fn next_event(&mut self, timeout: Duration) -> Result> { // First drain any already-buffered events. if let Some(ev) = self.state.pending_events.pop_front() { return Ok(Some(ev)); } if self.state.surface.is_none() { return Ok(None); } // Flush outgoing requests, then poll the fd with a timeout. self.event_queue .flush() .context("flush before poll")?; let read_guard = self.event_queue.prepare_read().unwrap(); let fd = read_guard.connection_fd(); let mut pollfd = [nix::poll::PollFd::new(fd, nix::poll::PollFlags::POLLIN)]; let millis = timeout.as_millis().min(u16::MAX as u128) as u16; let n = nix::poll::poll(&mut pollfd, millis).context("poll")?; if n > 0 { // Data available — read and dispatch. let _ = read_guard.read(); } else { // Timeout — drop the read guard (cancels the read). drop(read_guard); return Ok(None); } self.event_queue .dispatch_pending(&mut self.state) .context("dispatch_pending")?; Ok(self.state.pending_events.pop_front()) } /// Copy an ARGB8888 pixel buffer to the overlay surface via wl_shm. pub fn present(&mut self, pixels: &[u8], width: u32, height: u32) -> Result<()> { let shm = self.state.shm.as_ref().context("wl_shm not available")?; let surface = self .state .surface .as_ref() .context("wl_surface not available")?; let stride = width * 4; let mut file = tempfile::tempfile().context("create shm tempfile")?; file.write_all(pixels).context("write pixel buffer")?; let pool = shm.create_pool(file.as_fd(), pixels.len() as i32, &self.queue_handle, ()); let buffer = pool.create_buffer( 0, width as i32, height as i32, stride as i32, wl_shm::Format::Argb8888, &self.queue_handle, (), ); surface.attach(Some(&buffer), 0, 0); surface.damage_buffer(0, 0, width as i32, height as i32); surface.commit(); pool.destroy(); Ok(()) } } impl Drop for WaylandBackend { fn drop(&mut self) { if let Some(ls) = self.state.layer_surface.take() { ls.destroy(); } if let Some(s) = self.state.surface.take() { s.destroy(); } let _ = self.state.conn.flush(); } } // =========================================================================== // Internal state. // =========================================================================== struct WaylandState { conn: Connection, compositor: Option, shm: Option, seat: Option, layer_shell: Option, surface: Option, layer_surface: Option, screen_w: u32, screen_h: u32, configured: bool, pending_events: VecDeque, } impl WaylandState { fn init_layer_surface(&mut self, qh: &QueueHandle) -> Result<()> { let compositor = self .compositor .as_ref() .context("wl_compositor not available")?; let layer_shell = self .layer_shell .as_ref() .context("zwlr_layer_shell_v1 not available")?; let surface = compositor.create_surface(qh, ()); let layer_surface = layer_shell.get_layer_surface( &surface, None, zwlr_layer_shell_v1::Layer::Overlay, "ringa".to_string(), qh, (), ); layer_surface.set_size(0, 0); layer_surface.set_anchor( zwlr_layer_surface_v1::Anchor::Top | zwlr_layer_surface_v1::Anchor::Bottom | zwlr_layer_surface_v1::Anchor::Left | zwlr_layer_surface_v1::Anchor::Right, ); layer_surface.set_exclusive_zone(-1); layer_surface .set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive); surface.commit(); self.surface = Some(surface); self.layer_surface = Some(layer_surface); Ok(()) } } // =========================================================================== // Wayland dispatch implementations. // =========================================================================== impl Dispatch for WaylandState { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { let wl_registry::Event::Global { name, interface, version, } = event else { return; }; match interface.as_str() { "wl_compositor" => { state.compositor = Some(registry.bind(name, version.min(4), qh, ())); } "wl_shm" => { state.shm = Some(registry.bind(name, 1, qh, ())); } "wl_seat" => { state.seat = Some(registry.bind(name, version.min(7), qh, ())); } "zwlr_layer_shell_v1" => { state.layer_shell = Some(registry.bind(name, version.min(4), qh, ())); } _ => {} } } } impl Dispatch for WaylandState { fn event( _state: &mut Self, seat: &wl_seat::WlSeat, event: wl_seat::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { if let wl_seat::Event::Capabilities { capabilities: WEnum::Value(caps), } = event { if caps.contains(wl_seat::Capability::Keyboard) { seat.get_keyboard(qh, ()); } if caps.contains(wl_seat::Capability::Pointer) { seat.get_pointer(qh, ()); } } } } impl Dispatch for WaylandState { fn event( state: &mut Self, _: &wl_keyboard::WlKeyboard, event: wl_keyboard::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let wl_keyboard::Event::Key { key, state: WEnum::Value(wl_keyboard::KeyState::Pressed), .. } = event { if key == KEY_ESCAPE { state.pending_events.push_back(OverlayEvent::Close); } } } } impl Dispatch for WaylandState { fn event( state: &mut Self, _: &wl_pointer::WlPointer, event: wl_pointer::Event, _: &(), _: &Connection, _: &QueueHandle, ) { match event { wl_pointer::Event::Enter { surface_x, surface_y, .. } => { state .pending_events .push_back(OverlayEvent::CursorMoved(surface_x, surface_y)); } wl_pointer::Event::Motion { surface_x, surface_y, .. } => { state .pending_events .push_back(OverlayEvent::CursorMoved(surface_x, surface_y)); } _ => {} } } } impl Dispatch for WaylandState { fn event( state: &mut Self, layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, event: zwlr_layer_surface_v1::Event, _: &(), _: &Connection, _: &QueueHandle, ) { match event { zwlr_layer_surface_v1::Event::Configure { serial, width, height, } => { layer_surface.ack_configure(serial); const MAX_SCREEN_DIM: u32 = 16384; if width > 0 && height > 0 && width <= MAX_SCREEN_DIM && height <= MAX_SCREEN_DIM { state.screen_w = width; state.screen_h = height; state.configured = true; state .pending_events .push_back(OverlayEvent::Configure(width, height)); } } zwlr_layer_surface_v1::Event::Closed => { state.pending_events.push_back(OverlayEvent::Close); } _ => {} } } } delegate_noop!(WaylandState: ignore wl_compositor::WlCompositor); delegate_noop!(WaylandState: ignore wl_region::WlRegion); delegate_noop!(WaylandState: ignore wl_surface::WlSurface); delegate_noop!(WaylandState: ignore wl_shm::WlShm); delegate_noop!(WaylandState: ignore wl_shm_pool::WlShmPool); delegate_noop!(WaylandState: ignore wl_buffer::WlBuffer); delegate_noop!(WaylandState: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);