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