tauri_plugin_prevent_default/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(clippy::format_push_string)]
4
5mod display;
6mod error;
7mod shortcut;
8
9#[cfg(all(target_os = "windows", feature = "unstable-windows"))]
10mod platform;
11
12use bitflags::bitflags;
13use std::sync::Arc;
14use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
15use tauri::{Manager, Runtime};
16
17pub use error::Error;
18pub use shortcut::{
19  KeyboardShortcut, KeyboardShortcutBuilder, ModifierKey, PointerEvent, PointerShortcut,
20  PointerShortcutBuilder, Shortcut, ShortcutKind,
21};
22
23#[cfg(all(target_os = "windows", feature = "unstable-windows"))]
24pub use platform::windows::PlatformOptions;
25
26bitflags! {
27  #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28  pub struct Flags: u32 {
29      /// Find (`Ctrl+F`, `Ctrl+G`, `Ctrl+Shift+G`, `F3`)
30      const FIND            = 1 << 0;
31      /// Caret browsing (`F7`)
32      const CARET_BROWSING  = 1 << 1;
33      /// Developer tools (`Ctrl+Shift+I`)
34      const DEV_TOOLS       = 1 << 2;
35      /// Downloads (`Ctrl+J`)
36      const DOWNLOADS       = 1 << 3;
37      /// Focus move (`Shift+Tab`)
38      const FOCUS_MOVE      = 1 << 4;
39      /// Reload (`F5`, `Ctrl+F5`, `Shift+F5`, `Ctrl+R`, `Ctrl+Shift+R`)
40      const RELOAD          = 1 << 5;
41      /// Source (`Ctrl+U`)
42      const SOURCE          = 1 << 6;
43      /// Open (`Ctrl+O`)
44      const OPEN            = 1 << 7;
45      /// Print document (`Ctrl+P`, `Ctrl+Shift+P`)
46      const PRINT           = 1 << 8;
47      /// Context menu (mouse right click)
48      const CONTEXT_MENU    = 1 << 9;
49  }
50}
51
52impl Flags {
53  /// All keyboard shortcuts.
54  pub fn keyboard() -> Self {
55    Self::all().difference(Self::pointer())
56  }
57
58  /// All pointer shortcuts.
59  pub fn pointer() -> Self {
60    Self::CONTEXT_MENU
61  }
62
63  /// Keep `CONTEXT_MENU`, `DEV_TOOLS`, and `RELOAD` shortcuts enabled when in debug mode.
64  pub fn debug() -> Self {
65    if cfg!(debug_assertions) {
66      Self::all().difference(Self::CONTEXT_MENU | Self::DEV_TOOLS | Self::RELOAD)
67    } else {
68      Self::all()
69    }
70  }
71}
72
73impl Default for Flags {
74  fn default() -> Self {
75    Self::all()
76  }
77}
78
79pub struct Builder {
80  flags: Flags,
81  shortcuts: Vec<Box<dyn Shortcut>>,
82  check_origin: Option<String>,
83
84  #[cfg(all(target_os = "windows", feature = "unstable-windows"))]
85  platform: PlatformOptions,
86}
87
88#[allow(clippy::derivable_impls)]
89impl Default for Builder {
90  fn default() -> Self {
91    Self {
92      flags: Flags::default(),
93      shortcuts: Vec::new(),
94      check_origin: None,
95
96      #[cfg(all(target_os = "windows", feature = "unstable-windows"))]
97      platform: PlatformOptions::default(),
98    }
99  }
100}
101
102impl Builder {
103  /// Create a new builder with default values.
104  pub fn new() -> Self {
105    Self::default()
106  }
107
108  /// Set flags to control which shortcuts the plugin should disable.
109  ///
110  /// # Examples
111  /// ```
112  /// use tauri_plugin_prevent_default::Flags;
113  ///
114  /// tauri_plugin_prevent_default::Builder::new()
115  ///   .with_flags(Flags::CONTEXT_MENU | Flags::PRINT | Flags::DOWNLOADS)
116  ///   .build();
117  /// ```
118  #[must_use]
119  pub fn with_flags(mut self, flags: Flags) -> Self {
120    self.flags = flags;
121    self
122  }
123
124  /// Disable a custom shortcut.
125  ///
126  /// # Examples
127  /// ```
128  /// use tauri_plugin_prevent_default::KeyboardShortcut;
129  /// use tauri_plugin_prevent_default::ModifierKey::{CtrlKey, ShiftKey};
130  ///
131  /// tauri_plugin_prevent_default::Builder::new()
132  ///   .shortcut(KeyboardShortcut::new("F12"))
133  ///   .shortcut(KeyboardShortcut::with_modifiers("E", &[CtrlKey, ShiftKey]))
134  ///   .shortcut(KeyboardShortcut::with_shift_alt("I"))
135  ///   .build();
136  /// ```
137  #[must_use]
138  pub fn shortcut<S>(mut self, shortcut: S) -> Self
139  where
140    S: Shortcut + 'static,
141  {
142    self.shortcuts.push(Box::new(shortcut));
143    self
144  }
145
146  /// Check location origin before disabling the shortcuts.
147  #[must_use]
148  pub fn check_origin(mut self, origin: impl AsRef<str>) -> Self {
149    self.check_origin = origin.as_ref().to_owned().into();
150    self
151  }
152
153  /// Windows-specific options.
154  #[must_use]
155  #[cfg(all(target_os = "windows", feature = "unstable-windows"))]
156  pub fn platform(mut self, options: PlatformOptions) -> Self {
157    self.platform = options;
158    self
159  }
160
161  /// Build the plugin.
162  pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
163    let script = self.create_script();
164    self
165      .plugin_builder()
166      .js_init_script(script.into())
167      .build()
168  }
169
170  /// Build the plugin, but do not inject the script into the webviews.
171  /// The script should then be manually set as the initialization script when creating the window.
172  ///
173  /// # Examples
174  /// ```
175  /// use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder};
176  /// use tauri_plugin_prevent_default::PreventDefault;
177  ///
178  /// fn create_window(app: &AppHandle) {
179  ///   let url = WebviewUrl::App("index.html".into());
180  ///   WebviewWindowBuilder::new(app, "main", url)
181  ///     .initialization_script(app.script())
182  ///     .build()
183  ///     .unwrap();
184  /// }
185  /// ```
186  pub fn build_with_manual_injection<R: Runtime>(mut self) -> TauriPlugin<R> {
187    let script = self.create_script();
188    self
189      .plugin_builder()
190      .setup(move |app, _| {
191        app.manage(script);
192        Ok(())
193      })
194      .build()
195  }
196
197  #[allow(clippy::unused_self)]
198  fn plugin_builder<R: Runtime>(self) -> PluginBuilder<R> {
199    #[allow(unused_mut)]
200    let mut builder = PluginBuilder::new("prevent-default");
201
202    #[cfg(all(target_os = "windows", feature = "unstable-windows"))]
203    {
204      let options = self.platform;
205      builder = builder.on_webview_ready(move |webview| {
206        platform::windows::on_webview_ready(&webview, &options);
207      });
208    }
209
210    builder
211  }
212
213  fn create_script(&mut self) -> Script {
214    self.add_keyboard_shortcuts();
215    self.add_pointer_shortcuts();
216
217    let mut script = String::new();
218
219    for shortcut in &mut self.shortcuts {
220      match shortcut.kind() {
221        ShortcutKind::Keyboard(it) => {
222          let modifiers = it.modifiers();
223          let mut options = String::with_capacity(modifiers.len() * 12);
224          for modifier in modifiers {
225            options.push_str(&format!("{modifier}:true,"));
226          }
227
228          let options = options.trim_end_matches(',');
229          script.push_str(&format!("onKey('{}',{{{}}});", it.key(), options));
230        }
231        ShortcutKind::Pointer(it) => {
232          script.push_str(&format!("onPointer('{}');", it.event()));
233        }
234      }
235    }
236
237    let origin = self
238      .check_origin
239      .as_deref()
240      .map(|it| format!("const ORIGIN='{it}';"))
241      .unwrap_or_else(|| "const ORIGIN=null;".to_owned());
242
243    include_str!("../scripts/script.js")
244      .trim()
245      .replace("/*ORIGIN*/", &origin)
246      .replace("/*SCRIPT*/", &script)
247      .into()
248  }
249
250  fn add_keyboard_shortcuts(&mut self) {
251    use shortcut::ModifierKey::{CtrlKey, ShiftKey};
252
253    macro_rules! on_key {
254      ($($arg:literal)+) => {
255        $(
256          let shortcut = KeyboardShortcut::new($arg);
257          self.shortcuts.push(Box::new(shortcut));
258        )*
259      };
260      ($modifiers:expr, $($arg:literal),+) => {
261        $(
262          let shortcut = KeyboardShortcut::with_modifiers($arg, $modifiers);
263          self.shortcuts.push(Box::new(shortcut));
264        )*
265      };
266    }
267
268    if self.flags.contains(Flags::FIND) {
269      on_key!("F3");
270      on_key!(&[CtrlKey], "f", "g");
271      on_key!(&[CtrlKey, ShiftKey], "g");
272    }
273
274    if self.flags.contains(Flags::CARET_BROWSING) {
275      on_key!("F7");
276    }
277
278    if self.flags.contains(Flags::DEV_TOOLS) {
279      on_key!(&[CtrlKey, ShiftKey], "i");
280    }
281
282    if self.flags.contains(Flags::DOWNLOADS) {
283      on_key!(&[CtrlKey], "j");
284    }
285
286    if self.flags.contains(Flags::FOCUS_MOVE) {
287      on_key!(&[ShiftKey], "Tab");
288    }
289
290    if self.flags.contains(Flags::RELOAD) {
291      on_key!("F5");
292      on_key!(&[CtrlKey], "F5");
293      on_key!(&[ShiftKey], "F5");
294      on_key!(&[CtrlKey], "r");
295      on_key!(&[CtrlKey, ShiftKey], "r");
296    }
297
298    if self.flags.contains(Flags::SOURCE) {
299      on_key!(&[CtrlKey], "u");
300    }
301
302    if self.flags.contains(Flags::OPEN) {
303      on_key!(&[CtrlKey], "o");
304    }
305
306    if self.flags.contains(Flags::PRINT) {
307      on_key!(&[CtrlKey], "p");
308      on_key!(&[CtrlKey, ShiftKey], "p");
309    }
310  }
311
312  fn add_pointer_shortcuts(&mut self) {
313    if self.flags.contains(Flags::CONTEXT_MENU) {
314      let shortcut = PointerShortcut::new(PointerEvent::ContextMenu);
315      self.shortcuts.push(Box::new(shortcut));
316    }
317  }
318}
319
320/// Provide access to the script.
321pub trait PreventDefault<R: Runtime> {
322  /// Retrieve the script.
323  ///
324  /// # Panics
325  ///
326  /// Panics if the plugin was not [built with manual injection](Builder::build_with_manual_injection).
327  fn script(&self) -> Script;
328
329  /// Attempt to retrieve the script.
330  ///
331  /// Returns `Some` if the plugin was [built with manual injection](Builder::build_with_manual_injection).
332  /// Otherwise returns `None`.
333  fn try_script(&self) -> Option<Script>;
334}
335
336impl<R, T> PreventDefault<R> for T
337where
338  R: Runtime,
339  T: Manager<R>,
340{
341  fn script(&self) -> Script {
342    (*self.app_handle().state::<Script>()).clone()
343  }
344
345  fn try_script(&self) -> Option<Script> {
346    self
347      .app_handle()
348      .try_state::<Script>()
349      .as_deref()
350      .cloned()
351  }
352}
353
354/// Script to be injected into the webview.
355pub struct Script(Arc<str>);
356
357impl Clone for Script {
358  fn clone(&self) -> Self {
359    Self(Arc::clone(&self.0))
360  }
361}
362
363impl From<String> for Script {
364  fn from(value: String) -> Self {
365    Script(Arc::from(value))
366  }
367}
368
369impl From<Script> for String {
370  fn from(value: Script) -> Self {
371    String::from(value.0.as_ref())
372  }
373}
374
375/// Initialize the plugin with default values.
376pub fn init<R: Runtime>() -> TauriPlugin<R> {
377  Builder::default().build()
378}
379
380/// Initialize the plugin with default values while also allowing for manual injection.
381pub fn init_with_manual_injection<R: Runtime>() -> TauriPlugin<R> {
382  Builder::default().build_with_manual_injection()
383}
384
385/// Initialize the plugin with given flags.
386pub fn with_flags<R: Runtime>(flags: Flags) -> TauriPlugin<R> {
387  Builder::new().with_flags(flags).build()
388}
389
390/// Initialize the plugin with given flags while also allowing for manual injection.
391pub fn with_flags_and_manual_injection<R: Runtime>(flags: Flags) -> TauriPlugin<R> {
392  Builder::new()
393    .with_flags(flags)
394    .build_with_manual_injection()
395}
396
397/// Initialize the plugin with the default [debug flags](Flags::debug).
398pub fn debug<R: Runtime>() -> TauriPlugin<R> {
399  Builder::new().with_flags(Flags::debug()).build()
400}
401
402/// Initialize the plugin with the default [debug flags](Flags::debug)
403/// while also allowing for manual injection.
404pub fn debug_with_manual_injection<R: Runtime>() -> TauriPlugin<R> {
405  Builder::new()
406    .with_flags(Flags::debug())
407    .build_with_manual_injection()
408}