refactor: changed effects, add shell completions
Nightly Release / nightly-build (push) Has been cancelled
Code Quality / quality-checks (push) Has been cancelled
Security Scan / security-audit (push) Has been cancelled

- Extract all apply_* effect methods from screenshot.rs into dedicated
  src/effects.rs (blur, vignette, pixelate, swirl) — screenshot.rs
- Rewrite apply_blur: replace broken fastblur::gaussian_blur (horizontal-
  only box blur, backbuf dropped) with correct inline two-pass sliding-
  window box blur operating directly on Cairo surface bytes — ~10×
  faster, no ImageBuffer round-trip, no fastblur dependency
- Remove melting effect entirely (seems to be hard to implement
  properely, tried multiple itterations)
- Drop unused dependencies: fastblur, rand
- Add clap_complete dependency + --completions {bash,fish,zsh} flag,
  with CompletionShell enum in config.rs
- Fix show_* flags defaulting to true: remove default_value_t=true from
  show_media, show_battery, show_network, show_bluetooth, show_album_art,
  show_caps_lock_text — all now opt-in (matching user expectation)
- Convert peek toggle to hold: toggle_peek() → set_peek_held(held: bool),
  Press calls set_peek_held(true), new Release handler iterates all
  surfaces and calls set_peek_held(false), matching Ctrl-hold behavior
- Fix cursor rendering: increase font size 11→14 for peeked chars,
  handle cursor_position == 0 (prevent negative t wrapping to far ring
  end)
- Fix peek hit-test: use shape-aware point_in_shape() instead of simple
  circle pill/square/diamond/hexagon now detect clicks correctly within
  their area
- Remove per-frame DEBUG logs: PEEK/DOT mode in lock.rs,
  set_password_display/peek_password in render/mod.rs
- Update README: embed rustlock-effects.webp demo, remove melting
  references, correct show_* defaults and options table
