From 293bba32a65ae09c3c341a11c086c34d750794dc Mon Sep 17 00:00:00 2001 From: nevaforget Date: Sat, 28 Mar 2026 14:53:16 +0100 Subject: [PATCH] feat: add optional background blur via `image` crate Gaussian blur applied at texture load time when `background-blur` is set in the [appearance] section of moongreet.toml. Blur runs once, result is shared across monitors. --- Cargo.lock | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + DECISIONS.md | 7 +++ src/config.rs | 26 ++++++++-- src/greeter.rs | 40 +++++++++++++-- src/main.rs | 2 +- 6 files changed, 197 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40e607f..271abc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "anyhow" version = "1.0.102" @@ -20,6 +26,18 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cairo-rs" version = "0.22.0" @@ -59,6 +77,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -81,6 +108,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -91,6 +127,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -504,6 +550,21 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -567,6 +628,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "moongreet" version = "0.4.1" @@ -578,6 +649,7 @@ dependencies = [ "glib-build-tools", "gtk4", "gtk4-layer-shell", + "image", "log", "serde", "serde_json", @@ -586,6 +658,25 @@ dependencies = [ "toml 0.8.23", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -628,6 +719,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -656,6 +760,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + [[package]] name = "quote" version = "1.0.45" @@ -760,6 +870,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" @@ -1128,3 +1244,18 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 4abdcf3..fd58613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ gio = "0.22" toml = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" +image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } log = "0.4" systemd-journal-logger = "2.2" diff --git a/DECISIONS.md b/DECISIONS.md index 99c4ed4..4d051a5 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,12 @@ # Decisions +## 2026-03-28 – Optional background blur via `image` crate + +- **Who**: Selene, Dom +- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens +- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors. +- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns blurred `gdk::Texture`. Config option `background-blur: Option` in `[appearance]` TOML section. + ## 2026-03-28 – Audit fixes for shared wallpaper texture (v0.4.1) - **Who**: Selene, Dominik - **Why**: Quality, performance, and security audits flagged issues in `load_background_texture()`, debug logging, and greetd error handling diff --git a/src/config.rs b/src/config.rs index c529647..735000c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,8 @@ struct TomlConfig { #[derive(Debug, Clone, Default, Deserialize)] struct Appearance { background: Option, + #[serde(rename = "background-blur")] + background_blur: Option, #[serde(rename = "gtk-theme")] gtk_theme: Option, } @@ -30,6 +32,7 @@ struct Appearance { #[derive(Debug, Clone, Default)] pub struct Config { pub background_path: Option, + pub background_blur: Option, pub gtk_theme: Option, } @@ -56,6 +59,9 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { Some(parent.join(&bg).to_string_lossy().to_string()); } } + if appearance.background_blur.is_some() { + merged.background_blur = appearance.background_blur; + } if appearance.gtk_theme.is_some() { merged.gtk_theme = appearance.gtk_theme; } @@ -72,7 +78,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { } } - log::debug!("Config result: background={:?}, gtk_theme={:?}", merged.background_path, merged.gtk_theme); + log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme); merged } @@ -114,6 +120,7 @@ mod tests { fn default_config_has_none_fields() { let config = Config::default(); assert!(config.background_path.is_none()); + assert!(config.background_blur.is_none()); assert!(config.gtk_theme.is_none()); } @@ -131,7 +138,7 @@ mod tests { let conf = dir.path().join("moongreet.toml"); fs::write( &conf, - "[appearance]\nbackground = \"/custom/wallpaper.jpg\"\ngtk-theme = \"catppuccin\"\n", + "[appearance]\nbackground = \"/custom/wallpaper.jpg\"\nbackground-blur = 20.0\ngtk-theme = \"catppuccin\"\n", ) .unwrap(); let paths = vec![conf]; @@ -140,9 +147,20 @@ mod tests { config.background_path.as_deref(), Some("/custom/wallpaper.jpg") ); + assert_eq!(config.background_blur, Some(20.0)); assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin")); } + #[test] + fn load_config_blur_optional() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + fs::write(&conf, "[appearance]\nbackground = \"/bg.jpg\"\n").unwrap(); + let paths = vec![conf]; + let config = load_config(Some(&paths)); + assert!(config.background_blur.is_none()); + } + #[test] fn load_config_resolves_relative_background() { let dir = tempfile::tempdir().unwrap(); @@ -196,7 +214,7 @@ mod tests { fs::write(&wallpaper, "fake").unwrap(); let config = Config { background_path: Some(wallpaper.to_str().unwrap().to_string()), - gtk_theme: None, + ..Config::default() }; assert_eq!( resolve_background_path_with(&config, Path::new("/nonexistent")), @@ -208,7 +226,7 @@ mod tests { fn resolve_ignores_config_path_when_file_missing() { let config = Config { background_path: Some("/nonexistent/wallpaper.jpg".to_string()), - gtk_theme: None, + ..Config::default() }; let result = resolve_background_path_with(&config, Path::new("/nonexistent")); assert!(result.to_str().unwrap().contains("moongreet")); diff --git a/src/greeter.rs b/src/greeter.rs index 38c419c..90a7f09 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -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::collections::HashMap; use std::os::unix::net::UnixStream; @@ -96,9 +97,10 @@ fn is_valid_username(name: &str) -> bool { } /// Load the background image as a shared texture (decode once, reuse everywhere). -pub fn load_background_texture(bg_path: &Path) -> Option { +/// 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) -> Option { let path_str = bg_path.to_str()?; - if bg_path.starts_with("/dev/moonarch/moongreet") { + let texture = if bg_path.starts_with("/dev/moonarch/moongreet") { match gio::resources_lookup_data(path_str, gio::ResourceLookupFlags::NONE) { Ok(bytes) => match gdk::Texture::from_bytes(&bytes) { Ok(texture) => Some(texture), @@ -132,9 +134,37 @@ pub fn load_background_texture(bg_path: &Path) -> Option { None } } + }?; + + match blur_radius { + Some(sigma) if sigma > 0.0 => Some(apply_blur(&texture, sigma)), + _ => Some(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 wallpaper-only window for secondary monitors. pub fn create_wallpaper_window( texture: &gdk::Texture, @@ -1628,7 +1658,7 @@ mod tests { #[test] fn load_background_texture_missing_file_returns_none() { - let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg")); + let result = load_background_texture(Path::new("/nonexistent/wallpaper.jpg"), None); assert!(result.is_none()); } @@ -1639,7 +1669,7 @@ mod tests { // Create a sparse file that exceeds MAX_WALLPAPER_FILE_SIZE let f = std::fs::File::create(&path).unwrap(); f.set_len(MAX_WALLPAPER_FILE_SIZE + 1).unwrap(); - let result = load_background_texture(&path); + let result = load_background_texture(&path, None); assert!(result.is_none()); } @@ -1650,7 +1680,7 @@ mod tests { // 0xFF is not valid UTF-8 let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe, 0xfd]); let path = Path::new(non_utf8); - let result = load_background_texture(path); + let result = load_background_texture(path, None); assert!(result.is_none()); } } diff --git a/src/main.rs b/src/main.rs index f3a2717..aa074bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,7 @@ fn activate(app: >k::Application) { log::debug!("Background path: {}", bg_path.display()); // Load background texture once — shared across all windows - let bg_texture = greeter::load_background_texture(&bg_path); + let bg_texture = greeter::load_background_texture(&bg_path, config.background_blur); if bg_texture.is_none() { log::error!("Failed to load background texture — greeter will start without wallpaper"); }