Skip to main content

tauri_plugin_frame/
lib.rs

1use tauri::{
2    plugin::{Builder, TauriPlugin},
3    Manager, Runtime,
4};
5
6mod desktop;
7mod error;
8#[cfg(windows)]
9mod snap;
10
11pub use desktop::{Frame, WebviewWindowExt};
12pub use error::{Error, Result};
13
14#[cfg(windows)]
15use std::sync::{
16    atomic::{AtomicBool, AtomicU32, Ordering},
17    OnceLock,
18};
19
20#[cfg(windows)]
21use tauri::Emitter;
22
23#[cfg(windows)]
24static TITLEBAR_HEIGHT: AtomicU32 = AtomicU32::new(32);
25#[cfg(windows)]
26static BUTTON_WIDTH: AtomicU32 = AtomicU32::new(46);
27#[cfg(windows)]
28static AUTO_TITLEBAR: AtomicBool = AtomicBool::new(false);
29#[cfg(windows)]
30static NATIVE_SNAP_OVERLAY: AtomicBool = AtomicBool::new(true);
31#[cfg(windows)]
32static CLOSE_HOVER_BG: OnceLock<String> = OnceLock::new();
33#[cfg(windows)]
34static BUTTON_HOVER_BG_LIGHT: OnceLock<String> = OnceLock::new();
35#[cfg(windows)]
36static BUTTON_HOVER_BG_DARK: OnceLock<String> = OnceLock::new();
37
38pub struct FramePluginBuilder {
39    #[cfg(windows)]
40    titlebar_height: u32,
41    #[cfg(windows)]
42    button_width: u32,
43    #[cfg(windows)]
44    auto_titlebar: bool,
45    #[cfg(windows)]
46    snap_overlay: bool,
47    #[cfg(windows)]
48    close_hover_bg: String,
49    #[cfg(windows)]
50    button_hover_bg_light: String,
51    #[cfg(windows)]
52    button_hover_bg_dark: String,
53}
54
55impl Default for FramePluginBuilder {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl FramePluginBuilder {
62    pub fn new() -> Self {
63        Self {
64            #[cfg(windows)]
65            titlebar_height: 32,
66            #[cfg(windows)]
67            button_width: 46,
68            #[cfg(windows)]
69            auto_titlebar: false,
70            #[cfg(windows)]
71            snap_overlay: true,
72            #[cfg(windows)]
73            close_hover_bg: "rgba(196,43,28,1)".into(),
74            #[cfg(windows)]
75            button_hover_bg_light: "rgba(0,0,0,0.1)".into(),
76            #[cfg(windows)]
77            button_hover_bg_dark: "rgba(255,255,255,0.1)".into(),
78        }
79    }
80
81    #[cfg(windows)]
82    pub fn titlebar_height(mut self, height: u32) -> Self {
83        self.titlebar_height = height;
84        self
85    }
86
87    #[cfg(not(windows))]
88    pub fn titlebar_height(self, _: u32) -> Self {
89        self
90    }
91
92    #[cfg(windows)]
93    pub fn button_width(mut self, width: u32) -> Self {
94        self.button_width = width;
95        self
96    }
97
98    #[cfg(not(windows))]
99    pub fn button_width(self, _: u32) -> Self {
100        self
101    }
102
103    #[cfg(windows)]
104    pub fn auto_titlebar(mut self, auto: bool) -> Self {
105        self.auto_titlebar = auto;
106        self
107    }
108
109    #[cfg(not(windows))]
110    pub fn auto_titlebar(self, _: bool) -> Self {
111        self
112    }
113
114    #[cfg(windows)]
115    pub fn snap_overlay(mut self, enabled: bool) -> Self {
116        self.snap_overlay = enabled;
117        self
118    }
119
120    #[cfg(not(windows))]
121    pub fn snap_overlay(self, _: bool) -> Self {
122        self
123    }
124
125    #[cfg(windows)]
126    pub fn close_hover_bg(mut self, color: impl Into<String>) -> Self {
127        self.close_hover_bg = color.into();
128        self
129    }
130
131    #[cfg(not(windows))]
132    pub fn close_hover_bg(self, _: impl Into<String>) -> Self {
133        self
134    }
135
136    /// Set the hover background color for non-close buttons in light mode.
137    #[cfg(windows)]
138    pub fn button_hover_bg_light(mut self, color: impl Into<String>) -> Self {
139        self.button_hover_bg_light = color.into();
140        self
141    }
142
143    #[cfg(not(windows))]
144    pub fn button_hover_bg_light(self, _: impl Into<String>) -> Self {
145        self
146    }
147
148    /// Set the hover background color for non-close buttons in dark mode.
149    #[cfg(windows)]
150    pub fn button_hover_bg_dark(mut self, color: impl Into<String>) -> Self {
151        self.button_hover_bg_dark = color.into();
152        self
153    }
154
155    #[cfg(not(windows))]
156    pub fn button_hover_bg_dark(self, _: impl Into<String>) -> Self {
157        self
158    }
159
160    /// Set a single hover background color for non-close buttons (applies to both light and dark mode).
161    #[cfg(windows)]
162    pub fn button_hover_bg(mut self, color: impl Into<String>) -> Self {
163        let c = color.into();
164        self.button_hover_bg_light = c.clone();
165        self.button_hover_bg_dark = c;
166        self
167    }
168
169    #[cfg(not(windows))]
170    pub fn button_hover_bg(self, _: impl Into<String>) -> Self {
171        self
172    }
173
174    #[cfg(windows)]
175    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
176        TITLEBAR_HEIGHT.store(self.titlebar_height, Ordering::SeqCst);
177        BUTTON_WIDTH.store(self.button_width, Ordering::SeqCst);
178        AUTO_TITLEBAR.store(self.auto_titlebar, Ordering::SeqCst);
179        NATIVE_SNAP_OVERLAY.store(self.snap_overlay, Ordering::SeqCst);
180        let _ = CLOSE_HOVER_BG.set(self.close_hover_bg);
181        let _ = BUTTON_HOVER_BG_LIGHT.set(self.button_hover_bg_light);
182        let _ = BUTTON_HOVER_BG_DARK.set(self.button_hover_bg_dark);
183
184        Builder::new("frame")
185            .setup(|app, _| {
186                app.manage(Frame::new(app.clone()));
187                Ok(())
188            })
189            .on_page_load(|webview, _| {
190                let _ = webview.emit("frame-page-load", ());
191                if !AUTO_TITLEBAR.load(Ordering::SeqCst) {
192                    return;
193                }
194                let height = TITLEBAR_HEIGHT.load(Ordering::SeqCst);
195                let button_width = BUTTON_WIDTH.load(Ordering::SeqCst);
196                let snap_overlay = NATIVE_SNAP_OVERLAY.load(Ordering::SeqCst);
197                let webview = webview.clone();
198                tauri::async_runtime::spawn(async move {
199                    let _ = webview.eval(build_scripts(height, None));
200                    if snap_overlay {
201                        // Default right_index=1 (close button is present by default)
202                        let _ = crate::snap::install_window(&webview.window(), height, button_width, 1);
203                    }
204                });
205            })
206            .build()
207    }
208
209    #[cfg(not(windows))]
210    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
211        Builder::new("frame")
212            .setup(|app, _| {
213                app.manage(Frame::new(app.clone()));
214                Ok(())
215            })
216            .build()
217    }
218}
219
220pub fn init<R: Runtime>() -> TauriPlugin<R> {
221    FramePluginBuilder::new().build()
222}
223
224#[cfg(windows)]
225pub(crate) fn snap_overlay_enabled() -> bool {
226    NATIVE_SNAP_OVERLAY.load(Ordering::SeqCst)
227}
228
229
230#[cfg(windows)]
231pub(crate) fn get_titlebar_height() -> u32 {
232    TITLEBAR_HEIGHT.load(Ordering::SeqCst)
233}
234
235#[cfg(windows)]
236pub(crate) fn get_button_width() -> u32 {
237    BUTTON_WIDTH.load(Ordering::SeqCst)
238}
239
240#[cfg(windows)]
241pub(crate) fn get_auto_titlebar() -> bool {
242    AUTO_TITLEBAR.load(Ordering::SeqCst)
243}
244
245#[cfg(windows)]
246pub(crate) fn build_scripts(height: u32, controls: Option<Vec<&str>>) -> String {
247    let height_px = format!("\"{}px\"", height);
248    let width_px = format!("\"{}px\"", BUTTON_WIDTH.load(Ordering::SeqCst));
249    let close_hover = CLOSE_HOVER_BG
250        .get()
251        .map_or("rgba(196,43,28,1)", |s| s.as_str());
252    let button_hover_light = BUTTON_HOVER_BG_LIGHT
253        .get()
254        .map_or("rgba(0,0,0,0.1)", |s| s.as_str());
255    let button_hover_dark = BUTTON_HOVER_BG_DARK
256        .get()
257        .map_or("rgba(255,255,255,0.1)", |s| s.as_str());
258
259    let script_tb = include_str!("js/titlebar.js").replace("\"32px\"", &height_px);
260    let mut script_controls = include_str!("js/controls.js")
261        .replace("\"32px\"", &height_px)
262        .replace("\"46px\"", &width_px)
263        .replace("\"__CLOSE_HOVER_BG__\"", &format!("\"{}\"", close_hover))
264        .replace("\"__BUTTON_HOVER_BG_LIGHT__\"", &format!("\"{}\"", button_hover_light))
265        .replace("\"__BUTTON_HOVER_BG_DARK__\"", &format!("\"{}\"", button_hover_dark));
266
267    if let Some(ctrl) = controls {
268        script_controls = script_controls.replacen(
269            "[\"minimize\", \"maximize\", \"close\"]",
270            &format!("{:?}", ctrl),
271            1,
272        );
273    }
274
275    format!("{}\n{}", script_tb, script_controls)
276}
277
278pub trait FrameExt<R: Runtime> {
279    fn frame(&self) -> &Frame<R>;
280}
281
282impl<R: Runtime, T: Manager<R>> FrameExt<R> for T {
283    fn frame(&self) -> &Frame<R> {
284        self.state::<Frame<R>>().inner()
285    }
286}