feat: full desktop passthrough with dynamic cursor hole
Replace static input region with a dynamically updated wl_region that follows the cursor. The full screen receives pointer events except for a 512px hole around the cursor position, allowing clicks and scrolls to pass through to the desktop. Cursor avoidance shader updates in sync.
This commit is contained in:
4 files changed
+100
-47
No files matched your search
Generated
+36
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
+8
-6
@@ -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::<u32>() {
|
||||
Ok(k) if (1000..=40000).contains(&k) => k,
|
||||
_ => {
|
||||
eprintln!(
|
||||
"usage: ringa [white|warm|golden|studio|daylight|cool|<kelvin>]"
|
||||
);
|
||||
eprintln!("usage: ringa [white|warm|golden|studio|daylight|cool|<kelvin>]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
|
||||
+55
-41
@@ -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<WaylandState>,
|
||||
#[allow(dead_code)]
|
||||
queue_handle: QueueHandle<WaylandState>,
|
||||
}
|
||||
|
||||
@@ -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<RawDisplayHandle> {
|
||||
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<RawWindowHandle> {
|
||||
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<Option<OverlayEvent>> {
|
||||
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<OverlayEvent> = 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<wl_seat::WlSeat, ()> 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<wl_seat::WlSeat, ()> for WaylandState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
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<wl_pointer::WlPointer, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
@@ -381,6 +383,18 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandState {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
|
||||
Reference in new issue
Block a user