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:
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+32
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user