feat: add optional background blur via image crate
Gaussian blur applied at texture load time when `background_blur` is set in moonset.toml. Blur runs once, result is shared across monitors.
This commit is contained in:
parent
473bed479a
commit
529a1a54ae
133
Cargo.lock
generated
133
Cargo.lock
generated
@ -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 = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@ -79,6 +85,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"
|
||||
@ -130,6 +148,15 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@ -196,6 +223,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"
|
||||
@ -206,6 +242,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"
|
||||
@ -630,6 +676,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"
|
||||
@ -732,9 +793,19 @@ 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 = "moonset"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"env_logger",
|
||||
@ -744,6 +815,7 @@ dependencies = [
|
||||
"glib-build-tools",
|
||||
"gtk4",
|
||||
"gtk4-layer-shell",
|
||||
"image",
|
||||
"log",
|
||||
"nix",
|
||||
"serde",
|
||||
@ -751,6 +823,16 @@ 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 = "nix"
|
||||
version = "0.29.0"
|
||||
@ -763,6 +845,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -817,6 +908,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 = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@ -860,6 +964,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"
|
||||
@ -1004,6 +1114,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"
|
||||
@ -1394,3 +1510,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",
|
||||
]
|
||||
|
||||
@ -15,6 +15,7 @@ toml = "0.8"
|
||||
dirs = "6"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
nix = { version = "0.29", features = ["user"] }
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
|
||||
Architectural and design decisions for Moonset, in reverse chronological order.
|
||||
|
||||
## 2026-03-28 – Optional background blur via `image` crate
|
||||
|
||||
- **Who**: Hekate, Dom
|
||||
- **Why**: Blurred wallpaper as background is a common UX pattern for overlay menus
|
||||
- **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 `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML.
|
||||
|
||||
## 2026-03-28 – Use absolute paths for system binaries
|
||||
|
||||
- **Who**: Hekate, Dom
|
||||
|
||||
@ -20,6 +20,7 @@ fn default_config_paths() -> Vec<PathBuf> {
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
pub background_path: Option<String>,
|
||||
pub background_blur: Option<f32>,
|
||||
}
|
||||
|
||||
/// Load config from TOML files. Later paths override earlier ones.
|
||||
@ -34,6 +35,9 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
||||
if parsed.background_path.is_some() {
|
||||
merged.background_path = parsed.background_path;
|
||||
}
|
||||
if parsed.background_blur.is_some() {
|
||||
merged.background_blur = parsed.background_blur;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,6 +80,7 @@ mod tests {
|
||||
fn default_config_has_none_background() {
|
||||
let config = Config::default();
|
||||
assert!(config.background_path.is_none());
|
||||
assert!(config.background_blur.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -95,6 +100,28 @@ mod tests {
|
||||
assert_eq!(config.background_path.as_deref(), Some("/custom/wallpaper.jpg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_reads_background_blur() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moonset.toml");
|
||||
fs::write(&conf, "background_blur = 20.0\n").unwrap();
|
||||
let paths = vec![conf];
|
||||
let config = load_config(Some(&paths));
|
||||
assert_eq!(config.background_blur, Some(20.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_blur_override() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf1 = dir.path().join("first.toml");
|
||||
let conf2 = dir.path().join("second.toml");
|
||||
fs::write(&conf1, "background_blur = 10.0\n").unwrap();
|
||||
fs::write(&conf2, "background_blur = 25.0\n").unwrap();
|
||||
let paths = vec![conf1, conf2];
|
||||
let config = load_config(Some(&paths));
|
||||
assert_eq!(config.background_blur, Some(25.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_later_paths_override_earlier() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@ -124,6 +151,7 @@ mod tests {
|
||||
fs::write(&wallpaper, "fake").unwrap();
|
||||
let config = Config {
|
||||
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
||||
@ -135,6 +163,7 @@ mod tests {
|
||||
fn resolve_ignores_config_path_when_file_missing() {
|
||||
let config = Config {
|
||||
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
||||
// Falls through to gresource fallback
|
||||
|
||||
@ -56,7 +56,7 @@ fn activate(app: >k::Application) {
|
||||
// Resolve wallpaper once, decode texture once, share across all windows
|
||||
let config = config::load_config(None);
|
||||
let bg_path = config::resolve_background_path(&config);
|
||||
let texture = panel::load_background_texture(&bg_path);
|
||||
let texture = panel::load_background_texture(&bg_path, config.background_blur);
|
||||
|
||||
// Panel on focused output (no set_monitor → compositor picks focused)
|
||||
let panel = panel::create_panel_window(&texture, app);
|
||||
|
||||
34
src/panel.rs
34
src/panel.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::path::Path;
|
||||
use std::rc::Rc;
|
||||
@ -80,10 +81,11 @@ pub fn action_definitions() -> Vec<ActionDef> {
|
||||
}
|
||||
|
||||
/// Load the wallpaper as a texture once, for sharing across all windows.
|
||||
pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
|
||||
/// 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 = format!("{}/wallpaper.jpg", crate::GRESOURCE_PREFIX);
|
||||
|
||||
if bg_path.starts_with(crate::GRESOURCE_PREFIX) {
|
||||
let texture = if bg_path.starts_with(crate::GRESOURCE_PREFIX) {
|
||||
let resource_path = bg_path.to_str().unwrap_or(&fallback);
|
||||
gdk::Texture::from_resource(resource_path)
|
||||
} else {
|
||||
@ -91,9 +93,37 @@ pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
|
||||
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 wallpaper-only window for secondary monitors.
|
||||
pub fn create_wallpaper_window(texture: &gdk::Texture, app: >k::Application) -> gtk::ApplicationWindow {
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user