tauri_plugin_frame/
lib.rs

1use tauri::{
2    plugin::{Builder, TauriPlugin},
3    Manager, Runtime,
4};
5
6mod commands;
7mod desktop;
8mod error;
9
10pub use desktop::{Frame, WebviewWindowExt};
11pub use error::{Error, Result};
12
13#[cfg(windows)]
14use std::sync::{
15    atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
16    OnceLock,
17};
18
19#[cfg(windows)]
20use tauri::Emitter;
21
22#[cfg(windows)]
23static TITLEBAR_HEIGHT: AtomicU32 = AtomicU32::new(32);
24#[cfg(windows)]
25static BUTTON_WIDTH: AtomicU32 = AtomicU32::new(46);
26#[cfg(windows)]
27static AUTO_TITLEBAR: AtomicBool = AtomicBool::new(false);
28#[cfg(windows)]
29static SNAP_OVERLAY_DELAY_MS: AtomicU64 = AtomicU64::new(10);
30#[cfg(windows)]
31static CLOSE_HOVER_BG: OnceLock<String> = OnceLock::new();
32#[cfg(windows)]
33static BUTTON_HOVER_BG: OnceLock<String> = OnceLock::new();
34
35pub struct FramePluginBuilder {
36    #[cfg(windows)]
37    titlebar_height: u32,
38    #[cfg(windows)]
39    button_width: u32,
40    #[cfg(windows)]
41    auto_titlebar: bool,
42    #[cfg(windows)]
43    snap_overlay_delay_ms: u64,
44    #[cfg(windows)]
45    close_hover_bg: String,
46    #[cfg(windows)]
47    button_hover_bg: String,
48}
49
50impl Default for FramePluginBuilder {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl FramePluginBuilder {
57    pub fn new() -> Self {
58        Self {
59            #[cfg(windows)]
60            titlebar_height: 32,
61            #[cfg(windows)]
62            button_width: 46,
63            #[cfg(windows)]
64            auto_titlebar: false,
65            #[cfg(windows)]
66            snap_overlay_delay_ms: 10,
67            #[cfg(windows)]
68            close_hover_bg: "rgba(196,43,28,1)".into(),
69            #[cfg(windows)]
70            button_hover_bg: "rgba(0,0,0,0.2)".into(),
71        }
72    }
73
74    #[cfg(windows)]
75    pub fn titlebar_height(mut self, height: u32) -> Self {
76        self.titlebar_height = height;
77        self
78    }
79
80    #[cfg(not(windows))]
81    pub fn titlebar_height(self, _: u32) -> Self {
82        self
83    }
84
85    #[cfg(windows)]
86    pub fn button_width(mut self, width: u32) -> Self {
87        self.button_width = width;
88        self
89    }
90
91    #[cfg(not(windows))]
92    pub fn button_width(self, _: u32) -> Self {
93        self
94    }
95
96    #[cfg(windows)]
97    pub fn auto_titlebar(mut self, auto: bool) -> Self {
98        self.auto_titlebar = auto;
99        self
100    }
101
102    #[cfg(not(windows))]
103    pub fn auto_titlebar(self, _: bool) -> Self {
104        self
105    }
106
107    #[cfg(windows)]
108    pub fn snap_overlay_delay_ms(mut self, delay: u64) -> Self {
109        self.snap_overlay_delay_ms = delay;
110        self
111    }
112
113    #[cfg(not(windows))]
114    pub fn snap_overlay_delay_ms(self, _: u64) -> Self {
115        self
116    }
117
118    #[cfg(windows)]
119    pub fn close_hover_bg(mut self, color: impl Into<String>) -> Self {
120        self.close_hover_bg = color.into();
121        self
122    }
123
124    #[cfg(not(windows))]
125    pub fn close_hover_bg(self, _: impl Into<String>) -> Self {
126        self
127    }
128
129    #[cfg(windows)]
130    pub fn button_hover_bg(mut self, color: impl Into<String>) -> Self {
131        self.button_hover_bg = color.into();
132        self
133    }
134
135    #[cfg(not(windows))]
136    pub fn button_hover_bg(self, _: impl Into<String>) -> Self {
137        self
138    }
139
140    #[cfg(windows)]
141    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
142        TITLEBAR_HEIGHT.store(self.titlebar_height, Ordering::SeqCst);
143        BUTTON_WIDTH.store(self.button_width, Ordering::SeqCst);
144        AUTO_TITLEBAR.store(self.auto_titlebar, Ordering::SeqCst);
145        SNAP_OVERLAY_DELAY_MS.store(self.snap_overlay_delay_ms, Ordering::SeqCst);
146        let _ = CLOSE_HOVER_BG.set(self.close_hover_bg);
147        let _ = BUTTON_HOVER_BG.set(self.button_hover_bg);
148
149        Builder::new("frame")
150            .invoke_handler(tauri::generate_handler![commands::show_snap_overlay])
151            .setup(|app, _| {
152                app.manage(Frame::new(app.clone()));
153                Ok(())
154            })
155            .on_page_load(|webview, _| {
156                let _ = webview.emit("frame-page-load", ());
157                if !AUTO_TITLEBAR.load(Ordering::SeqCst) {
158                    return;
159                }
160                let height = TITLEBAR_HEIGHT.load(Ordering::SeqCst);
161                let webview = webview.clone();
162                tauri::async_runtime::spawn(async move {
163                    let _ = webview.eval(build_scripts(height, None));
164                });
165            })
166            .build()
167    }
168
169    #[cfg(not(windows))]
170    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
171        Builder::new("frame")
172            .invoke_handler(tauri::generate_handler![commands::show_snap_overlay])
173            .setup(|app, _| {
174                app.manage(Frame::new(app.clone()));
175                Ok(())
176            })
177            .build()
178    }
179}
180
181pub fn init<R: Runtime>() -> TauriPlugin<R> {
182    FramePluginBuilder::new().build()
183}
184
185#[cfg(windows)]
186pub(crate) fn get_snap_overlay_delay_ms() -> u64 {
187    SNAP_OVERLAY_DELAY_MS.load(Ordering::SeqCst)
188}
189
190#[cfg(windows)]
191pub(crate) fn get_titlebar_height() -> u32 {
192    TITLEBAR_HEIGHT.load(Ordering::SeqCst)
193}
194
195#[cfg(windows)]
196pub(crate) fn get_auto_titlebar() -> bool {
197    AUTO_TITLEBAR.load(Ordering::SeqCst)
198}
199
200#[cfg(windows)]
201pub(crate) fn build_scripts(height: u32, controls: Option<Vec<&str>>) -> String {
202    let height_px = format!("\"{}px\"", height);
203    let width_px = format!("\"{}px\"", BUTTON_WIDTH.load(Ordering::SeqCst));
204    let close_hover = CLOSE_HOVER_BG.get().map_or("rgba(196,43,28,1)", |s| s.as_str());
205    let button_hover = BUTTON_HOVER_BG.get().map_or("rgba(0,0,0,0.2)", |s| s.as_str());
206
207    let script_tb = include_str!("js/titlebar.js").replace("\"32px\"", &height_px);
208    let mut script_controls = include_str!("js/controls.js")
209        .replace("\"32px\"", &height_px)
210        .replace("\"46px\"", &width_px)
211        .replace("rgba(196,43,28,1)", close_hover)
212        .replace("rgba(0,0,0,0.2)", button_hover);
213
214    if let Some(ctrl) = controls {
215        script_controls = script_controls.replacen(
216            "[\"minimize\", \"maximize\", \"close\"]",
217            &format!("{:?}", ctrl),
218            1,
219        );
220    }
221
222    format!("{}\n{}", script_tb, script_controls)
223}
224
225pub trait FrameExt<R: Runtime> {
226    fn frame(&self) -> &Frame<R>;
227}
228
229impl<R: Runtime, T: Manager<R>> FrameExt<R> for T {
230    fn frame(&self) -> &Frame<R> {
231        self.state::<Frame<R>>().inner()
232    }
233}