tauri-plugin-decor 1.0.1

Opinionated window decoration controls for Tauri apps.
Documentation
use serde::Deserialize;
use tauri::{Manager, Runtime, WebviewWindow};

use crate::error;
use crate::overlay;

#[derive(Clone, Default, Debug, Deserialize)]
#[serde(default, rename_all = "snake_case")]
pub struct DecorStyle {
    pub controls_height: Option<f64>,
    pub controls_inset_x: Option<f64>,
    pub controls_spacing: Option<f64>,
    pub controls_scale: Option<f64>,
    pub controls_button_width: Option<u32>,
    pub controls_close_hover_bg: Option<String>,
    pub controls_button_hover_bg: Option<String>,
}

pub struct Decor<R: Runtime> {
    app: tauri::AppHandle<R>,
}

impl<R: Runtime> Decor<R> {
    pub fn new(app: tauri::AppHandle<R>) -> Self {
        Self { app }
    }

    pub fn reconfigure(&self, style: DecorStyle) {
        if let Some(v) = style.controls_scale {
            self.set_controls_scale(v);
        }
        if let Some(v) = style.controls_height {
            self.set_controls_height(v);
        }
        if let Some(v) = style.controls_inset_x {
            self.set_controls_inset_x(v);
        }
        if let Some(v) = style.controls_spacing {
            self.set_controls_spacing(v);
        }
        if let Some(v) = style.controls_button_width {
            self.set_controls_button_width(v);
        }
        if let Some(v) = style.controls_close_hover_bg {
            self.set_controls_close_hover_bg(v);
        }
        if let Some(v) = style.controls_button_hover_bg {
            self.set_controls_button_hover_bg(v);
        }
    }

    pub fn set_controls_height(&self, height: f64) {
        #[cfg(any(target_os = "windows", target_os = "linux"))]
        {
            crate::config::set_titlebar_height(height.round().max(0.0) as u32);
            self.refresh_html();
        }
        #[cfg(target_os = "macos")]
        {
            crate::config::set_traffic_inset_y(height);
            crate::traffic::reposition_all(&self.app);
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
        let _ = height;
    }

    pub fn set_controls_inset_x(&self, inset: f64) {
        #[cfg(target_os = "macos")]
        {
            crate::config::set_traffic_inset_x(inset);
            crate::traffic::reposition_all(&self.app);
        }
        #[cfg(not(target_os = "macos"))]
        let _ = inset;
    }

    pub fn set_controls_spacing(&self, spacing: f64) {
        #[cfg(target_os = "macos")]
        {
            crate::config::set_traffic_spacing(spacing);
            crate::traffic::reposition_all(&self.app);
        }
        #[cfg(not(target_os = "macos"))]
        let _ = spacing;
    }

    pub fn set_controls_scale(&self, scale: f64) {
        #[cfg(target_os = "macos")]
        {
            crate::config::set_traffic_scale(scale);
            crate::traffic::reposition_all(&self.app);
        }
        #[cfg(any(target_os = "windows", target_os = "linux"))]
        {
            crate::config::set_controls_scale(scale);
            self.refresh_html();
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
        let _ = scale;
    }

    pub fn set_controls_button_width(&self, width: u32) {
        #[cfg(any(target_os = "windows", target_os = "linux"))]
        {
            crate::config::set_button_width(width);
            self.refresh_html();
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        let _ = width;
    }

    pub fn set_controls_close_hover_bg(&self, color: impl Into<String>) {
        #[cfg(any(target_os = "windows", target_os = "linux"))]
        {
            crate::config::set_close_hover_bg(color);
            self.refresh_html();
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        let _ = color.into();
    }

    pub fn set_controls_button_hover_bg(&self, color: impl Into<String>) {
        #[cfg(any(target_os = "windows", target_os = "linux"))]
        {
            crate::config::set_button_hover_bg(color);
            self.refresh_html();
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        let _ = color.into();
    }

    #[cfg(target_os = "windows")]
    fn refresh_html(&self) {
        crate::windows::apply_runtime_style(&self.app);
    }

    #[cfg(target_os = "linux")]
    fn refresh_html(&self) {
        crate::linux::apply_runtime_style(&self.app);
    }
}

pub trait DecorExt<R: Runtime> {
    fn decor(&self) -> &Decor<R>;
}

impl<R: Runtime, T: Manager<R>> DecorExt<R> for T {
    fn decor(&self) -> &Decor<R> {
        self.state::<Decor<R>>().inner()
    }
}

pub trait WebviewWindowExt {
    fn create_overlay_titlebar(&self) -> error::Result<&WebviewWindow>;

    #[cfg(target_os = "macos")]
    fn make_transparent(&self) -> error::Result<&WebviewWindow>;

    #[cfg(target_os = "macos")]
    fn set_window_level(&self, level: u32) -> error::Result<&WebviewWindow>;
}

impl WebviewWindowExt for WebviewWindow {
    fn create_overlay_titlebar(&self) -> error::Result<&WebviewWindow> {
        #[cfg(target_os = "windows")]
        self.set_decorations(false)?;

        overlay::register(self.label());

        #[cfg(target_os = "windows")]
        {
            let _ = self.eval(&crate::windows::build_scripts(
                crate::config::titlebar_height(),
            ));
        }

        Ok(self)
    }

    #[cfg(target_os = "macos")]
    fn make_transparent(&self) -> error::Result<&WebviewWindow> {
        use cocoa::{
            appkit::NSColor,
            base::{id, nil},
            foundation::NSString,
        };

        self.with_webview(|webview| unsafe {
            let id = webview.inner() as *mut objc::runtime::Object;
            let no: id = msg_send![class!(NSNumber), numberWithBool: 0];
            let _: id = msg_send![
                id,
                setValue: no
                forKey: NSString::alloc(nil).init_str("drawsBackground")
            ];
        })?;

        ensure_main_thread(self, move |win| {
            let ns_win = win.ns_window()? as id;
            unsafe {
                let bg = NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0.0, 0.0, 0.0, 0.0);
                let _: id = msg_send![ns_win, setBackgroundColor: bg];
            }
            Ok(win)
        })
    }

    #[cfg(target_os = "macos")]
    fn set_window_level(&self, level: u32) -> error::Result<&WebviewWindow> {
        ensure_main_thread(self, move |win| {
            let ns_win = win.ns_window()? as cocoa::base::id;
            unsafe {
                let _: () = msg_send![ns_win, setLevel: level];
            }
            Ok(win)
        })
    }
}

#[cfg(target_os = "macos")]
fn is_main_thread() -> bool {
    std::thread::current().name() == Some("main")
}

#[cfg(target_os = "macos")]
fn ensure_main_thread<F>(win: &WebviewWindow, action: F) -> error::Result<&WebviewWindow>
where
    F: FnOnce(&WebviewWindow) -> error::Result<&WebviewWindow> + Send + 'static,
{
    if is_main_thread() {
        action(win)?;
        Ok(win)
    } else {
        let win2 = win.clone();
        win.run_on_main_thread(move || {
            let _ = action(&win2);
        })?;
        Ok(win)
    }
}