diff --git a/Cargo.lock b/Cargo.lock index 6263f91..760b3d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,6 +345,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jni-sys" version = "0.3.1" @@ -492,6 +498,16 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "niri-ipc" +version = "25.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bd9eb4e437f5282ce853cf66837658379cb537b4b6bae687da59246005329a" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "nix" version = "0.29.0" @@ -731,6 +747,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytemuck", + "niri-ipc", "nix", "pollster", "raw-window-handle", @@ -807,6 +824,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1366,3 +1396,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c65a9f8..fd52d61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ rust-version = "1.85" [dependencies] anyhow = "1.0" bytemuck = { version = "1", features = ["derive"] } +niri-ipc = "25.11.0" nix = { version = "0.29", features = ["signal", "process", "poll"] } pollster = "0.4" raw-window-handle = "0.6" diff --git a/src/main.rs b/src/main.rs index 34fb999..36b50a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,8 +37,9 @@ fn main() -> Result<()> { color, )?; - let mut cursor_x = w as f64 / 2.0; - let mut cursor_y = h as f64 / 2.0; + // Initial cursor offscreen — will update when pointer enters ring zone. + let mut cursor_x = -10000.0_f64; + let mut cursor_y = -10000.0_f64; renderer.render(cursor_x, cursor_y)?; @@ -76,6 +77,9 @@ fn main() -> Result<()> { OverlayEvent::CursorMoved(x, y) => { cursor_x = x; cursor_y = y; + // Punch a passthrough hole at the new cursor position so + // clicks go to the desktop, then re-render the shader. + wl.update_cursor_hole(cursor_x, cursor_y)?; dirty = true; } OverlayEvent::Configure(_, _) => {} @@ -98,7 +102,7 @@ fn main() -> Result<()> { fn parse_color_arg() -> [f32; 3] { let arg = match std::env::args().nth(1) { Some(a) => a, - None => return [1.0, 1.0, 1.0], // pure white + None => return [1.0, 1.0, 1.0], }; let kelvin = match arg.to_lowercase().as_str() { @@ -111,9 +115,7 @@ fn parse_color_arg() -> [f32; 3] { other => match other.parse::() { Ok(k) if (1000..=40000).contains(&k) => k, _ => { - eprintln!( - "usage: ringa [white|warm|golden|studio|daylight|cool|]" - ); + eprintln!("usage: ringa [white|warm|golden|studio|daylight|cool|]"); std::process::exit(1); } }, diff --git a/src/wayland.rs b/src/wayland.rs index 7795706..703c64b 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -1,7 +1,10 @@ -//! Wayland backend: layer-shell overlay with keyboard and pointer tracking. +//! Wayland backend: layer-shell overlay with pointer tracking. //! -//! Creates a full-screen layer-shell surface. Exposes raw Wayland handles -//! for wgpu surface creation. No CPU pixel buffers — GPU renders directly. +//! The input region is dynamically updated to be the full screen MINUS a +//! hole around the cursor. This lets the cursor pass through to the desktop +//! while still letting us track its position: when the cursor moves outside +//! the hole, it lands on our surface, we get the event, punch a new hole, +//! and the cursor passes through again. use std::collections::VecDeque; use std::ptr::NonNull; @@ -22,19 +25,20 @@ use raw_window_handle::{ RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, }; -const KEY_ESCAPE: u32 = 1; +/// Size of the passthrough hole around the cursor. Should be large enough +/// that normal mouse movement stays within the hole between frames. +const CURSOR_HOLE_SIZE: i32 = 512; -#[allow(dead_code)] pub enum OverlayEvent { Close, CursorMoved(f64, f64), + #[allow(dead_code)] Configure(u32, u32), } pub struct WaylandBackend { state: WaylandState, event_queue: EventQueue, - #[allow(dead_code)] queue_handle: QueueHandle, } @@ -96,14 +100,12 @@ impl WaylandBackend { (self.state.screen_w, self.state.screen_h) } - /// Raw Wayland display handle for wgpu surface creation. pub fn raw_display_handle(&self) -> Result { let ptr = self.state.conn.backend().display_ptr() as *mut std::ffi::c_void; let nn = NonNull::new(ptr).context("wl_display pointer is null")?; Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle::new(nn))) } - /// Raw Wayland surface handle for wgpu surface creation. pub fn raw_window_handle(&self) -> Result { let surface = self.state.surface.as_ref().context("no wl_surface")?; let ptr = surface.id().as_ptr() as *mut std::ffi::c_void; @@ -111,7 +113,32 @@ impl WaylandBackend { Ok(RawWindowHandle::Wayland(WaylandWindowHandle::new(nn))) } - /// Wait up to `timeout` for the next event. Returns `None` on timeout. + /// Update the input region to punch a passthrough hole at the cursor. + pub fn update_cursor_hole(&mut self, cx: f64, cy: f64) -> Result<()> { + let compositor = self + .state + .compositor + .as_ref() + .context("wl_compositor not available")?; + let surface = self.state.surface.as_ref().context("no wl_surface")?; + + let w = self.state.screen_w as i32; + let h = self.state.screen_h as i32; + let half = CURSOR_HOLE_SIZE / 2; + let hx = (cx as i32).clamp(0, w); + let hy = (cy as i32).clamp(0, h); + + // Full screen minus a hole around the cursor. + let region = compositor.create_region(&self.queue_handle, ()); + region.add(0, 0, w, h); + region.subtract(hx - half, hy - half, CURSOR_HOLE_SIZE, CURSOR_HOLE_SIZE); + + surface.set_input_region(Some(®ion)); + surface.commit(); + self.state.conn.flush().context("flush after cursor hole")?; + Ok(()) + } + pub fn next_event(&mut self, timeout: Duration) -> Result> { if let Some(ev) = self.state.pending_events.pop_front() { return Ok(Some(ev)); @@ -124,15 +151,12 @@ impl WaylandBackend { .flush() .context("flush before poll")?; - // Try to dispatch any already-buffered events first. self.event_queue .dispatch_pending(&mut self.state) .context("dispatch_pending (pre-poll)")?; if let Some(ev) = self.state.pending_events.pop_front() { - // Re-run coalescing below with this event re-queued. self.state.pending_events.push_front(ev); } else { - // No pending events — try to read from the fd with a timeout. match self.event_queue.prepare_read() { Some(read_guard) => { let fd = read_guard.connection_fd(); @@ -153,7 +177,6 @@ impl WaylandBackend { .context("dispatch_pending (post-read)")?; } None => { - // Another queue has events buffered — dispatch ours. self.event_queue .dispatch_pending(&mut self.state) .context("dispatch_pending (no-read)")?; @@ -161,7 +184,7 @@ impl WaylandBackend { } } - // Coalesce CursorMoved events. + // Coalesce CursorMoved — only keep the latest position. let mut last_cursor: Option<(f64, f64)> = None; let mut first_non_cursor: Option = None; while let Some(ev) = self.state.pending_events.pop_front() { @@ -236,8 +259,12 @@ impl WaylandState { let surface = compositor.create_surface(qh, ()); + // Start with full-screen input region. The cursor hole will be + // punched on the first pointer event. let input_region = compositor.create_region(qh, ()); - input_region.add(0, 0, i32::MAX, i32::MAX); + let w = i32::MAX; + let h = i32::MAX; + input_region.add(0, 0, w, h); surface.set_input_region(Some(&input_region)); let layer_surface = layer_shell.get_layer_surface( @@ -258,7 +285,7 @@ impl WaylandState { ); layer_surface.set_exclusive_zone(-1); layer_surface - .set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive); + .set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None); surface.commit(); @@ -320,9 +347,6 @@ impl Dispatch for WaylandState { 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, ()); } @@ -330,28 +354,6 @@ impl Dispatch for WaylandState { } } -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, @@ -381,6 +383,18 @@ impl Dispatch for WaylandState { } } +impl Dispatch for WaylandState { + fn event( + _: &mut Self, + _: &wl_keyboard::WlKeyboard, + _: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + impl Dispatch for WaylandState { fn event( state: &mut Self,