tauri_plugin_prevent_default/
lib.rs

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