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 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    self.check_origin = origin.as_ref().to_owned().into();
151    self
152  }
153
154  /// Windows-specific options.
155  #[must_use]
156  #[cfg(all(target_os = "windows", feature = "platform-windows"))]
157  pub fn platform(mut self, options: PlatformOptions) -> Self {
158    self.platform = options;
159    self
160  }
161
162  /// Build the plugin.
163  pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
164    let script = self.create_script();
165    self
166      .plugin_builder()
167      .js_init_script(script.to_string())
168      .build()
169  }
170
171  /// Build the plugin, but do not inject the script into the webviews.
172  /// The script should then be manually set as the initialization script when creating the window.
173  ///
174  /// # Examples
175  /// ```
176  /// use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder};
177  /// use tauri_plugin_prevent_default::PreventDefault;
178  ///
179  /// fn create_window(app: &AppHandle) {
180  ///   let url = WebviewUrl::App("index.html".into());
181  ///   WebviewWindowBuilder::new(app, "main", url)
182  ///     .initialization_script(app.script())
183  ///     .build()
184  ///     .unwrap();
185  /// }
186  /// ```
187  pub fn build_with_manual_injection<R: Runtime>(mut self) -> TauriPlugin<R> {
188    let script = self.create_script();
189    self
190      .plugin_builder()
191      .setup(move |app, _| {
192        app.manage(script);
193        Ok(())
194      })
195      .build()
196  }
197
198  #[allow(clippy::unused_self)]
199  fn plugin_builder<R: Runtime>(self) -> PluginBuilder<R> {
200    #[allow(unused_mut)]
201    let mut builder = PluginBuilder::new("prevent-default");
202
203    #[cfg(all(target_os = "windows", feature = "platform-windows"))]
204    {
205      let options = self.platform;
206      builder = builder.on_webview_ready(move |webview| {
207        platform::windows::on_webview_ready(&webview, &options);
208      });
209    }
210
211    builder
212  }
213
214  fn create_script(&mut self) -> Script {
215    self.add_keyboard_shortcuts();
216    self.add_pointer_shortcuts();
217
218    let mut script = String::new();
219
220    for shortcut in &mut self.shortcuts {
221      match shortcut.kind() {
222        ShortcutKind::Keyboard(it) => {
223          let modifiers = it.modifiers();
224          let mut options = String::with_capacity(modifiers.len() * 12);
225          for modifier in modifiers {
226            options.push_str(&format!("{modifier}:true,"));
227          }
228
229          let options = options.trim_end_matches(',');
230          script.push_str(&format!("onKey('{}',{{{}}});", it.key(), options));
231        }
232        ShortcutKind::Pointer(it) => {
233          script.push_str(&format!("onPointer('{}');", it.event()));
234        }
235      }
236    }
237
238    let origin = self
239      .check_origin
240      .as_deref()
241      .map(|it| format!("const ORIGIN='{it}';"))
242      .unwrap_or_else(|| "const ORIGIN=null;".to_owned());
243
244    include_str!("../scripts/script.js")
245      .trim()
246      .replace("/*ORIGIN*/", &origin)
247      .replace("/*SCRIPT*/", &script)
248      .into()
249  }
250
251  fn add_keyboard_shortcuts(&mut self) {
252    use shortcut::ModifierKey::{CtrlKey, ShiftKey};
253
254    macro_rules! on_key {
255      ($($arg:literal)+) => {
256        $(
257          let shortcut = KeyboardShortcut::new($arg);
258          self.shortcuts.push(Box::new(shortcut));
259        )*
260      };
261      ($modifiers:expr, $($arg:literal),+) => {
262        $(
263          let shortcut = KeyboardShortcut::with_modifiers($arg, $modifiers);
264          self.shortcuts.push(Box::new(shortcut));
265        )*
266      };
267    }
268
269    if self.flags.contains(Flags::FIND) {
270      on_key!("F3");
271      on_key!(&[CtrlKey], "f", "g");
272      on_key!(&[CtrlKey, ShiftKey], "g");
273    }
274
275    if self.flags.contains(Flags::CARET_BROWSING) {
276      on_key!("F7");
277    }
278
279    if self.flags.contains(Flags::DEV_TOOLS) {
280      on_key!(&[CtrlKey, ShiftKey], "i");
281    }
282
283    if self.flags.contains(Flags::DOWNLOADS) {
284      on_key!(&[CtrlKey], "j");
285    }
286
287    if self.flags.contains(Flags::FOCUS_MOVE) {
288      on_key!(&[ShiftKey], "Tab");
289    }
290
291    if self.flags.contains(Flags::RELOAD) {
292      on_key!("F5");
293      on_key!(&[CtrlKey], "F5");
294      on_key!(&[ShiftKey], "F5");
295      on_key!(&[CtrlKey], "r");
296      on_key!(&[CtrlKey, ShiftKey], "r");
297    }
298
299    if self.flags.contains(Flags::SOURCE) {
300      on_key!(&[CtrlKey], "u");
301    }
302
303    if self.flags.contains(Flags::OPEN) {
304      on_key!(&[CtrlKey], "o");
305    }
306
307    if self.flags.contains(Flags::PRINT) {
308      on_key!(&[CtrlKey], "p");
309      on_key!(&[CtrlKey, ShiftKey], "p");
310    }
311  }
312
313  fn add_pointer_shortcuts(&mut self) {
314    if self.flags.contains(Flags::CONTEXT_MENU) {
315      let shortcut = PointerShortcut::new(PointerEvent::ContextMenu);
316      self.shortcuts.push(Box::new(shortcut));
317    }
318  }
319}
320
321/// Provide access to the script.
322pub trait PreventDefault<R: Runtime> {
323  /// Retrieve the script.
324  ///
325  /// # Panics
326  ///
327  /// Panics if the plugin was not [built with manual injection](Builder::build_with_manual_injection).
328  fn script(&self) -> Script;
329
330  /// Attempt to retrieve the script.
331  ///
332  /// Returns `Some` if the plugin was [built with manual injection](Builder::build_with_manual_injection).
333  /// Otherwise returns `None`.
334  fn try_script(&self) -> Option<Script>;
335}
336
337impl<R, T> PreventDefault<R> for T
338where
339  R: Runtime,
340  T: Manager<R>,
341{
342  fn script(&self) -> Script {
343    (*self.app_handle().state::<Script>()).clone()
344  }
345
346  fn try_script(&self) -> Option<Script> {
347    self
348      .app_handle()
349      .try_state::<Script>()
350      .as_deref()
351      .cloned()
352  }
353}
354
355/// Initialize the plugin with default values.
356pub fn init<R: Runtime>() -> TauriPlugin<R> {
357  Builder::default().build()
358}
359
360/// Initialize the plugin with default values while also allowing for manual injection.
361pub fn init_with_manual_injection<R: Runtime>() -> TauriPlugin<R> {
362  Builder::default().build_with_manual_injection()
363}
364
365/// Initialize the plugin with given flags.
366pub fn with_flags<R: Runtime>(flags: Flags) -> TauriPlugin<R> {
367  Builder::new().with_flags(flags).build()
368}
369
370/// Initialize the plugin with given flags while also allowing for manual injection.
371pub fn with_flags_and_manual_injection<R: Runtime>(flags: Flags) -> TauriPlugin<R> {
372  Builder::new()
373    .with_flags(flags)
374    .build_with_manual_injection()
375}
376
377/// Initialize the plugin with the default [debug flags](Flags::debug).
378pub fn debug<R: Runtime>() -> TauriPlugin<R> {
379  Builder::new().with_flags(Flags::debug()).build()
380}
381
382/// Initialize the plugin with the default [debug flags](Flags::debug)
383/// while also allowing for manual injection.
384pub fn debug_with_manual_injection<R: Runtime>() -> TauriPlugin<R> {
385  Builder::new()
386    .with_flags(Flags::debug())
387    .build_with_manual_injection()
388}