Skip to main content

repose_core/
shortcuts.rs

1use crate::Vec2;
2use crate::effects::{Dispose, on_unmount};
3use crate::input::{Key, Modifiers, PointerKind};
4use std::cell::RefCell;
5use std::rc::Rc;
6
7#[derive(Clone, Debug, PartialEq)]
8pub enum Gesture {
9    SwipeLeft,
10    SwipeRight,
11    /// delta_scale > 1 => zoom in; < 1 => zoom out
12    Pinch {
13        delta_scale: f32,
14    },
15}
16
17/// Low-level drag-and-drop actions dispatched by the platform.
18/// The framework handles gesture detection (mouse drag vs touch long press)
19/// and the DnD state machine internally.
20#[derive(Clone, Debug, PartialEq)]
21pub enum DragAction {
22    /// Pointer button pressed (mouse down / touch start).
23    Press {
24        position: Vec2,
25        capture_id: u64,
26        kind: PointerKind,
27        modifiers: Modifiers,
28    },
29    /// Pointer moved while button is pressed or touch is active.
30    Move {
31        position: Vec2,
32        modifiers: Modifiers,
33    },
34    /// Pointer released.
35    Release {
36        position: Vec2,
37        modifiers: Modifiers,
38    },
39    /// Drag cancelled (e.g. Escape key).
40    Cancel,
41}
42
43#[derive(Clone, Debug, PartialEq)]
44pub enum Action {
45    Copy,
46    Cut,
47    Paste,
48    SelectAll,
49    Undo,
50    Redo,
51
52    Back,
53    Find,
54    Save,
55
56    FocusNext,
57    FocusPrevious,
58    FocusLeft,
59    FocusRight,
60    FocusUp,
61    FocusDown,
62
63    Gesture(Gesture),
64    Drag(DragAction),
65    Custom(Rc<str>),
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Hash)]
69pub struct KeyChord {
70    pub key: Key,
71    pub modifiers: Modifiers,
72}
73
74impl KeyChord {
75    pub fn new(key: Key, modifiers: Modifiers) -> Self {
76        Self { key, modifiers }
77    }
78}
79
80#[derive(Clone, Debug)]
81pub struct ShortcutBinding {
82    pub chord: KeyChord,
83    pub action: Action,
84}
85
86#[derive(Clone, Debug, Default)]
87pub struct ShortcutMap {
88    pub bindings: Vec<ShortcutBinding>,
89}
90
91impl ShortcutMap {
92    pub fn new() -> Self {
93        Self {
94            bindings: Vec::new(),
95        }
96    }
97
98    pub fn bind(mut self, key: Key, modifiers: Modifiers, action: Action) -> Self {
99        self.bindings.push(ShortcutBinding {
100            chord: KeyChord::new(key, modifiers),
101            action,
102        });
103        self
104    }
105
106    pub fn bind_action(mut self, action: Action) -> Self {
107        if let Some(chord) = default_chord_for(&action) {
108            self.bindings.push(ShortcutBinding { chord, action });
109        }
110        self
111    }
112
113    pub fn merge(mut self, other: ShortcutMap) -> Self {
114        self.bindings.extend(other.bindings);
115        self
116    }
117
118    pub fn insert(&mut self, key: Key, modifiers: Modifiers, action: Action) {
119        self.bindings.push(ShortcutBinding {
120            chord: KeyChord::new(key, modifiers),
121            action,
122        });
123    }
124
125    pub fn action_for(&self, chord: &KeyChord) -> Option<Action> {
126        self.bindings
127            .iter()
128            .rev()
129            .find(|binding| &binding.chord == chord)
130            .map(|binding| binding.action.clone())
131    }
132}
133
134pub type Handler = Rc<dyn Fn(Action) -> bool>;
135
136thread_local! {
137    static HANDLER: RefCell<Option<Handler>> = RefCell::new(None);
138    static DEFAULT_MAP: RefCell<ShortcutMap> = RefCell::new(default_map());
139    static SCOPES: RefCell<Vec<ShortcutMap>> = const { RefCell::new(Vec::new()) };
140}
141
142/// Set/clear the global handler (prefer InstallShortcutHandler + scoped_effect).
143pub fn set(handler: Option<Handler>) {
144    HANDLER.with(|h| *h.borrow_mut() = handler);
145}
146
147/// Dispatch an action to the global handler. Returns true if consumed.
148pub fn handle(action: Action) -> bool {
149    HANDLER.with(|h| h.borrow().as_ref().map(|f| f(action)).unwrap_or(false))
150}
151
152/// Resolve a key chord to an action using scoped + default maps.
153pub fn resolve_action(chord: KeyChord) -> Option<Action> {
154    if chord.key == Key::Unknown {
155        return None;
156    }
157
158    if let Some(action) = SCOPES.with(|scopes| {
159        scopes
160            .borrow()
161            .iter()
162            .rev()
163            .find_map(|scope| scope.action_for(&chord))
164    }) {
165        return Some(action);
166    }
167
168    DEFAULT_MAP.with(|m| m.borrow().action_for(&chord))
169}
170
171/// Replace the default shortcut map used by resolve_action.
172pub fn set_default_map(map: ShortcutMap) {
173    DEFAULT_MAP.with(|m| *m.borrow_mut() = map);
174}
175
176/// Push a shortcut map for the current scope, popped on unmount.
177#[allow(non_snake_case)]
178pub fn InstallShortcutMap(map: ShortcutMap) -> Dispose {
179    SCOPES.with(|scopes| scopes.borrow_mut().push(map));
180    on_unmount(|| {
181        let _ = SCOPES.try_with(|scopes| {
182            scopes.borrow_mut().pop();
183        });
184    })
185}
186
187/// Install/uninstall a global shortcut handler for the current scope.
188/// Restores the previous handler on unmount (supports nesting).
189#[allow(non_snake_case)]
190pub fn InstallShortcutHandler(handler: Handler) -> Dispose {
191    let prev = HANDLER.with(|h| h.borrow_mut().replace(handler));
192    on_unmount(move || {
193        let _ = HANDLER.try_with(|h| *h.borrow_mut() = prev);
194    })
195}
196
197pub fn default_chord_for(action: &Action) -> Option<KeyChord> {
198    // On non-macOS, sets ctrl true
199    let cmd = Modifiers {
200        command: true,
201        ctrl: !cfg!(target_os = "macos"),
202        ..Modifiers::default()
203    };
204    match action {
205        Action::Copy => Some(KeyChord::new(Key::Character('c'), cmd)),
206        Action::Cut => Some(KeyChord::new(Key::Character('x'), cmd)),
207        Action::Paste => Some(KeyChord::new(Key::Character('v'), cmd)),
208        Action::SelectAll => Some(KeyChord::new(Key::Character('a'), cmd)),
209        Action::Undo => Some(KeyChord::new(Key::Character('z'), cmd)),
210        Action::Redo => Some(KeyChord::new(
211            Key::Character('z'),
212            Modifiers {
213                command: true,
214                shift: true,
215                ctrl: !cfg!(target_os = "macos"),
216                ..Modifiers::default()
217            },
218        )),
219        Action::Find => Some(KeyChord::new(Key::Character('f'), cmd)),
220        Action::Save => Some(KeyChord::new(Key::Character('s'), cmd)),
221        Action::FocusNext => Some(KeyChord::new(Key::Tab, Modifiers::default())),
222        Action::FocusPrevious => Some(KeyChord::new(
223            Key::Tab,
224            Modifiers {
225                shift: true,
226                ..Modifiers::default()
227            },
228        )),
229        Action::FocusLeft => Some(KeyChord::new(Key::ArrowLeft, Modifiers::default())),
230        Action::FocusRight => Some(KeyChord::new(Key::ArrowRight, Modifiers::default())),
231        Action::FocusUp => Some(KeyChord::new(Key::ArrowUp, Modifiers::default())),
232        Action::FocusDown => Some(KeyChord::new(Key::ArrowDown, Modifiers::default())),
233        _ => None,
234    }
235}
236
237pub fn default_map() -> ShortcutMap {
238    let mut map = ShortcutMap::new();
239    let mut actions = vec![
240        Action::Copy,
241        Action::Cut,
242        Action::Paste,
243        Action::SelectAll,
244        Action::Undo,
245        Action::Redo,
246        Action::Find,
247        Action::Save,
248        Action::FocusNext,
249        Action::FocusPrevious,
250        Action::FocusLeft,
251        Action::FocusRight,
252        Action::FocusUp,
253        Action::FocusDown,
254    ];
255    for action in actions {
256        if let Some(chord) = default_chord_for(&action) {
257            map.insert(chord.key, chord.modifiers, action);
258        }
259    }
260    map
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn resolve_action_prefers_scopes() {
269        let mut map = ShortcutMap::new();
270        map.insert(
271            Key::Character('k'),
272            Modifiers::default(),
273            Action::Custom("one".into()),
274        );
275        set_default_map(map);
276
277        let mut scope = ShortcutMap::new();
278        scope.insert(
279            Key::Character('k'),
280            Modifiers::default(),
281            Action::Custom("two".into()),
282        );
283
284        SCOPES.with(|scopes| scopes.borrow_mut().push(scope));
285
286        let chord = KeyChord::new(Key::Character('k'), Modifiers::default());
287        assert_eq!(
288            resolve_action(chord.clone()),
289            Some(Action::Custom("two".into()))
290        );
291
292        SCOPES.with(|scopes| scopes.borrow_mut().pop());
293        assert_eq!(resolve_action(chord), Some(Action::Custom("one".into())));
294    }
295}