Skip to main content

tauri_plugin_macos_fps/
lib.rs

1//! # tauri-plugin-macos-fps
2//!
3//! Unlock >60fps rendering on macOS for Tauri v2 apps.
4//!
5//! WKWebView caps `requestAnimationFrame` at 60fps regardless of display refresh rate.
6//! This plugin disables that cap by toggling WebKit's internal
7//! `PreferPageRenderingUpdatesNear60FPSEnabled` preference via the private `_features` API.
8//!
9//! On non-macOS platforms, the plugin is a no-op.
10//!
11//! ## Quick start
12//!
13//! ```rust,ignore
14//! fn main() {
15//!     tauri::Builder::default()
16//!         .plugin(tauri_plugin_macos_fps::init())
17//!         .run(tauri::generate_context!())
18//!         .expect("error while running tauri application");
19//! }
20//! ```
21//!
22//! ## Manual per-webview control
23//!
24//! ```rust,ignore
25//! use tauri_plugin_macos_fps::MacFpsExt;
26//!
27//! // Unlock native refresh rate for a specific webview:
28//! webview.unlock_fps()?;
29//!
30//! // Re-lock to 60fps:
31//! webview.lock_fps()?;
32//! ```
33
34use serde::Deserialize;
35use tauri::{
36    plugin::{Builder, TauriPlugin},
37    Manager, Runtime, Webview,
38};
39
40#[cfg(target_os = "macos")]
41mod macos;
42
43// ── Configuration ──────────────────────────────────────────────
44
45fn default_enabled() -> bool {
46    true
47}
48
49/// Plugin configuration. Set in `tauri.conf.json` under `plugins.macos-fps`:
50///
51/// ```json
52/// {
53///   "plugins": {
54///     "macos-fps": {
55///       "enabled": true
56///     }
57///   }
58/// }
59/// ```
60#[derive(Debug, Deserialize)]
61pub struct Config {
62    /// Whether to automatically unlock the display's native refresh rate on all webviews.
63    /// Defaults to `true`.
64    #[serde(default = "default_enabled")]
65    pub enabled: bool,
66}
67
68impl Default for Config {
69    fn default() -> Self {
70        Self { enabled: true }
71    }
72}
73
74struct PluginState {
75    enabled: bool,
76}
77
78// ── Extension trait ────────────────────────────────────────────
79
80/// Extension trait for manual per-webview control of the frame rate cap.
81pub trait MacFpsExt<R: Runtime> {
82    /// Disable the 60fps cap on this webview, enabling the display's native refresh rate.
83    ///
84    /// On non-macOS platforms, this is a no-op.
85    fn unlock_fps(&self) -> tauri::Result<()>;
86
87    /// Re-enable the 60fps cap on this webview.
88    ///
89    /// On non-macOS platforms, this is a no-op.
90    fn lock_fps(&self) -> tauri::Result<()>;
91}
92
93impl<R: Runtime> MacFpsExt<R> for Webview<R> {
94    fn unlock_fps(&self) -> tauri::Result<()> {
95        #[cfg(target_os = "macos")]
96        {
97            self.with_webview(|webview| {
98                let ptr = webview.inner();
99                if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
100                    macos::disable_60fps_cap(ptr);
101                })) {
102                    log::error!("tauri-plugin-macos-fps: panic in unlock_fps: {:?}", e);
103                }
104            })?;
105        }
106        Ok(())
107    }
108
109    fn lock_fps(&self) -> tauri::Result<()> {
110        #[cfg(target_os = "macos")]
111        {
112            self.with_webview(|webview| {
113                let ptr = webview.inner();
114                if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
115                    macos::enable_60fps_cap(ptr);
116                })) {
117                    log::error!("tauri-plugin-macos-fps: panic in lock_fps: {:?}", e);
118                }
119            })?;
120        }
121        Ok(())
122    }
123}
124
125// ── Plugin initializer ────────────────────────────────────────
126
127/// Initialize the plugin.
128///
129/// When registered, automatically disables the 60fps cap on every webview
130/// as it is created (unless `enabled: false` in config).
131///
132/// ```rust,ignore
133/// tauri::Builder::default()
134///     .plugin(tauri_plugin_macos_fps::init())
135///     .run(tauri::generate_context!())
136///     .expect("error while running tauri application");
137/// ```
138pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
139    Builder::<R, Config>::new("macos-fps")
140        .setup(|app, api| {
141            let enabled = api.config().enabled;
142            if !enabled {
143                log::info!("tauri-plugin-macos-fps: disabled via configuration");
144            }
145            app.manage(PluginState { enabled });
146            Ok(())
147        })
148        .on_webview_ready(|webview| {
149            let enabled = webview
150                .try_state::<PluginState>()
151                .map(|s| s.enabled)
152                .unwrap_or(true);
153
154            if !enabled {
155                return;
156            }
157
158            #[cfg(target_os = "macos")]
159            {
160                let _ = webview.with_webview(|wv| {
161                    let ptr = wv.inner();
162                    if let Err(e) =
163                        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
164                            macos::disable_60fps_cap(ptr);
165                        }))
166                    {
167                        log::error!(
168                            "tauri-plugin-macos-fps: panic in on_webview_ready: {:?}",
169                            e
170                        );
171                    }
172                });
173            }
174
175            #[cfg(not(target_os = "macos"))]
176            {
177                let _ = webview;
178            }
179        })
180        .build()
181}