diff --git a/Cargo.lock b/Cargo.lock index 29afaccf..89b82e3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -6808,6 +6808,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "window-vibrancy", "windows 0.58.0", ] diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 1b56d76b..21920b9a 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -36,6 +36,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } netdev = "0.24" regex = "1" +window-vibrancy = "0.5" [target.'cfg(target_os = "windows")'.dependencies] komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" } diff --git a/packages/desktop/src/common/mod.rs b/packages/desktop/src/common/mod.rs index 328132b3..9730c135 100644 --- a/packages/desktop/src/common/mod.rs +++ b/packages/desktop/src/common/mod.rs @@ -1,11 +1,13 @@ mod format_bytes; mod fs_util; mod length_value; +mod parse_rgba; mod path_ext; mod window_ext; pub use format_bytes::*; pub use fs_util::*; pub use length_value::*; +pub use parse_rgba::*; pub use path_ext::*; pub use window_ext::*; diff --git a/packages/desktop/src/common/parse_rgba.rs b/packages/desktop/src/common/parse_rgba.rs new file mode 100644 index 00000000..a454030d --- /dev/null +++ b/packages/desktop/src/common/parse_rgba.rs @@ -0,0 +1,79 @@ +#[derive(Debug)] +pub enum ColorError { + InvalidFormat, + InvalidValue, +} + +pub fn parse_rgba( + color_str: &str, +) -> Result<(u8, u8, u8, u8), ColorError> { + let content = color_str + .trim_start_matches("rgba(") + .trim_end_matches(")") + .trim(); + + let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect(); + + if parts.len() != 4 { + return Err(ColorError::InvalidFormat); + } + + let r = parts[0].parse().map_err(|_| ColorError::InvalidValue)?; + let g = parts[1].parse().map_err(|_| ColorError::InvalidValue)?; + let b = parts[2].parse().map_err(|_| ColorError::InvalidValue)?; + + let a = if parts[3].ends_with('%') { + let percent = parts[3] + .trim_end_matches('%') + .parse::() + .map_err(|_| ColorError::InvalidValue)?; + if percent < 0.0 || percent > 100.0 { + return Err(ColorError::InvalidValue); + } + (percent / 100.0 * 255.0) as u8 + } else { + let alpha = parts[3] + .parse::() + .map_err(|_| ColorError::InvalidValue)?; + if alpha < 0.0 || alpha > 1.0 { + return Err(ColorError::InvalidValue); + } + (alpha * 255.0) as u8 + }; + + Ok((r, g, b, a)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_rgba() { + // Test valid inputs + assert_eq!( + parse_rgba("rgba(255, 0, 0, 1.0)").unwrap(), + (255, 0, 0, 255) + ); + assert_eq!( + parse_rgba("rgba(255, 0, 0, 0.5)").unwrap(), + (255, 0, 0, 127) + ); + assert_eq!( + parse_rgba("rgba(255, 0, 0, 100%)").unwrap(), + (255, 0, 0, 255) + ); + assert_eq!( + parse_rgba("rgba(255, 0, 0, 50%)").unwrap(), + (255, 0, 0, 127) + ); + + // Test invalid inputs + assert!(parse_rgba("rgb(255, 0, 0)").is_err()); + assert!(parse_rgba("rgba(300, 0, 0, 1.0)").is_err()); + assert!(parse_rgba("rgba(255, 0, 0, 1.5)").is_err()); + assert!(parse_rgba("rgba(255, 0, 0, 101%)").is_err()); + assert!(parse_rgba("rgba(255, 0, 0)").is_err()); + assert!(parse_rgba("rgba(255, 0, 0, invalid)").is_err()); + } +} diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 50bfecca..0748c99e 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -90,6 +90,9 @@ pub struct WidgetConfig { /// Whether the Tauri window frame should be transparent. pub transparent: bool, + /// Whether the window frame should have an effect + pub background_effect: Option, + /// Where to place the widget. Add alias for `defaultPlacements` for /// compatibility with v2.3.0 and earlier. #[serde(alias = "defaultPlacements")] @@ -161,6 +164,46 @@ pub enum MonitorSelection { Name(String), } +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct BackgroundEffect { + pub windows: Option, + pub mac_os: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum WindowsBackgroundEffect { + Blur { color: String }, + Acrylic { color: String }, + Mica { prefer_dark: bool }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MacOsBackgroundEffect { + Vibrancy { material: VibrancyMaterial }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum VibrancyMaterial { + Titlebar, + Selection, + Menu, + Popover, + Sidebar, + HeaderView, + Sheet, + WindowBackground, + HudWindow, + FullScreenUI, + Tooltip, + ContentBackground, + UnderWindowBackground, + UnderPageBackground, +} + #[derive(Debug)] pub struct Config { /// Handle to the Tauri application. diff --git a/packages/desktop/src/widget_factory.rs b/packages/desktop/src/widget_factory.rs index 9d78e295..826fb7c6 100644 --- a/packages/desktop/src/widget_factory.rs +++ b/packages/desktop/src/widget_factory.rs @@ -18,10 +18,14 @@ use tokio::{ task, }; use tracing::{error, info}; +use window_vibrancy::apply_vibrancy; use crate::{ - common::{PathExt, WindowExt}, - config::{AnchorPoint, Config, WidgetConfig, WidgetPlacement, ZOrder}, + common::{parse_rgba, PathExt, WindowExt}, + config::{ + AnchorPoint, Config, MacOsBackgroundEffect, VibrancyMaterial, + WidgetConfig, WidgetPlacement, WindowsBackgroundEffect, ZOrder, + }, monitor_state::MonitorState, }; @@ -254,6 +258,109 @@ impl WidgetFactory { widget_states.insert(state.id.clone(), state.clone()); } + #[cfg(target_os = "windows")] + { + use window_vibrancy::{apply_acrylic, apply_blur, apply_mica}; + + if let Some(window_effect) = &widget_config.background_effect { + if let Some(effect) = &window_effect.windows { + let result = match effect { + WindowsBackgroundEffect::Blur { color } + | WindowsBackgroundEffect::Acrylic { color } => { + let color = + parse_rgba(color).unwrap_or((255, 255, 255, 200)); + match effect { + WindowsBackgroundEffect::Blur { .. } => { + apply_blur(&window, Some(color)) + } + _ => apply_acrylic(&window, Some(color)), + } + } + WindowsBackgroundEffect::Mica { prefer_dark } => { + apply_mica(&window, Some(*prefer_dark)) + } + }; + + if let Err(e) = result { + error!("Failed to apply effect: {:?}", e); + } + } + } + } + + #[cfg(target_os = "macos")] + { + use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; + + if let Some(window_effect) = &widget_config.background_effect { + if let Some(effect) = &window_effect.mac_os { + let window = window.clone(); + let effect = effect.clone(); + + let result = + self.app_handle.run_on_main_thread(move || match effect { + MacOsBackgroundEffect::Vibrancy { material } => { + let ns_material = match material { + VibrancyMaterial::Titlebar => { + NSVisualEffectMaterial::Titlebar + } + VibrancyMaterial::Selection => { + NSVisualEffectMaterial::Selection + } + VibrancyMaterial::Menu => NSVisualEffectMaterial::Menu, + VibrancyMaterial::Popover => { + NSVisualEffectMaterial::Popover + } + VibrancyMaterial::Sidebar => { + NSVisualEffectMaterial::Sidebar + } + VibrancyMaterial::HeaderView => { + NSVisualEffectMaterial::HeaderView + } + VibrancyMaterial::Sheet => { + NSVisualEffectMaterial::Sheet + } + VibrancyMaterial::WindowBackground => { + NSVisualEffectMaterial::WindowBackground + } + VibrancyMaterial::HudWindow => { + NSVisualEffectMaterial::HudWindow + } + VibrancyMaterial::FullScreenUI => { + NSVisualEffectMaterial::FullScreenUI + } + VibrancyMaterial::Tooltip => { + NSVisualEffectMaterial::Tooltip + } + VibrancyMaterial::ContentBackground => { + NSVisualEffectMaterial::ContentBackground + } + VibrancyMaterial::UnderWindowBackground => { + NSVisualEffectMaterial::UnderWindowBackground + } + VibrancyMaterial::UnderPageBackground => { + NSVisualEffectMaterial::UnderPageBackground + } + }; + + if let Err(err) = + apply_vibrancy(&window, ns_material, None, None) + { + error!("Failed to apply vibrancy effect: {:?}", err); + } + } + }); + + if let Err(err) = result { + error!( + "Unable to change background effect on main thread: {:?}", + err + ); + } + } + } + } + self.register_window_events(&window, widget_id); self.open_tx.send(state)?; }