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.
This commit is contained in:
+22
-4
@@ -22,6 +22,8 @@ struct TomlConfig {
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct Appearance {
|
||||
background: Option<String>,
|
||||
#[serde(rename = "background-blur")]
|
||||
background_blur: Option<f32>,
|
||||
#[serde(rename = "gtk-theme")]
|
||||
gtk_theme: Option<String>,
|
||||
}
|
||||
@@ -30,6 +32,7 @@ struct Appearance {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
pub background_path: Option<String>,
|
||||
pub background_blur: Option<f32>,
|
||||
pub gtk_theme: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
+35
-5
@@ -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<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>) -> Option<gdk::Texture> {
|
||||
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<gdk::Texture> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user