tauri_plugin_frame/
lib.rs

1use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
2use tauri::plugin::{Builder, TauriPlugin};
3use tauri::{async_runtime, Emitter, Error, Listener, Runtime, WebviewWindow};
4
5mod commands;
6
7static TITLEBAR_HEIGHT: AtomicU32 = AtomicU32::new(32);
8static AUTO_TITLEBAR: AtomicBool = AtomicBool::new(false);
9
10/// Builder for the frame plugin
11pub struct FramePluginBuilder {
12    titlebar_height: u32,
13    auto_titlebar: bool,
14}
15
16impl FramePluginBuilder {
17    pub fn new() -> Self {
18        Self {
19            titlebar_height: 32,
20            auto_titlebar: false,
21        }
22    }
23
24    /// Set the titlebar height in pixels (default: 32)
25    pub fn titlebar_height(mut self, height: u32) -> Self {
26        self.titlebar_height = height;
27        self
28    }
29
30    /// Automatically apply titlebar to all windows (default: false)
31    pub fn auto_titlebar(mut self, auto: bool) -> Self {
32        self.auto_titlebar = auto;
33        self
34    }
35
36    /// Build the plugin
37    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
38        TITLEBAR_HEIGHT.store(self.titlebar_height, Ordering::SeqCst);
39        AUTO_TITLEBAR.store(self.auto_titlebar, Ordering::SeqCst);
40
41        Builder::new("frame")
42            .invoke_handler(tauri::generate_handler![commands::show_snap_overlay])
43            .on_page_load(|webview, _| {
44                let _ = webview.emit("frame-page-load", ());
45
46                if AUTO_TITLEBAR.load(Ordering::SeqCst) {
47                    let height = TITLEBAR_HEIGHT.load(Ordering::SeqCst);
48                    let webview = webview.clone();
49
50                    async_runtime::spawn(async move {
51                        let _ = webview.eval(build_scripts(height, None));
52                    });
53                }
54            })
55            .build()
56    }
57}
58
59impl Default for FramePluginBuilder {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65/// Initialize the frame plugin with default settings
66pub fn init<R: Runtime>() -> TauriPlugin<R> {
67    FramePluginBuilder::new().build()
68}
69
70fn build_scripts(height: u32, controls: Option<Vec<&str>>) -> String {
71    let height_px = format!("\"{}px\"", height);
72
73    let script_tb = include_str!("js/titlebar.js").replace("\"32px\"", &height_px);
74    let mut script_controls = include_str!("js/controls.js").replace("\"32px\"", &height_px);
75
76    if let Some(ctrl) = controls {
77        script_controls = script_controls.replacen(
78            "[\"minimize\", \"maximize\", \"close\"]",
79            &format!("{:?}", ctrl),
80            1,
81        );
82    }
83
84    format!("{}\n{}", script_tb, script_controls)
85}
86
87/// Extensions to [`tauri::WebviewWindow`] to access the frame APIs.
88pub trait WebviewWindowExt {
89    fn create_overlay_titlebar(&self) -> Result<&WebviewWindow, Error>;
90    fn create_overlay_titlebar_with_height(&self, height: u32) -> Result<&WebviewWindow, Error>;
91}
92
93impl WebviewWindowExt for WebviewWindow {
94    fn create_overlay_titlebar(&self) -> Result<&WebviewWindow, Error> {
95        self.create_overlay_titlebar_with_height(TITLEBAR_HEIGHT.load(Ordering::SeqCst))
96    }
97
98    fn create_overlay_titlebar_with_height(&self, height: u32) -> Result<&WebviewWindow, Error> {
99        self.set_decorations(false)?;
100
101        let win = self.clone();
102        self.listen("frame-page-load", move |event| {
103            let controls: Vec<&str> = [
104                win.is_minimizable().unwrap_or(false).then_some("minimize"),
105                (win.is_maximizable().unwrap_or(false) && win.is_resizable().unwrap_or(false))
106                    .then_some("maximize"),
107                win.is_closable().unwrap_or(false).then_some("close"),
108            ]
109            .into_iter()
110            .flatten()
111            .collect();
112
113            let _ = win.eval(build_scripts(height, Some(controls)));
114
115            let win2 = win.clone();
116            win.on_window_event(move |e| {
117                if matches!(e, tauri::WindowEvent::CloseRequested { .. }) {
118                    win2.unlisten(event.id());
119                }
120            });
121        });
122
123        Ok(self)
124    }
125}