Skip to main content

repose_core/
shortcuts.rs

1use crate::effects::{Dispose, on_unmount};
2use crate::input::{Key, Modifiers};
3use std::cell::RefCell;
4use std::rc::Rc;
5
6#[derive(Clone, Debug, PartialEq)]
7pub enum Gesture {
8    SwipeLeft,
9    SwipeRight,
10    /// delta_scale > 1 => zoom in; < 1 => zoom out
11    Pinch {
12        delta_scale: f32,
13    },
14}
15
16#[derive(Clone, Debug, PartialEq)]
17pub enum Action {
18    Copy,
19    Cut,
20    Paste,
21    SelectAll,
22    Undo,
23    Redo,
24
25    Back,
26    Find,
27    Save,
28
29    Gesture(Gesture),
30    Custom(Rc<str>),
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub struct KeyChord {
35    pub key: Key,
36    pub modifiers: Modifiers,
37}
38
39impl KeyChord {
40    pub fn new(key: Key, modifiers: Modifiers) -> Self {
41        Self { key, modifiers }
42    }
43}
44
45#[derive(Clone, Debug)]
46pub struct ShortcutBinding {
47    pub chord: KeyChord,
48    pub action: Action,
49}
50
51#[derive(Clone, Debug, Default)]
52pub struct ShortcutMap {
53    pub bindings: Vec<ShortcutBinding>,
54}
55
56impl ShortcutMap {
57    pub fn new() -> Self {
58        Self {
59            bindings: Vec::new(),
60        }
61    }
62
63    pub fn bind(mut self, key: Key, modifiers: Modifiers, action: Action) -> Self {
64        self.bindings.push(ShortcutBinding {
65            chord: KeyChord::new(key, modifiers),
66            action,
67        });
68        self
69    }
70
71    pub fn bind_action(mut self, action: Action) -> Self {
72        if let Some(chord) = default_chord_for(&action) {
73            self.bindings.push(ShortcutBinding { chord, action });
74        }
75        self
76    }
77
78    pub fn merge(mut self, other: ShortcutMap) -> Self {
79        self.bindings.extend(other.bindings);
80        self
81    }
82
83    pub fn insert(&mut self, key: Key, modifiers: Modifiers, action: Action) {
84        self.bindings.push(ShortcutBinding {
85            chord: KeyChord::new(key, modifiers),
86            action,
87        });
88    }
89
90    pub fn action_for(&self, chord: &KeyChord) -> Option<Action> {
91        self.bindings
92            .iter()
93            .rev()
94            .find(|binding| &binding.chord == chord)
95            .map(|binding| binding.action.clone())
96    }
97}
98
99pub type Handler = Rc<dyn Fn(Action) -> bool>;
100
101thread_local! {
102    static HANDLER: RefCell<Option<Handler>> = RefCell::new(None);
103    static DEFAULT_MAP: RefCell<ShortcutMap> = RefCell::new(default_map());
104    static SCOPES: RefCell<Vec<ShortcutMap>> = RefCell::new(Vec::new());
105}
106
107/// Set/clear the global handler (prefer InstallShortcutHandler + scoped_effect).
108pub fn set(handler: Option<Handler>) {
109    HANDLER.with(|h| *h.borrow_mut() = handler);
110}
111
112/// Dispatch an action to the global handler. Returns true if consumed.
113pub fn handle(action: Action) -> bool {
114    HANDLER.with(|h| h.borrow().as_ref().map(|f| f(action)).unwrap_or(false))
115}
116
117/// Resolve a key chord to an action using scoped + default maps.
118pub fn resolve_action(chord: KeyChord) -> Option<Action> {
119    if chord.key == Key::Unknown {
120        return None;
121    }
122
123    if let Some(action) = SCOPES.with(|scopes| {
124        scopes
125            .borrow()
126            .iter()
127            .rev()
128            .find_map(|scope| scope.action_for(&chord))
129    }) {
130        return Some(action);
131    }
132
133    DEFAULT_MAP.with(|m| m.borrow().action_for(&chord))
134}
135
136/// Replace the default shortcut map used by resolve_action.
137pub fn set_default_map(map: ShortcutMap) {
138    DEFAULT_MAP.with(|m| *m.borrow_mut() = map);
139}
140
141/// Push a shortcut map for the current scope, popped on unmount.
142#[allow(non_snake_case)]
143pub fn InstallShortcutMap(map: ShortcutMap) -> Dispose {
144    SCOPES.with(|scopes| scopes.borrow_mut().push(map));
145    on_unmount(|| {
146        SCOPES.with(|scopes| {
147            scopes.borrow_mut().pop();
148        });
149    })
150}
151
152/// Install/uninstall a global shortcut handler for the current scope.
153#[allow(non_snake_case)]
154pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
155    set(Some(handler));
156    on_unmount(|| set(None))
157}
158
159pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
160    let cmd = Modifiers {
161        command: true,
162        ..Modifiers::default()
163    };
164    match action {
165        Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
166        Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
167        Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
168        Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
169        Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
170        Action::Redo => Some(KeyChord::new(
171            Key::Character('z'),
172            Modifiers {
173                command: true,
174                shift: true,
175                ..Modifiers::default()
176            },
177        )),
178        Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
179        Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
180        _ => None,
181    }
182}
183
184pub fn default_map() -> ShortcutMap {
185    let mut map = ShortcutMap::new();
186    let actions = [
187        Action::Copy,
188        Action::Cut,
189        Action::Paste,
190        Action::SelectAll,
191        Action::Undo,
192        Action::Redo,
193        Action::Find,
194        Action::Save,
195    ];
196    for action in actions {
197        if let Some(chord) = default_chord_for(&action) {
198            map.insert(chord.key, chord.modifiers, action);
199        }
200    }
201    map
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn resolve_action_prefers_scopes() {
210        let mut map = ShortcutMap::new();
211        map.insert(
212            Key::Character('k'),
213            Modifiers::default(),
214            Action::Custom("one".into()),
215        );
216        set_default_map(map);
217
218        let mut scope = ShortcutMap::new();
219        scope.insert(
220            Key::Character('k'),
221            Modifiers::default(),
222            Action::Custom("two".into()),
223        );
224
225        SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
226
227        let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
228        assert_eq!(
229            resolve_action(chord.clone()),
230            Some(Action::Custom("two".into()))
231        );
232
233        SCOPES.with(|scopes| scopes.borrow_mut().pop());
234        assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
235    }
236}