use crate::Vec2;
use crate::effects::{Dispose, on_unmount};
use crate::input::{Key, Modifiers, PointerKind};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq)]
pub enum Gesture {
SwipeLeft,
SwipeRight,
Pinch {
delta_scale: f32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum DragAction {
Press {
position: Vec2,
capture_id: u64,
kind: PointerKind,
modifiers: Modifiers,
},
Move {
position: Vec2,
modifiers: Modifiers,
},
Release {
position: Vec2,
modifiers: Modifiers,
},
Cancel,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Action {
Copy,
Cut,
Paste,
SelectAll,
Undo,
Redo,
Back,
Find,
Save,
FocusNext,
FocusPrevious,
FocusLeft,
FocusRight,
FocusUp,
FocusDown,
Gesture(Gesture),
Drag(DragAction),
Custom(Rc<str>),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct KeyChord {
pub key: Key,
pub modifiers: Modifiers,
}
impl KeyChord {
pub fn new(key: Key, modifiers: Modifiers) -> Self {
Self { key, modifiers }
}
}
#[derive(Clone, Debug)]
pub struct ShortcutBinding {
pub chord: KeyChord,
pub action: Action,
}
#[derive(Clone, Debug, Default)]
pub struct ShortcutMap {
pub bindings: Vec<ShortcutBinding>,
}
impl ShortcutMap {
pub fn new() -> Self {
Self {
bindings: Vec::new(),
}
}
pub fn bind(mut self, key: Key, modifiers: Modifiers, action: Action) -> Self {
self.bindings.push(ShortcutBinding {
chord: KeyChord::new(key, modifiers),
action,
});
self
}
pub fn bind_action(mut self, action: Action) -> Self {
if let Some(chord) = default_chord_for(&action) {
self.bindings.push(ShortcutBinding { chord, action });
}
self
}
pub fn merge(mut self, other: ShortcutMap) -> Self {
self.bindings.extend(other.bindings);
self
}
pub fn insert(&mut self, key: Key, modifiers: Modifiers, action: Action) {
self.bindings.push(ShortcutBinding {
chord: KeyChord::new(key, modifiers),
action,
});
}
pub fn action_for(&self, chord: &KeyChord) -> Option<Action> {
self.bindings
.iter()
.rev()
.find(|binding| &binding.chord == chord)
.map(|binding| binding.action.clone())
}
}
pub type Handler = Rc<dyn Fn(Action) -> bool>;
thread_local! {
static HANDLER: RefCell<Option<Handler>> = RefCell::new(None);
static DEFAULT_MAP: RefCell<ShortcutMap> = RefCell::new(default_map());
static SCOPES: RefCell<Vec<ShortcutMap>> = const { RefCell::new(Vec::new()) };
}
pub fn set(handler: Option<Handler>) {
HANDLER.with(|h| *h.borrow_mut() = handler);
}
pub fn handle(action: Action) -> bool {
HANDLER.with(|h| h.borrow().as_ref().map(|f| f(action)).unwrap_or(false))
}
pub fn resolve_action(chord: KeyChord) -> Option<Action> {
if chord.key == Key::Unknown {
return None;
}
if let Some(action) = SCOPES.with(|scopes| {
scopes
.borrow()
.iter()
.rev()
.find_map(|scope| scope.action_for(&chord))
}) {
return Some(action);
}
DEFAULT_MAP.with(|m| m.borrow().action_for(&chord))
}
pub fn set_default_map(map: ShortcutMap) {
DEFAULT_MAP.with(|m| *m.borrow_mut() = map);
}
#[allow(non_snake_case)]
pub fn InstallShortcutMap(map: ShortcutMap) -> Dispose {
SCOPES.with(|scopes| scopes.borrow_mut().push(map));
on_unmount(|| {
let _ = SCOPES.try_with(|scopes| {
scopes.borrow_mut().pop();
});
})
}
#[allow(non_snake_case)]
pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
let prev = HANDLER.with(|h| h.borrow_mut().replace(handler));
on_unmount(move || {
let _ = HANDLER.try_with(|h| *h.borrow_mut() = prev);
})
}
pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
let cmd = Modifiers {
command: true,
ctrl: !cfg!(target_os = "macos"),
..Modifiers::default()
};
match action {
Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
Action::Redo => Some(KeyChord::new(
Key::Character('z'),
Modifiers {
command: true,
shift: true,
ctrl: !cfg!(target_os = "macos"),
..Modifiers::default()
},
)),
Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
Action::FocusNext => Some(KeyChord::new(Key::Tab, Modifiers::default())),
Action::FocusPrevious => Some(KeyChord::new(
Key::Tab,
Modifiers {
shift: true,
..Modifiers::default()
},
)),
Action::FocusLeft => Some(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
Action::FocusRight => Some(KeyChord::new(Key::ArrowRight, Modifiers::default())),
Action::FocusUp => Some(KeyChord::new(Key::ArrowUp, Modifiers::default())),
Action::FocusDown => Some(KeyChord::new(Key::ArrowDown, Modifiers::default())),
_ => None,
}
}
pub fn default_map() -> ShortcutMap {
let mut map = ShortcutMap::new();
let mut actions = vec![
Action::Copy,
Action::Cut,
Action::Paste,
Action::SelectAll,
Action::Undo,
Action::Redo,
Action::Find,
Action::Save,
Action::FocusNext,
Action::FocusPrevious,
Action::FocusLeft,
Action::FocusRight,
Action::FocusUp,
Action::FocusDown,
];
for action in actions {
if let Some(chord) = default_chord_for(&action) {
map.insert(chord.key, chord.modifiers, action);
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_action_prefers_scopes() {
let mut map = ShortcutMap::new();
map.insert(
Key::Character('k'),
Modifiers::default(),
Action::Custom("one".into()),
);
set_default_map(map);
let mut scope = ShortcutMap::new();
scope.insert(
Key::Character('k'),
Modifiers::default(),
Action::Custom("two".into()),
);
SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
assert_eq!(
resolve_action(chord.clone()),
Some(Action::Custom("two".into()))
);
SCOPES.with(|scopes| scopes.borrow_mut().pop());
assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
}
}