feat: add optional background blur, align to shared texture pattern
Gaussian blur applied at texture load time when `background_blur` is set in moonlock.toml. Refactored wallpaper loading from per-window Picture::for_filename() to shared gdk::Texture pattern (matching moonset/moongreet), avoiding redundant JPEG decoding on multi-monitor.
This commit is contained in:
+15
-3
@@ -21,6 +21,7 @@ fn default_config_paths() -> Vec<PathBuf> {
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct RawConfig {
|
||||
pub background_path: Option<String>,
|
||||
pub background_blur: Option<f32>,
|
||||
pub fingerprint_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ struct RawConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub background_path: Option<String>,
|
||||
pub background_blur: Option<f32>,
|
||||
pub fingerprint_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -35,6 +37,7 @@ impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
background_path: None,
|
||||
background_blur: None,
|
||||
fingerprint_enabled: true,
|
||||
}
|
||||
}
|
||||
@@ -48,6 +51,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
||||
if let Ok(content) = fs::read_to_string(path) {
|
||||
if let Ok(parsed) = toml::from_str::<RawConfig>(&content) {
|
||||
if parsed.background_path.is_some() { merged.background_path = parsed.background_path; }
|
||||
if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; }
|
||||
if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; }
|
||||
}
|
||||
}
|
||||
@@ -72,7 +76,7 @@ pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path)
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test] fn default_config() { let c = Config::default(); assert!(c.background_path.is_none()); assert!(c.fingerprint_enabled); }
|
||||
#[test] fn default_config() { let c = Config::default(); assert!(c.background_path.is_none()); assert!(c.background_blur.is_none()); assert!(c.fingerprint_enabled); }
|
||||
#[test] fn load_default_fingerprint_true() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moonlock.toml");
|
||||
@@ -83,15 +87,23 @@ mod tests {
|
||||
#[test] fn load_background() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moonlock.toml");
|
||||
fs::write(&conf, "background_path = \"/custom/bg.jpg\"\nfingerprint_enabled = false\n").unwrap();
|
||||
fs::write(&conf, "background_path = \"/custom/bg.jpg\"\nbackground_blur = 15.0\nfingerprint_enabled = false\n").unwrap();
|
||||
let c = load_config(Some(&[conf]));
|
||||
assert_eq!(c.background_path.as_deref(), Some("/custom/bg.jpg"));
|
||||
assert_eq!(c.background_blur, Some(15.0));
|
||||
assert!(!c.fingerprint_enabled);
|
||||
}
|
||||
#[test] fn load_blur_optional() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moonlock.toml");
|
||||
fs::write(&conf, "background_path = \"/bg.jpg\"\n").unwrap();
|
||||
let c = load_config(Some(&[conf]));
|
||||
assert!(c.background_blur.is_none());
|
||||
}
|
||||
#[test] fn resolve_config_path() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wp = dir.path().join("bg.jpg"); fs::write(&wp, "fake").unwrap();
|
||||
let c = Config { background_path: Some(wp.to_str().unwrap().to_string()), fingerprint_enabled: true };
|
||||
let c = Config { background_path: Some(wp.to_str().unwrap().to_string()), ..Config::default() };
|
||||
assert_eq!(resolve_background_path_with(&c, Path::new("/nonexistent")), wp);
|
||||
}
|
||||
#[test] fn empty_user_config_preserves_system_fingerprint() {
|
||||
|
||||
+48
-7
@@ -6,6 +6,7 @@ use gdk_pixbuf::Pixbuf;
|
||||
use glib::clone;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{self as gtk, gio};
|
||||
use image::imageops;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
@@ -41,7 +42,7 @@ struct LockscreenState {
|
||||
/// Create a lockscreen window for a single monitor.
|
||||
/// Fingerprint is not initialized here — use `wire_fingerprint()` after async init.
|
||||
pub fn create_lockscreen_window(
|
||||
bg_path: &Path,
|
||||
bg_texture: &gdk::Texture,
|
||||
_config: &Config,
|
||||
app: >k::Application,
|
||||
unlock_callback: Rc<dyn Fn()>,
|
||||
@@ -82,7 +83,7 @@ pub fn create_lockscreen_window(
|
||||
window.set_child(Some(&overlay));
|
||||
|
||||
// Background wallpaper
|
||||
let background = create_background_picture(bg_path);
|
||||
let background = create_background_picture(bg_texture);
|
||||
overlay.set_child(Some(&background));
|
||||
|
||||
// Centered vertical box
|
||||
@@ -413,13 +414,53 @@ pub fn start_fingerprint(
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a Picture widget for the wallpaper background.
|
||||
fn create_background_picture(bg_path: &Path) -> gtk::Picture {
|
||||
let background = if bg_path.starts_with("/dev/moonarch/moonlock") {
|
||||
gtk::Picture::for_resource(bg_path.to_str().unwrap_or(""))
|
||||
/// Load the wallpaper as a texture once, for sharing across all windows.
|
||||
/// When `blur_radius` is `Some(sigma)` with sigma > 0, a Gaussian blur is applied.
|
||||
pub fn load_background_texture(bg_path: &Path, blur_radius: Option<f32>) -> gdk::Texture {
|
||||
let fallback = "/dev/moonarch/moonlock/wallpaper.jpg";
|
||||
|
||||
let texture = if bg_path.starts_with("/dev/moonarch/moonlock") {
|
||||
let resource_path = bg_path.to_str().unwrap_or(fallback);
|
||||
gdk::Texture::from_resource(resource_path)
|
||||
} else {
|
||||
gtk::Picture::for_filename(bg_path.to_str().unwrap_or(""))
|
||||
let file = gio::File::for_path(bg_path);
|
||||
gdk::Texture::from_file(&file).unwrap_or_else(|_| {
|
||||
gdk::Texture::from_resource(fallback)
|
||||
})
|
||||
};
|
||||
|
||||
match blur_radius {
|
||||
Some(sigma) if sigma > 0.0 => apply_blur(&texture, sigma),
|
||||
_ => texture,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply Gaussian blur to a texture and return a blurred texture.
|
||||
fn apply_blur(texture: &gdk::Texture, sigma: f32) -> gdk::Texture {
|
||||
let width = texture.width() as u32;
|
||||
let height = texture.height() as u32;
|
||||
let stride = width as usize * 4;
|
||||
let mut pixel_data = vec![0u8; stride * height as usize];
|
||||
texture.download(&mut pixel_data, stride);
|
||||
|
||||
let img = image::RgbaImage::from_raw(width, height, pixel_data)
|
||||
.expect("pixel buffer size matches texture dimensions");
|
||||
let blurred = imageops::blur(&image::DynamicImage::ImageRgba8(img), sigma);
|
||||
|
||||
let bytes = glib::Bytes::from(blurred.as_raw());
|
||||
let mem_texture = gdk::MemoryTexture::new(
|
||||
width as i32,
|
||||
height as i32,
|
||||
gdk::MemoryFormat::B8g8r8a8Premultiplied,
|
||||
&bytes,
|
||||
stride,
|
||||
);
|
||||
mem_texture.upcast()
|
||||
}
|
||||
|
||||
/// Create a Picture widget for the wallpaper background from a shared texture.
|
||||
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
|
||||
let background = gtk::Picture::for_paintable(texture);
|
||||
background.set_content_fit(gtk::ContentFit::Cover);
|
||||
background.set_hexpand(true);
|
||||
background.set_vexpand(true);
|
||||
|
||||
+7
-7
@@ -14,7 +14,6 @@ use gtk4::prelude::*;
|
||||
use gtk4::{self as gtk, gio};
|
||||
use gtk4_session_lock;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::fingerprint::FingerprintListener;
|
||||
@@ -42,14 +41,15 @@ fn activate(app: >k::Application) {
|
||||
|
||||
let config = config::load_config(None);
|
||||
let bg_path = config::resolve_background_path(&config);
|
||||
let bg_texture = lockscreen::load_background_texture(&bg_path, config.background_blur);
|
||||
|
||||
if gtk4_session_lock::is_supported() {
|
||||
activate_with_session_lock(app, &display, &bg_path, &config);
|
||||
activate_with_session_lock(app, &display, &bg_texture, &config);
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
log::warn!("ext-session-lock-v1 not supported — running in development mode");
|
||||
activate_without_lock(app, &bg_path, &config);
|
||||
activate_without_lock(app, &bg_texture, &config);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
@@ -62,7 +62,7 @@ fn activate(app: >k::Application) {
|
||||
fn activate_with_session_lock(
|
||||
app: >k::Application,
|
||||
display: &gdk::Display,
|
||||
bg_path: &PathBuf,
|
||||
bg_texture: &gdk::Texture,
|
||||
config: &config::Config,
|
||||
) {
|
||||
let lock = gtk4_session_lock::Instance::new();
|
||||
@@ -87,7 +87,7 @@ fn activate_with_session_lock(
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
{
|
||||
let handles = lockscreen::create_lockscreen_window(
|
||||
bg_path,
|
||||
bg_texture,
|
||||
config,
|
||||
app,
|
||||
unlock_callback.clone(),
|
||||
@@ -144,7 +144,7 @@ fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
|
||||
#[cfg(debug_assertions)]
|
||||
fn activate_without_lock(
|
||||
app: >k::Application,
|
||||
bg_path: &PathBuf,
|
||||
bg_texture: &gdk::Texture,
|
||||
config: &config::Config,
|
||||
) {
|
||||
let app_clone = app.clone();
|
||||
@@ -153,7 +153,7 @@ fn activate_without_lock(
|
||||
});
|
||||
|
||||
let handles = lockscreen::create_lockscreen_window(
|
||||
bg_path,
|
||||
bg_texture,
|
||||
config,
|
||||
app,
|
||||
unlock_callback,
|
||||
|
||||
Reference in New Issue
Block a user