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/// Restores the previous handler on unmount (supports nesting).
154#[allow(non_snake_case)]
155pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
156    let prev = HANDLER.with(|h| h.borrow_mut().replace(handler));
157    on_unmount(move || {
158        HANDLER.with(|h| *h.borrow_mut() = prev);
159    })
160}
161
162pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
163    let cmd = Modifiers {
164        command: true,
165        ..Modifiers::default()
166    };
167    match action {
168        Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
169        Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
170        Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
171        Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
172        Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
173        Action::Redo => Some(KeyChord::new(
174            Key::Character('z'),
175            Modifiers {
176                command: true,
177                shift: true,
178                ..Modifiers::default()
179            },
180        )),
181        Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
182        Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
183        _ => None,
184    }
185}
186
187pub fn default_map() -> ShortcutMap {
188    let mut map = ShortcutMap::new();
189    let actions = [
190        Action::Copy,
191        Action::Cut,
192        Action::Paste,
193        Action::SelectAll,
194        Action::Undo,
195        Action::Redo,
196        Action::Find,
197        Action::Save,
198    ];
199    for action in actions {
200        if let Some(chord) = default_chord_for(&action) {
201            map.insert(chord.key, chord.modifiers, action);
202        }
203    }
204    map
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn resolve_action_prefers_scopes() {
213        let mut map = ShortcutMap::new();
214        map.insert(
215            Key::Character('k'),
216            Modifiers::default(),
217            Action::Custom("one".into()),
218        );
219        set_default_map(map);
220
221        let mut scope = ShortcutMap::new();
222        scope.insert(
223            Key::Character('k'),
224            Modifiers::default(),
225            Action::Custom("two".into()),
226        );
227
228        SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
229
230        let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
231        assert_eq!(
232            resolve_action(chord.clone()),
233            Some(Action::Custom("two".into()))
234        );
235
236        SCOPES.with(|scopes| scopes.borrow_mut().pop());
237        assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
238    }
239}