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}