feat!: replace CPU rendering with GPU via wgpu/WGSL
Drop tiny-skia and tempfile. All rendering now runs on the GPU using a WGSL fragment shader with rounded-rect SDF, gaussian glow falloff, and cursor avoidance. Add color temperature support with presets (warm, cool, studio, daylight, golden) and custom Kelvin values. Suppress MangoHUD via environment variable.
This commit is contained in:
6 files changed
+1419
-502
No files matched your search
Generated
+911
-240
File diff suppressed because it is too large.
Load diff
+5
-2
@@ -8,8 +8,11 @@ rust-version = "1.85"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
nix = { version = "0.29", features = ["signal", "process", "poll"] }
|
nix = { version = "0.29", features = ["signal", "process", "poll"] }
|
||||||
tempfile = "3"
|
pollster = "0.4"
|
||||||
tiny-skia = "0.12.0"
|
raw-window-handle = "0.6"
|
||||||
|
wayland-backend = { version = "0.3", features = ["client_system"] }
|
||||||
wayland-client = "0.31"
|
wayland-client = "0.31"
|
||||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||||
|
wgpu = "29"
|
||||||
+74
-25
@@ -4,71 +4,120 @@ mod wayland;
|
|||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use tiny_skia::Pixmap;
|
|
||||||
|
|
||||||
use lock::ToggleResult;
|
use lock::ToggleResult;
|
||||||
|
use render::GpuRenderer;
|
||||||
use wayland::{OverlayEvent, WaylandBackend};
|
use wayland::{OverlayEvent, WaylandBackend};
|
||||||
|
|
||||||
const DISPLAY_DURATION: Duration = Duration::from_secs(5);
|
const DISPLAY_DURATION: Duration = Duration::from_secs(10);
|
||||||
|
const FRAME_INTERVAL: Duration = Duration::from_millis(16);
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
// Suppress MangoHUD — it hooks into Vulkan and we don't want an overlay on our overlay.
|
||||||
|
std::env::set_var("DISABLE_MANGOHUD", "1");
|
||||||
|
|
||||||
match lock::try_toggle()? {
|
match lock::try_toggle()? {
|
||||||
ToggleResult::SignaledExisting => return Ok(()),
|
ToggleResult::SignaledExisting => return Ok(()),
|
||||||
ToggleResult::RunNew => {}
|
ToggleResult::RunNew => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let color = parse_color_arg();
|
||||||
|
|
||||||
let _lock = lock::create_lock()?;
|
let _lock = lock::create_lock()?;
|
||||||
|
|
||||||
let mut wl = WaylandBackend::new()?;
|
let mut wl = WaylandBackend::new()?;
|
||||||
let (w, h) = wl.screen_size();
|
let (w, h) = wl.screen_size();
|
||||||
let mut pixmap = Pixmap::new(w, h).context("create pixmap")?;
|
|
||||||
let mut argb_buf = vec![0u8; (w as usize) * (h as usize) * 4];
|
let mut renderer = GpuRenderer::new(
|
||||||
|
wl.raw_display_handle()?,
|
||||||
|
wl.raw_window_handle()?,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
color,
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut cursor_x = w as f64 / 2.0;
|
let mut cursor_x = w as f64 / 2.0;
|
||||||
let mut cursor_y = h as f64 / 2.0;
|
let mut cursor_y = h as f64 / 2.0;
|
||||||
|
|
||||||
draw(&mut wl, &mut pixmap, &mut argb_buf, cursor_x, cursor_y)?;
|
renderer.render(cursor_x, cursor_y)?;
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
let mut last_draw = Instant::now();
|
||||||
|
let mut dirty = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
if elapsed >= DISPLAY_DURATION || lock::should_exit() {
|
if elapsed >= DISPLAY_DURATION || lock::should_exit() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dirty && last_draw.elapsed() >= FRAME_INTERVAL {
|
||||||
|
renderer.render(cursor_x, cursor_y)?;
|
||||||
|
last_draw = Instant::now();
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
let remaining = DISPLAY_DURATION - elapsed;
|
let remaining = DISPLAY_DURATION - elapsed;
|
||||||
let event = match wl.next_event(remaining)? {
|
let poll_timeout = if dirty {
|
||||||
|
FRAME_INTERVAL
|
||||||
|
.saturating_sub(last_draw.elapsed())
|
||||||
|
.min(remaining)
|
||||||
|
} else {
|
||||||
|
remaining
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = match wl.next_event(poll_timeout)? {
|
||||||
Some(ev) => ev,
|
Some(ev) => ev,
|
||||||
None => continue, // poll timeout — loop back to check elapsed time
|
None => continue,
|
||||||
};
|
};
|
||||||
match event {
|
match event {
|
||||||
OverlayEvent::Close => break,
|
OverlayEvent::Close => break,
|
||||||
OverlayEvent::CursorMoved(x, y) => {
|
OverlayEvent::CursorMoved(x, y) => {
|
||||||
cursor_x = x;
|
cursor_x = x;
|
||||||
cursor_y = y;
|
cursor_y = y;
|
||||||
draw(&mut wl, &mut pixmap, &mut argb_buf, cursor_x, cursor_y)?;
|
dirty = true;
|
||||||
}
|
|
||||||
OverlayEvent::Configure(new_w, new_h) => {
|
|
||||||
if new_w != w || new_h != h {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
OverlayEvent::Configure(_, _) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
/// Parse color temperature from CLI args.
|
||||||
wl: &mut WaylandBackend,
|
///
|
||||||
pixmap: &mut Pixmap,
|
/// Usage:
|
||||||
argb_buf: &mut [u8],
|
/// ringa → pure white (default)
|
||||||
cursor_x: f64,
|
/// ringa warm → 3200K (warm tungsten)
|
||||||
cursor_y: f64,
|
/// ringa cool → 6500K (cool daylight)
|
||||||
) -> Result<()> {
|
/// ringa studio → 5000K (neutral studio)
|
||||||
render::render_ring_light(pixmap, cursor_x, cursor_y);
|
/// ringa daylight → 5600K (daylight balanced)
|
||||||
render::pixmap_to_argb8888(pixmap, argb_buf);
|
/// ringa golden → 2700K (golden hour)
|
||||||
let (w, h) = wl.screen_size();
|
/// ringa 4200 → custom Kelvin value
|
||||||
wl.present(argb_buf, w, h)
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
let kelvin = match arg.to_lowercase().as_str() {
|
||||||
|
"warm" => 3200,
|
||||||
|
"golden" => 2700,
|
||||||
|
"studio" => 5000,
|
||||||
|
"daylight" => 5600,
|
||||||
|
"cool" => 6500,
|
||||||
|
"white" => return [1.0, 1.0, 1.0],
|
||||||
|
other => match other.parse::<u32>() {
|
||||||
|
Ok(k) if (1000..=40000).contains(&k) => k,
|
||||||
|
_ => {
|
||||||
|
eprintln!(
|
||||||
|
"usage: ringa [white|warm|golden|studio|daylight|cool|<kelvin>]"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render::kelvin_to_rgb(kelvin)
|
||||||
}
|
}
|
||||||
+266
-162
@@ -1,183 +1,287 @@
|
|||||||
//! Ring light glow rendering.
|
//! GPU renderer using wgpu + WGSL fragment shader.
|
||||||
//!
|
|
||||||
//! The ring is a thick stroke centered on a single rounded rectangle SDF.
|
|
||||||
//! This guarantees uniform width everywhere, including at corners.
|
|
||||||
//! Glow bleeds outward and inward from the stroke edges.
|
|
||||||
|
|
||||||
use tiny_skia::Pixmap;
|
use anyhow::{Context, Result};
|
||||||
|
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
// The rounded rect centerline is inset this far from the screen edge.
|
#[repr(C)]
|
||||||
// The visible ring extends RING_HALF_WIDTH on either side of this line.
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
const CENTERLINE_INSET: f32 = 80.0;
|
struct Uniforms {
|
||||||
// Half the stroke width. Total ring thickness = 2 * RING_HALF_WIDTH.
|
screen_size: [f32; 2],
|
||||||
const RING_HALF_WIDTH: f32 = 30.0;
|
cursor_pos: [f32; 2],
|
||||||
// Corner radius of the centerline rounded rect.
|
outer_inset: f32,
|
||||||
const CORNER_RADIUS: f32 = 30.0;
|
outer_corner_radius: f32,
|
||||||
|
ring_width: f32,
|
||||||
|
inner_corner_radius: f32,
|
||||||
|
glow_spread: f32,
|
||||||
|
cursor_avoid_radius: f32,
|
||||||
|
cursor_avoid_falloff: f32,
|
||||||
|
_pad: f32,
|
||||||
|
color: [f32; 3],
|
||||||
|
_pad2: f32,
|
||||||
|
}
|
||||||
|
|
||||||
// Soft glow spread outward and inward from the ring edges.
|
pub struct GpuRenderer {
|
||||||
const GLOW_SPREAD: f32 = 50.0;
|
surface: wgpu::Surface<'static>,
|
||||||
const RING_ALPHA: f32 = 1.00;
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
uniforms: Uniforms,
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor avoidance.
|
impl GpuRenderer {
|
||||||
const CURSOR_AVOID_RADIUS: f32 = 120.0;
|
pub fn new(
|
||||||
const CURSOR_AVOID_FALLOFF: f32 = 60.0;
|
display_handle: RawDisplayHandle,
|
||||||
|
window_handle: RawWindowHandle,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
color: [f32; 3],
|
||||||
|
) -> Result<Self> {
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::VULKAN,
|
||||||
|
flags: wgpu::InstanceFlags::empty(),
|
||||||
|
backend_options: Default::default(),
|
||||||
|
memory_budget_thresholds: Default::default(),
|
||||||
|
display: None,
|
||||||
|
});
|
||||||
|
|
||||||
/// Render the ring light glow into `pixmap`, avoiding the cursor at `(cx, cy)`.
|
let surface = unsafe {
|
||||||
pub fn render_ring_light(pixmap: &mut Pixmap, cx: f64, cy: f64) {
|
instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
||||||
let w = pixmap.width() as f32;
|
raw_display_handle: Some(display_handle),
|
||||||
let h = pixmap.height() as f32;
|
raw_window_handle: window_handle,
|
||||||
let cx = cx as f32;
|
})
|
||||||
let cy = cy as f32;
|
|
||||||
let wi = pixmap.width() as usize;
|
|
||||||
let hi = pixmap.height() as usize;
|
|
||||||
|
|
||||||
let data = pixmap.data_mut();
|
|
||||||
data.fill(0);
|
|
||||||
|
|
||||||
let screen_cx = w / 2.0;
|
|
||||||
let screen_cy = h / 2.0;
|
|
||||||
|
|
||||||
// Single rounded rect centerline.
|
|
||||||
let hw = w / 2.0 - CENTERLINE_INSET;
|
|
||||||
let hh = h / 2.0 - CENTERLINE_INSET;
|
|
||||||
|
|
||||||
// The affected band: from (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD)
|
|
||||||
// to (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD) from each screen edge.
|
|
||||||
let band_start = (CENTERLINE_INSET - RING_HALF_WIDTH - GLOW_SPREAD)
|
|
||||||
.max(0.0)
|
|
||||||
.floor() as usize;
|
|
||||||
let band_end = (CENTERLINE_INSET + RING_HALF_WIDTH + GLOW_SPREAD).ceil() as usize + 1;
|
|
||||||
|
|
||||||
// Top band
|
|
||||||
for py in band_start..band_end.min(hi) {
|
|
||||||
for px in 0..wi {
|
|
||||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
|
||||||
write_pixel(data, py * wi + px, a);
|
|
||||||
}
|
}
|
||||||
|
.context("create wgpu surface")?;
|
||||||
|
|
||||||
|
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
power_preference: wgpu::PowerPreference::LowPower,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.context("no compatible GPU adapter")?;
|
||||||
|
|
||||||
|
let (device, queue) = pollster::block_on(adapter.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
label: Some("ringa"),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.context("request GPU device")?;
|
||||||
|
|
||||||
|
let caps = surface.get_capabilities(&adapter);
|
||||||
|
let format = caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.is_srgb())
|
||||||
|
.copied()
|
||||||
|
.or_else(|| caps.formats.first().copied())
|
||||||
|
.context("no supported surface format")?;
|
||||||
|
|
||||||
|
let alpha_mode = if caps
|
||||||
|
.alpha_modes
|
||||||
|
.contains(&wgpu::CompositeAlphaMode::PreMultiplied)
|
||||||
|
{
|
||||||
|
wgpu::CompositeAlphaMode::PreMultiplied
|
||||||
|
} else {
|
||||||
|
caps.alpha_modes
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
.context("no supported alpha mode")?
|
||||||
|
};
|
||||||
|
|
||||||
|
surface.configure(
|
||||||
|
&device,
|
||||||
|
&wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
present_mode: wgpu::PresentMode::Mailbox,
|
||||||
|
desired_maximum_frame_latency: 1,
|
||||||
|
alpha_mode,
|
||||||
|
view_formats: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("ring shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
screen_size: [width as f32, height as f32],
|
||||||
|
cursor_pos: [width as f32 / 2.0, height as f32 / 2.0],
|
||||||
|
outer_inset: 65.0,
|
||||||
|
outer_corner_radius: 36.0,
|
||||||
|
ring_width: 50.0,
|
||||||
|
inner_corner_radius: 100.0,
|
||||||
|
glow_spread: 50.0,
|
||||||
|
cursor_avoid_radius: 300.0,
|
||||||
|
cursor_avoid_falloff: 1500.0,
|
||||||
|
_pad: 0.0,
|
||||||
|
color,
|
||||||
|
_pad2: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("uniforms"),
|
||||||
|
contents: bytemuck::bytes_of(&uniforms),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("uniforms layout"),
|
||||||
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("uniforms bind group"),
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("ring pipeline layout"),
|
||||||
|
bind_group_layouts: &[Some(&bind_group_layout)],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("ring pipeline"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: Default::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
pipeline,
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group,
|
||||||
|
uniforms,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom band
|
pub fn render(&mut self, cx: f64, cy: f64) -> Result<()> {
|
||||||
let bottom_start = hi.saturating_sub(band_end);
|
self.uniforms.cursor_pos = [cx as f32, cy as f32];
|
||||||
let bottom_end = hi.saturating_sub(band_start);
|
self.queue
|
||||||
for py in bottom_start..bottom_end.min(hi) {
|
.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&self.uniforms));
|
||||||
if py < band_end {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for px in 0..wi {
|
|
||||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
|
||||||
write_pixel(data, py * wi + px, a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left band (between top and bottom)
|
let frame = match self.surface.get_current_texture() {
|
||||||
let vert_start = band_end.min(hi);
|
wgpu::CurrentSurfaceTexture::Success(tex)
|
||||||
let vert_end = hi.saturating_sub(band_end);
|
| wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
|
||||||
for py in vert_start..vert_end {
|
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
|
||||||
for px in band_start..band_end.min(wi) {
|
return Ok(())
|
||||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
|
||||||
write_pixel(data, py * wi + px, a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right band (between top and bottom)
|
|
||||||
let right_start = wi.saturating_sub(band_end);
|
|
||||||
let right_end = wi.saturating_sub(band_start);
|
|
||||||
for py in vert_start..vert_end {
|
|
||||||
for px in right_start..right_end.min(wi) {
|
|
||||||
if px < band_end {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
let a = pixel_alpha(px as f32, py as f32, screen_cx, screen_cy, hw, hh, cx, cy);
|
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
|
||||||
write_pixel(data, py * wi + px, a);
|
anyhow::bail!("surface lost")
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("unexpected surface texture state"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = frame
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("ring encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("ring pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
pass.draw(0..3, 0..1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
frame.present();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a tiny-skia `Pixmap` (premultiplied RGBA) to Wayland's ARGB8888
|
/// Convert color temperature in Kelvin to linear RGB.
|
||||||
/// format (premultiplied BGRA in memory on little-endian).
|
/// Based on Tanner Helland's algorithm.
|
||||||
pub fn pixmap_to_argb8888(pixmap: &Pixmap, out: &mut [u8]) {
|
pub fn kelvin_to_rgb(kelvin: u32) -> [f32; 3] {
|
||||||
let src = pixmap.data();
|
let temp = (kelvin as f64 / 100.0).clamp(10.0, 400.0);
|
||||||
assert_eq!(out.len(), src.len());
|
|
||||||
for (dst, src) in out.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
|
|
||||||
dst[0] = src[2]; // B
|
|
||||||
dst[1] = src[1]; // G
|
|
||||||
dst[2] = src[0]; // R
|
|
||||||
dst[3] = src[3]; // A
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Private helpers --------------------------------------------------------
|
let r = if temp <= 66.0 {
|
||||||
|
1.0
|
||||||
/// Compute glow alpha for a single pixel using a single rounded rect SDF.
|
|
||||||
///
|
|
||||||
/// The ring is all pixels where |sdf| <= RING_HALF_WIDTH (a thick stroke).
|
|
||||||
/// Glow fades from the ring edges outward/inward over GLOW_SPREAD pixels.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn pixel_alpha(
|
|
||||||
px: f32,
|
|
||||||
py: f32,
|
|
||||||
scx: f32,
|
|
||||||
scy: f32,
|
|
||||||
hw: f32,
|
|
||||||
hh: f32,
|
|
||||||
cursor_x: f32,
|
|
||||||
cursor_y: f32,
|
|
||||||
) -> u8 {
|
|
||||||
// Signed distance to the rounded rect centerline.
|
|
||||||
let sd = rounded_rect_sdf(px, py, scx, scy, hw, hh, CORNER_RADIUS);
|
|
||||||
let abs_sd = sd.abs();
|
|
||||||
|
|
||||||
let glow = if abs_sd <= RING_HALF_WIDTH {
|
|
||||||
// Inside the ring stroke — full brightness.
|
|
||||||
RING_ALPHA
|
|
||||||
} else {
|
} else {
|
||||||
// Distance from the nearest ring edge.
|
let r = 329.698727446 * (temp - 60.0).powf(-0.1332047592);
|
||||||
let dist = abs_sd - RING_HALF_WIDTH;
|
(r / 255.0).clamp(0.0, 1.0)
|
||||||
if dist >= GLOW_SPREAD {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let t = dist / GLOW_SPREAD;
|
|
||||||
smoothstep(1.0 - t) * RING_ALPHA
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cursor avoidance.
|
let g = if temp <= 66.0 {
|
||||||
let dx = px - cursor_x;
|
let g = 99.4708025861 * temp.ln() - 161.1195681661;
|
||||||
let dy = py - cursor_y;
|
(g / 255.0).clamp(0.0, 1.0)
|
||||||
let cursor_dist = (dx * dx + dy * dy).sqrt();
|
} else {
|
||||||
let cursor_factor =
|
let g = 288.1221695283 * (temp - 60.0).powf(-0.0755148492);
|
||||||
smoothstep(((cursor_dist - CURSOR_AVOID_RADIUS) / CURSOR_AVOID_FALLOFF).clamp(0.0, 1.0));
|
(g / 255.0).clamp(0.0, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
let alpha = glow * cursor_factor;
|
let b = if temp >= 66.0 {
|
||||||
(alpha * 255.0) as u8
|
1.0
|
||||||
}
|
} else if temp <= 19.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
let b = 138.5177312231 * (temp - 10.0).ln() - 305.0447927307;
|
||||||
|
(b / 255.0).clamp(0.0, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
/// Signed distance field for a rounded rectangle.
|
[r as f32, g as f32, b as f32]
|
||||||
///
|
|
||||||
/// Returns negative inside, positive outside, zero on the boundary.
|
|
||||||
fn rounded_rect_sdf(px: f32, py: f32, cx: f32, cy: f32, hw: f32, hh: f32, r: f32) -> f32 {
|
|
||||||
let dx = (px - cx).abs() - (hw - r);
|
|
||||||
let dy = (py - cy).abs() - (hh - r);
|
|
||||||
let outside = dx.max(0.0);
|
|
||||||
let outside_y = dy.max(0.0);
|
|
||||||
let outside_len = (outside * outside + outside_y * outside_y).sqrt();
|
|
||||||
let inside = dx.max(dy).min(0.0);
|
|
||||||
outside_len + inside - r
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a premultiplied white pixel with the given alpha.
|
|
||||||
fn write_pixel(data: &mut [u8], idx: usize, alpha: u8) {
|
|
||||||
if alpha == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let offset = idx * 4;
|
|
||||||
data[offset] = alpha;
|
|
||||||
data[offset + 1] = alpha;
|
|
||||||
data[offset + 2] = alpha;
|
|
||||||
data[offset + 3] = alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hermite smoothstep: 3t^2 - 2t^3
|
|
||||||
fn smoothstep(t: f32) -> f32 {
|
|
||||||
let t = t.clamp(0.0, 1.0);
|
|
||||||
t * t * (3.0 - 2.0 * t)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
struct Uniforms {
|
||||||
|
screen_size: vec2f,
|
||||||
|
cursor_pos: vec2f,
|
||||||
|
// Outer rounded rect: inset from screen edge
|
||||||
|
outer_inset: f32,
|
||||||
|
outer_corner_radius: f32,
|
||||||
|
// Inner rounded rect: inset further by ring_width
|
||||||
|
ring_width: f32,
|
||||||
|
inner_corner_radius: f32,
|
||||||
|
// Glow
|
||||||
|
glow_spread: f32,
|
||||||
|
// Cursor avoidance
|
||||||
|
cursor_avoid_radius: f32,
|
||||||
|
cursor_avoid_falloff: f32,
|
||||||
|
// Color temperature (linear RGB)
|
||||||
|
_pad: f32,
|
||||||
|
color: vec3f,
|
||||||
|
_pad2: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
||||||
|
let x = f32(i32(i) / 2) * 4.0 - 1.0;
|
||||||
|
let y = f32(i32(i) % 2) * 4.0 - 1.0;
|
||||||
|
return vec4f(x, y, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounded rectangle SDF (Inigo Quilez).
|
||||||
|
fn rounded_rect_sdf(p: vec2f, h: vec2f, r: f32) -> f32 {
|
||||||
|
let q = abs(p) - h + vec2f(r);
|
||||||
|
return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
||||||
|
let center = u.screen_size / 2.0;
|
||||||
|
let p = pos.xy - center;
|
||||||
|
|
||||||
|
// Outer rounded rect
|
||||||
|
let outer_half = center - vec2f(u.outer_inset);
|
||||||
|
let sd_outer = rounded_rect_sdf(p, outer_half, u.outer_corner_radius);
|
||||||
|
|
||||||
|
// Inner rounded rect
|
||||||
|
let inner_half = outer_half - vec2f(u.ring_width);
|
||||||
|
let sd_inner = rounded_rect_sdf(p, inner_half, u.inner_corner_radius);
|
||||||
|
|
||||||
|
// Ring: inside outer AND outside inner = solid at full brightness
|
||||||
|
var alpha: f32;
|
||||||
|
if sd_outer <= 0.0 && sd_inner >= 0.0 {
|
||||||
|
alpha = 1.0;
|
||||||
|
} else if sd_outer > 0.0 {
|
||||||
|
// Gaussian glow fading outward
|
||||||
|
let t = sd_outer / u.glow_spread;
|
||||||
|
alpha = exp(-3.0 * t * t);
|
||||||
|
} else {
|
||||||
|
// Gaussian glow fading inward
|
||||||
|
let t = -sd_inner / u.glow_spread;
|
||||||
|
alpha = exp(-3.0 * t * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor avoidance
|
||||||
|
let cursor_dist = distance(pos.xy, u.cursor_pos);
|
||||||
|
let avoid_start = u.cursor_avoid_radius;
|
||||||
|
let avoid_end = u.cursor_avoid_radius + u.cursor_avoid_falloff;
|
||||||
|
if cursor_dist < avoid_end {
|
||||||
|
let t_avoid = clamp((cursor_dist - avoid_start) / (avoid_end - avoid_start), 0.0, 1.0);
|
||||||
|
alpha *= t_avoid * t_avoid * (3.0 - 2.0 * t_avoid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Premultiplied color output
|
||||||
|
let rgb = u.color * alpha;
|
||||||
|
return vec4f(rgb, alpha);
|
||||||
|
}
|
||||||
+88
-73
@@ -1,12 +1,10 @@
|
|||||||
//! Wayland backend: layer-shell overlay with keyboard and pointer tracking.
|
//! Wayland backend: layer-shell overlay with keyboard and pointer tracking.
|
||||||
//!
|
//!
|
||||||
//! Creates a full-screen layer-shell surface for the ring light overlay.
|
//! Creates a full-screen layer-shell surface. Exposes raw Wayland handles
|
||||||
//! Tracks pointer position for cursor avoidance and captures keyboard
|
//! for wgpu surface creation. No CPU pixel buffers — GPU renders directly.
|
||||||
//! for Escape to exit.
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::Write;
|
use std::ptr::NonNull;
|
||||||
use std::os::fd::AsFd;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
@@ -15,32 +13,28 @@ use wayland_client::{
|
|||||||
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat,
|
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat,
|
||||||
wl_shm, wl_shm_pool, wl_surface,
|
wl_shm, wl_shm_pool, wl_surface,
|
||||||
},
|
},
|
||||||
Connection, Dispatch, EventQueue, QueueHandle, WEnum,
|
Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum,
|
||||||
};
|
};
|
||||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use raw_window_handle::{
|
||||||
|
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
|
||||||
|
};
|
||||||
|
|
||||||
// Linux evdev key code for Escape.
|
|
||||||
const KEY_ESCAPE: u32 = 1;
|
const KEY_ESCAPE: u32 = 1;
|
||||||
|
|
||||||
/// Events emitted by the Wayland backend to the main loop.
|
#[allow(dead_code)]
|
||||||
pub enum OverlayEvent {
|
pub enum OverlayEvent {
|
||||||
/// Escape pressed or surface closed by compositor.
|
|
||||||
Close,
|
Close,
|
||||||
/// Pointer moved to (x, y) in surface-local coordinates.
|
|
||||||
CursorMoved(f64, f64),
|
CursorMoved(f64, f64),
|
||||||
/// Surface (re-)configured with new dimensions.
|
|
||||||
Configure(u32, u32),
|
Configure(u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// WaylandBackend — public API.
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
pub struct WaylandBackend {
|
pub struct WaylandBackend {
|
||||||
state: WaylandState,
|
state: WaylandState,
|
||||||
event_queue: EventQueue<WaylandState>,
|
event_queue: EventQueue<WaylandState>,
|
||||||
|
#[allow(dead_code)]
|
||||||
queue_handle: QueueHandle<WaylandState>,
|
queue_handle: QueueHandle<WaylandState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +67,6 @@ impl WaylandBackend {
|
|||||||
if state.compositor.is_none() {
|
if state.compositor.is_none() {
|
||||||
anyhow::bail!("compositor does not advertise wl_compositor");
|
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() {
|
if state.layer_shell.is_none() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"compositor does not support wlr-layer-shell-unstable-v1. \
|
"compositor does not support wlr-layer-shell-unstable-v1. \
|
||||||
@@ -105,10 +96,23 @@ impl WaylandBackend {
|
|||||||
(self.state.screen_w, self.state.screen_h)
|
(self.state.screen_w, self.state.screen_h)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait up to `timeout` for the next event. Returns `None` on timeout
|
/// Raw Wayland display handle for wgpu surface creation.
|
||||||
/// or if the surface has been torn down.
|
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;
|
||||||
|
let nn = NonNull::new(ptr).context("wl_surface pointer is null")?;
|
||||||
|
Ok(RawWindowHandle::Wayland(WaylandWindowHandle::new(nn)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait up to `timeout` for the next event. Returns `None` on timeout.
|
||||||
pub fn next_event(&mut self, timeout: Duration) -> Result<Option<OverlayEvent>> {
|
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() {
|
if let Some(ev) = self.state.pending_events.pop_front() {
|
||||||
return Ok(Some(ev));
|
return Ok(Some(ev));
|
||||||
}
|
}
|
||||||
@@ -116,62 +120,71 @@ impl WaylandBackend {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush outgoing requests, then poll the fd with a timeout.
|
|
||||||
self.event_queue
|
self.event_queue
|
||||||
.flush()
|
.flush()
|
||||||
.context("flush before poll")?;
|
.context("flush before poll")?;
|
||||||
|
|
||||||
let read_guard = self.event_queue.prepare_read().unwrap();
|
// Try to dispatch any already-buffered events first.
|
||||||
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
|
self.event_queue
|
||||||
.dispatch_pending(&mut self.state)
|
.dispatch_pending(&mut self.state)
|
||||||
.context("dispatch_pending")?;
|
.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();
|
||||||
|
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")?;
|
||||||
|
|
||||||
Ok(self.state.pending_events.pop_front())
|
if n > 0 {
|
||||||
}
|
let _ = read_guard.read();
|
||||||
|
} else {
|
||||||
|
drop(read_guard);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy an ARGB8888 pixel buffer to the overlay surface via wl_shm.
|
self.event_queue
|
||||||
pub fn present(&mut self, pixels: &[u8], width: u32, height: u32) -> Result<()> {
|
.dispatch_pending(&mut self.state)
|
||||||
let shm = self.state.shm.as_ref().context("wl_shm not available")?;
|
.context("dispatch_pending (post-read)")?;
|
||||||
let surface = self
|
}
|
||||||
.state
|
None => {
|
||||||
.surface
|
// Another queue has events buffered — dispatch ours.
|
||||||
.as_ref()
|
self.event_queue
|
||||||
.context("wl_surface not available")?;
|
.dispatch_pending(&mut self.state)
|
||||||
|
.context("dispatch_pending (no-read)")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let stride = width * 4;
|
// Coalesce CursorMoved events.
|
||||||
let mut file = tempfile::tempfile().context("create shm tempfile")?;
|
let mut last_cursor: Option<(f64, f64)> = None;
|
||||||
file.write_all(pixels).context("write pixel buffer")?;
|
let mut first_non_cursor: Option<OverlayEvent> = None;
|
||||||
|
while let Some(ev) = self.state.pending_events.pop_front() {
|
||||||
let pool = shm.create_pool(file.as_fd(), pixels.len() as i32, &self.queue_handle, ());
|
match ev {
|
||||||
let buffer = pool.create_buffer(
|
OverlayEvent::CursorMoved(x, y) => last_cursor = Some((x, y)),
|
||||||
0,
|
other => {
|
||||||
width as i32,
|
first_non_cursor = Some(other);
|
||||||
height as i32,
|
break;
|
||||||
stride as i32,
|
}
|
||||||
wl_shm::Format::Argb8888,
|
}
|
||||||
&self.queue_handle,
|
}
|
||||||
(),
|
if let Some(ev) = first_non_cursor {
|
||||||
);
|
if let Some((x, y)) = last_cursor {
|
||||||
|
self.state
|
||||||
surface.attach(Some(&buffer), 0, 0);
|
.pending_events
|
||||||
surface.damage_buffer(0, 0, width as i32, height as i32);
|
.push_front(OverlayEvent::CursorMoved(x, y));
|
||||||
surface.commit();
|
}
|
||||||
pool.destroy();
|
return Ok(Some(ev));
|
||||||
Ok(())
|
}
|
||||||
|
if let Some((x, y)) = last_cursor {
|
||||||
|
return Ok(Some(OverlayEvent::CursorMoved(x, y)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +208,7 @@ struct WaylandState {
|
|||||||
conn: Connection,
|
conn: Connection,
|
||||||
|
|
||||||
compositor: Option<wl_compositor::WlCompositor>,
|
compositor: Option<wl_compositor::WlCompositor>,
|
||||||
|
#[allow(dead_code)]
|
||||||
shm: Option<wl_shm::WlShm>,
|
shm: Option<wl_shm::WlShm>,
|
||||||
seat: Option<wl_seat::WlSeat>,
|
seat: Option<wl_seat::WlSeat>,
|
||||||
layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||||
@@ -221,6 +235,11 @@ impl WaylandState {
|
|||||||
.context("zwlr_layer_shell_v1 not available")?;
|
.context("zwlr_layer_shell_v1 not available")?;
|
||||||
|
|
||||||
let surface = compositor.create_surface(qh, ());
|
let surface = compositor.create_surface(qh, ());
|
||||||
|
|
||||||
|
let input_region = compositor.create_region(qh, ());
|
||||||
|
input_region.add(0, 0, i32::MAX, i32::MAX);
|
||||||
|
surface.set_input_region(Some(&input_region));
|
||||||
|
|
||||||
let layer_surface = layer_shell.get_layer_surface(
|
let layer_surface = layer_shell.get_layer_surface(
|
||||||
&surface,
|
&surface,
|
||||||
None,
|
None,
|
||||||
@@ -347,12 +366,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandState {
|
|||||||
surface_x,
|
surface_x,
|
||||||
surface_y,
|
surface_y,
|
||||||
..
|
..
|
||||||
} => {
|
|
||||||
state
|
|
||||||
.pending_events
|
|
||||||
.push_back(OverlayEvent::CursorMoved(surface_x, surface_y));
|
|
||||||
}
|
}
|
||||||
wl_pointer::Event::Motion {
|
| wl_pointer::Event::Motion {
|
||||||
surface_x,
|
surface_x,
|
||||||
surface_y,
|
surface_y,
|
||||||
..
|
..
|
||||||
|
|||||||
Reference in new issue
Block a user