This commit is contained in:
2026-06-28 18:17:53 +02:00
parent 833706f5a5
commit 7603975062
24 changed files with 862 additions and 1038 deletions
+285
View File
@@ -0,0 +1,285 @@
//! Visual effects for lock screen backgrounds.
//!
//! Each effect takes a `&mut ImageSurface` and processes it in-place.
//! Effects are applied in order: blur → vignette → pixelate → swirl.
use anyhow::{Context, Result};
use cairo::ImageSurface;
/// Apply a swirl effect (radial rotation around the image centre).
pub fn apply_swirl(surface: &mut ImageSurface, angle: f32) -> Result<()> {
let width = surface.width();
let height = surface.height();
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let radius = center_x.min(center_y);
let stride = surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
surface
.with_data(|src| data.copy_from_slice(src))
.context("swirl: failed to read surface data")?;
let original = data.clone();
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let d = (dx * dx + dy * dy).sqrt();
if d < radius {
let percent = (radius - d) / radius;
let theta = percent * percent * angle;
let s = theta.sin();
let c = theta.cos();
let nx = (c * dx - s * dy + center_x) as i32;
let ny = (s * dx + c * dy + center_y) as i32;
if nx >= 0 && nx < width && ny >= 0 && ny < height {
let src_idx = (ny as usize * stride) + (nx as usize * 4);
let dst_idx = (y as usize * stride) + (x as usize * 4);
data[dst_idx..dst_idx + 4].copy_from_slice(&original[src_idx..src_idx + 4]);
}
}
}
}
let mut surface_data = surface
.data()
.context("swirl: failed to write surface data")?;
surface_data.copy_from_slice(&data);
Ok(())
}
/// Pixelate the surface.
pub fn apply_pixelate(surface: &mut ImageSurface, pixel_size: u32) -> Result<()> {
if pixel_size <= 1 {
return Ok(());
}
let width = surface.width();
let height = surface.height();
let stride = surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
surface
.with_data(|src| data.copy_from_slice(src))
.context("pixelate: failed to read surface data")?;
for y in (0..height).step_by(pixel_size as usize) {
for x in (0..width).step_by(pixel_size as usize) {
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
let mut count = 0u32;
// Average pixels in the block
for py in 0..pixel_size {
for px in 0..pixel_size {
let cur_x = x + px as i32;
let cur_y = y + py as i32;
if cur_x < width && cur_y < height {
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
r += data[index] as u32;
g += data[index + 1] as u32;
b += data[index + 2] as u32;
count += 1;
}
}
}
if count > 0 {
let r = r.checked_div(count).unwrap_or(0) as u8;
let g = g.checked_div(count).unwrap_or(0) as u8;
let b = b.checked_div(count).unwrap_or(0) as u8;
// Fill the block
for py in 0..pixel_size {
for px in 0..pixel_size {
let cur_x = x + px as i32;
let cur_y = y + py as i32;
if cur_x < width && cur_y < height {
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
}
}
}
}
}
}
let mut surface_data = surface
.data()
.context("pixelate: failed to write surface data")?;
surface_data.copy_from_slice(&data);
Ok(())
}
/// Apply a box blur effect (fast two-pass sliding-window implementation).
/// Uses multiply-shift to avoid slow integer division in the hot loop.
pub fn apply_blur(surface: &mut ImageSurface, radius: u32, times: u32) -> Result<()> {
if radius == 0 || times == 0 {
return Ok(());
}
let width = surface.width() as usize;
let height = surface.height() as usize;
let stride = surface.stride() as usize;
let r = radius as usize;
let mut data = vec![0u8; stride * height];
surface
.with_data(|src| data.copy_from_slice(src))
.context("blur: failed to read surface data")?;
let mut scratch = vec![0u8; stride * height];
// Precompute ceil(2^32 / c) for every possible window size c.
let max_count = (2 * r + 1).min(width.max(height));
let factor: Vec<u32> = (0..=max_count)
.map(|c| {
if c == 0 {
0
} else {
(1u64 << 32).div_ceil(c as u64) as u32
}
})
.collect();
#[inline(always)]
fn div_mul(n: u32, factor: u32) -> u8 {
((n as u64 * factor as u64) >> 32) as u8
}
for _ in 0..times {
// Horizontal box blur: data -> scratch
for y in 0..height {
let row = y * stride;
let init_end = r.min(width - 1);
let mut b_acc = 0u32;
let mut g_acc = 0u32;
let mut r_acc = 0u32;
for x in 0..=init_end {
let px = row + x * 4;
b_acc += data[px] as u32;
g_acc += data[px + 1] as u32;
r_acc += data[px + 2] as u32;
}
let mut count = (init_end + 1) as u32;
for x in 0..width {
let dst = row + x * 4;
let f = factor[count as usize];
scratch[dst] = div_mul(b_acc, f);
scratch[dst + 1] = div_mul(g_acc, f);
scratch[dst + 2] = div_mul(r_acc, f);
scratch[dst + 3] = data[dst + 3];
if x >= r {
let old = row + (x - r) * 4;
b_acc -= data[old] as u32;
g_acc -= data[old + 1] as u32;
r_acc -= data[old + 2] as u32;
count -= 1;
}
if x + r + 1 < width {
let new = row + (x + r + 1) * 4;
b_acc += data[new] as u32;
g_acc += data[new + 1] as u32;
r_acc += data[new + 2] as u32;
count += 1;
}
}
}
// Vertical box blur: scratch -> data
let init_end = r.min(height - 1);
let mut b_acc = vec![0u32; width];
let mut g_acc = vec![0u32; width];
let mut r_acc = vec![0u32; width];
for y in 0..=init_end {
let row = y * stride;
for x in 0..width {
let px = row + x * 4;
b_acc[x] += scratch[px] as u32;
g_acc[x] += scratch[px + 1] as u32;
r_acc[x] += scratch[px + 2] as u32;
}
}
let mut count = (init_end + 1) as u32;
for y in 0..height {
let f = factor[count as usize];
let dst_row = y * stride;
for x in 0..width {
let dst = dst_row + x * 4;
data[dst] = div_mul(b_acc[x], f);
data[dst + 1] = div_mul(g_acc[x], f);
data[dst + 2] = div_mul(r_acc[x], f);
}
if y >= r {
let old_row = (y - r) * stride;
for x in 0..width {
let px = old_row + x * 4;
b_acc[x] -= scratch[px] as u32;
g_acc[x] -= scratch[px + 1] as u32;
r_acc[x] -= scratch[px + 2] as u32;
}
count -= 1;
}
if y + r + 1 < height {
let new_row = (y + r + 1) * stride;
for x in 0..width {
let px = new_row + x * 4;
b_acc[x] += scratch[px] as u32;
g_acc[x] += scratch[px + 1] as u32;
r_acc[x] += scratch[px + 2] as u32;
}
count += 1;
}
}
}
let mut surface_data = surface.data()?;
surface_data.copy_from_slice(&data);
Ok(())
}
/// Apply a vignette effect (darken edges).
pub fn apply_vignette(surface: &mut ImageSurface, base: f32, factor: f32) -> Result<()> {
let width = surface.width();
let height = surface.height();
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let max_distance = (center_x * center_x + center_y * center_y).sqrt();
let stride = surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
surface
.with_data(|src| data.copy_from_slice(src))
.context("vignette: failed to read surface data")?;
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let distance = (dx * dx + dy * dy).sqrt();
let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor);
let index = (y as usize * stride) + (x as usize * 4);
for i in 0..3 {
let value = data[index + i] as f32 * vignette_factor;
data[index + i] = value.clamp(0.0, 255.0) as u8;
}
}
}
let mut surface_data = surface
.data()
.context("vignette: failed to write surface data")?;
surface_data.copy_from_slice(&data);
Ok(())
}