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.
410 lines
12 KiB
Rust
410 lines
12 KiB
Rust
//! 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);
|