#![forbid(unsafe_code)]
#![cfg(not(any(target_os = "android", target_os = "ios")))]
mod command;
mod display;
mod error;
mod event;
pub mod listener;
pub mod shortcut;
mod state;
use bitflags::bitflags;
pub use error::Error;
pub use event::EmitPolicy;
use event::EventEmitter;
use listener::EventListener;
pub use shortcut::{
KeyboardShortcut, KeyboardShortcutBuilder, ModifierKey, PointerEvent, PointerShortcut,
PointerShortcutBuilder, Shortcut, ShortcutKind,
};
use state::PluginState;
use tauri::plugin::TauriPlugin;
use tauri::{Manager, Runtime, Window};
#[cfg(feature = "ahash")]
use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
#[cfg(not(feature = "ahash"))]
use std::collections::{HashMap, HashSet};
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Flags: u32 {
const FIND = 1 << 0;
const CARET_BROWSING = 1 << 1;
const DEV_TOOLS = 1 << 2;
const DOWNLOADS = 1 << 3;
const FOCUS_MOVE = 1 << 4;
const RELOAD = 1 << 5;
const SOURCE = 1 << 6;
const OPEN = 1 << 7;
const PRINT = 1 << 8;
const CONTEXT_MENU = 1 << 9;
}
}
impl Flags {
pub fn keyboard() -> Self {
Self::all().difference(Self::pointer())
}
pub fn pointer() -> Self {
Self::CONTEXT_MENU
}
}
impl Default for Flags {
fn default() -> Self {
Self::all()
}
}
pub struct Builder<R: Runtime> {
flags: Flags,
flag_listeners: HashMap<Flags, HashSet<EventListener<R>>>,
shortcuts: Vec<Box<dyn Shortcut<R>>>,
check_origin: Option<String>,
emit_policy: EmitPolicy,
}
impl<R: Runtime> Default for Builder<R> {
fn default() -> Self {
Self {
flags: Flags::default(),
flag_listeners: HashMap::new(),
shortcuts: Vec::new(),
check_origin: None,
emit_policy: EmitPolicy::default(),
}
}
}
impl<R: Runtime> Builder<R> {
pub fn new() -> Self {
Self::default()
}
pub fn with_flags(mut self, flags: Flags) -> Self {
self.flags = flags;
self
}
pub fn on_flag_event<F>(mut self, flag: Flags, listener: F) -> Self
where
F: Fn(&Window<R>) + Send + Sync + 'static,
{
let listener = EventListener::new(listener);
if let Some(listeners) = self.flag_listeners.get_mut(&flag) {
listeners.insert(listener);
} else {
let mut set = HashSet::new();
set.insert(listener);
self.flag_listeners.insert(flag, set);
}
self
}
pub fn shortcut<S>(mut self, shortcut: S) -> Self
where
S: Shortcut<R> + 'static,
{
self.shortcuts.push(Box::new(shortcut));
self
}
pub fn check_origin(mut self, origin: impl AsRef<str>) -> Self {
self.check_origin = origin.as_ref().to_owned().into();
self
}
pub fn emit_policy(mut self, policy: EmitPolicy) -> Self {
self.emit_policy = policy;
self
}
pub fn build(mut self) -> TauriPlugin<R> {
self.add_keyboard_shortcuts();
self.add_pointer_shortcuts();
let mut script = String::new();
let mut state = PluginState::<R> {
emitter: EventEmitter(self.emit_policy),
listeners: HashMap::new(),
};
for shortcut in &mut self.shortcuts {
match shortcut.downcast_ref() {
ShortcutKind::Keyboard(it) => {
let modifiers = it.modifiers();
let mut options = String::with_capacity(modifiers.len() * 12);
for modifier in modifiers {
match modifier {
ModifierKey::AltKey => options.push_str("altKey:true,"),
ModifierKey::CtrlKey => options.push_str("ctrlKey:true,"),
ModifierKey::ShiftKey => options.push_str("shiftKey:true,"),
}
}
let options = options.trim_end_matches(',');
script.push_str(&format!("onKey('{}',{{{}}});", it.key(), options));
}
ShortcutKind::Pointer(it) => match it.event() {
PointerEvent::ContextMenu => script.push_str("onPointer('contextmenu');"),
},
}
let listeners = shortcut.take_listeners();
if !listeners.is_empty() {
let shortcut = shortcut.to_string();
if let Some(it) = state.listeners.get_mut(&shortcut) {
it.extend(listeners);
} else {
#[cfg(feature = "ahash")]
let set = {
let mut set = HashSet::new();
set.extend(listeners);
set
};
#[cfg(not(feature = "ahash"))]
let set = HashSet::from_iter(listeners);
state.listeners.insert(shortcut, set);
}
}
}
let origin = self
.check_origin
.map(|it| format!("const ORIGIN='{it}';"))
.unwrap_or_else(|| "const ORIGIN=null;".to_owned());
let mut script = include_str!("../scripts/script.js")
.trim()
.replace("/*ORIGIN*/", &origin)
.replace("/*SCRIPT*/", &script);
if state.emitter.0.is_none() {
script = script.replace("/*EMIT*/", "const EMIT=false;");
} else {
script = script.replace("/*EMIT*/", "const EMIT=true;");
}
#[cfg(feature = "tracing")]
tracing::trace!(script);
tauri::plugin::Builder::new("prevent-default")
.js_init_script(script)
.invoke_handler(tauri::generate_handler![
command::keyboard,
command::pointer
])
.setup(|app, _| {
app.manage(state);
Ok(())
})
.build()
}
fn add_keyboard_shortcuts(&mut self) {
use shortcut::ModifierKey::{CtrlKey, ShiftKey};
macro_rules! on_key {
($flag:ident, $($arg:literal)+) => {
$(
let mut shortcut = KeyboardShortcut::new($arg);
self.set_flag_listeners(Flags::$flag, &mut shortcut);
self.shortcuts.push(Box::new(shortcut));
)*
};
($flag:ident, $modifiers:expr, $($arg:literal),+) => {
$(
let mut shortcut = KeyboardShortcut::with_modifiers($arg, $modifiers);
self.set_flag_listeners(Flags::$flag, &mut shortcut);
self.shortcuts.push(Box::new(shortcut));
)*
};
}
if self.flags.contains(Flags::FIND) {
on_key!(FIND, "F3");
on_key!(FIND, &[CtrlKey], "f", "g");
on_key!(FIND, &[CtrlKey, ShiftKey], "g");
}
if self.flags.contains(Flags::CARET_BROWSING) {
on_key!(CARET_BROWSING, "F7");
}
if self.flags.contains(Flags::DEV_TOOLS) {
on_key!(DEV_TOOLS, &[CtrlKey, ShiftKey], "i");
}
if self.flags.contains(Flags::DOWNLOADS) {
on_key!(DOWNLOADS, &[CtrlKey], "j");
}
if self.flags.contains(Flags::FOCUS_MOVE) {
on_key!(FOCUS_MOVE, &[ShiftKey], "Tab");
}
if self.flags.contains(Flags::RELOAD) {
on_key!(RELOAD, "F5");
on_key!(RELOAD, &[CtrlKey], "F5");
on_key!(RELOAD, &[ShiftKey], "F5");
on_key!(RELOAD, &[CtrlKey], "r");
on_key!(RELOAD, &[CtrlKey, ShiftKey], "r");
}
if self.flags.contains(Flags::SOURCE) {
on_key!(SOURCE, &[CtrlKey], "u");
}
if self.flags.contains(Flags::OPEN) {
on_key!(OPEN, &[CtrlKey], "o");
}
if self.flags.contains(Flags::PRINT) {
on_key!(PRINT, &[CtrlKey], "p");
on_key!(PRINT, &[CtrlKey, ShiftKey], "p");
}
}
fn add_pointer_shortcuts(&mut self) {
if self.flags.contains(Flags::CONTEXT_MENU) {
let mut shortcut = PointerShortcut::new(PointerEvent::ContextMenu);
self.set_flag_listeners(Flags::CONTEXT_MENU, &mut shortcut);
self.shortcuts.push(Box::new(shortcut));
}
}
fn set_flag_listeners(&self, flag: Flags, shortcut: &mut dyn Shortcut<R>) {
let mut listeners = Vec::new();
for (flags, set) in &self.flag_listeners {
if flags.contains(flag) {
listeners.extend(set.iter().cloned());
}
}
if !listeners.is_empty() {
shortcut.add_listeners(&listeners);
}
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::default().build()
}
pub fn with_flags<R: Runtime>(flags: Flags) -> TauriPlugin<R> {
Builder::new().with_flags(flags).build()
}