Skip to main content

tauri_plugin_prevent_default/
lib.rs

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