feat: initial project with CPU-rendered ring light overlay
Wayland layer-shell overlay using tiny-skia for rendering a glowing rounded rectangle ring. Includes PID-based lock file toggle via SIGUSR1, keyboard capture (Escape to exit), and pointer tracking for cursor avoidance. Configurable ring geometry and glow parameters.
This commit is contained in:
7 files changed
+1484
No files matched your search
+409
@@ -0,0 +1,409 @@
|
||||
//! 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<WaylandState>,
|
||||
queue_handle: QueueHandle<WaylandState>,
|
||||
}
|
||||
|
||||
impl WaylandBackend {
|
||||
pub fn new() -> Result<Self> {
|
||||
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<Option<OverlayEvent>> {
|
||||
// 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<wl_compositor::WlCompositor>,
|
||||
shm: Option<wl_shm::WlShm>,
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
|
||||
surface: Option<wl_surface::WlSurface>,
|
||||
layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
|
||||
|
||||
screen_w: u32,
|
||||
screen_h: u32,
|
||||
configured: bool,
|
||||
|
||||
pending_events: VecDeque<OverlayEvent>,
|
||||
}
|
||||
|
||||
impl WaylandState {
|
||||
fn init_layer_surface(&mut self, qh: &QueueHandle<Self>) -> 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<wl_registry::WlRegistry, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
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<wl_seat::WlSeat, ()> for WaylandState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: wl_seat::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
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<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,
|
||||
_: &wl_pointer::WlPointer,
|
||||
event: wl_pointer::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
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<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for WaylandState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
event: zwlr_layer_surface_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
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);
|
||||
Reference in new issue
Block a user