1use std::collections::BTreeMap;
8
9use egui::{Key, KeyboardShortcut, Modifiers};
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
16pub enum Action {
17 Copy,
19 Cut,
20 Paste,
21 SelectAll,
22 Undo,
23 Redo,
24 Find,
25 Save,
26 Confirm,
27 Cancel,
28 Left,
30 Right,
31 Up,
32 Down,
33 WordLeft,
34 WordRight,
35 LineStart,
36 LineEnd,
37 DocStart,
38 DocEnd,
39 PageUp,
40 PageDown,
41 ExtendLeft,
43 ExtendRight,
44 ExtendUp,
45 ExtendDown,
46 ZoomIn,
48 ZoomOut,
49 FitToView,
50 FocusNext,
52 FocusPrev,
53 FocusLeft,
54 FocusRight,
55 FocusUp,
56 FocusDown,
57 FocusHints,
59}
60
61impl Action {
62 pub const ALL: &'static [Action] = &[
64 Action::Copy,
65 Action::Cut,
66 Action::Paste,
67 Action::SelectAll,
68 Action::Undo,
69 Action::Redo,
70 Action::Find,
71 Action::Save,
72 Action::Confirm,
73 Action::Cancel,
74 Action::Left,
75 Action::Right,
76 Action::Up,
77 Action::Down,
78 Action::WordLeft,
79 Action::WordRight,
80 Action::LineStart,
81 Action::LineEnd,
82 Action::DocStart,
83 Action::DocEnd,
84 Action::PageUp,
85 Action::PageDown,
86 Action::ExtendLeft,
87 Action::ExtendRight,
88 Action::ExtendUp,
89 Action::ExtendDown,
90 Action::ZoomIn,
91 Action::ZoomOut,
92 Action::FitToView,
93 Action::FocusNext,
94 Action::FocusPrev,
95 Action::FocusLeft,
96 Action::FocusRight,
97 Action::FocusUp,
98 Action::FocusDown,
99 Action::FocusHints,
100 ];
101
102 pub fn as_str(self) -> &'static str {
103 match self {
104 Action::Copy => "Copy",
105 Action::Cut => "Cut",
106 Action::Paste => "Paste",
107 Action::SelectAll => "SelectAll",
108 Action::Undo => "Undo",
109 Action::Redo => "Redo",
110 Action::Find => "Find",
111 Action::Save => "Save",
112 Action::Confirm => "Confirm",
113 Action::Cancel => "Cancel",
114 Action::Left => "Left",
115 Action::Right => "Right",
116 Action::Up => "Up",
117 Action::Down => "Down",
118 Action::WordLeft => "WordLeft",
119 Action::WordRight => "WordRight",
120 Action::LineStart => "LineStart",
121 Action::LineEnd => "LineEnd",
122 Action::DocStart => "DocStart",
123 Action::DocEnd => "DocEnd",
124 Action::PageUp => "PageUp",
125 Action::PageDown => "PageDown",
126 Action::ExtendLeft => "ExtendLeft",
127 Action::ExtendRight => "ExtendRight",
128 Action::ExtendUp => "ExtendUp",
129 Action::ExtendDown => "ExtendDown",
130 Action::ZoomIn => "ZoomIn",
131 Action::ZoomOut => "ZoomOut",
132 Action::FitToView => "FitToView",
133 Action::FocusNext => "FocusNext",
134 Action::FocusPrev => "FocusPrev",
135 Action::FocusLeft => "FocusLeft",
136 Action::FocusRight => "FocusRight",
137 Action::FocusUp => "FocusUp",
138 Action::FocusDown => "FocusDown",
139 Action::FocusHints => "FocusHints",
140 }
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148pub struct KeyMap {
149 pub bindings: BTreeMap<Action, KeyboardShortcut>,
150}
151
152fn cmd(k: Key) -> KeyboardShortcut {
154 KeyboardShortcut::new(Modifiers::COMMAND, k)
155}
156fn cmd_shift(k: Key) -> KeyboardShortcut {
158 KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, k)
159}
160fn bare(k: Key) -> KeyboardShortcut {
162 KeyboardShortcut::new(Modifiers::NONE, k)
163}
164fn with(m: Modifiers, k: Key) -> KeyboardShortcut {
165 KeyboardShortcut::new(m, k)
166}
167
168impl KeyMap {
169 fn common() -> BTreeMap<Action, KeyboardShortcut> {
172 let mut m = BTreeMap::new();
173 m.insert(Action::Copy, cmd(Key::C));
174 m.insert(Action::Cut, cmd(Key::X));
175 m.insert(Action::Paste, cmd(Key::V));
176 m.insert(Action::SelectAll, cmd(Key::A));
177 m.insert(Action::Undo, cmd(Key::Z));
178 m.insert(Action::Redo, cmd_shift(Key::Z));
179 m.insert(Action::Find, cmd(Key::F));
180 m.insert(Action::Save, cmd(Key::S));
181 m.insert(Action::Confirm, bare(Key::Enter));
182 m.insert(Action::Cancel, bare(Key::Escape));
183 m.insert(Action::Left, bare(Key::ArrowLeft));
184 m.insert(Action::Right, bare(Key::ArrowRight));
185 m.insert(Action::Up, bare(Key::ArrowUp));
186 m.insert(Action::Down, bare(Key::ArrowDown));
187 m.insert(Action::PageUp, bare(Key::PageUp));
188 m.insert(Action::PageDown, bare(Key::PageDown));
189 m.insert(Action::ExtendLeft, with(Modifiers::SHIFT, Key::ArrowLeft));
190 m.insert(Action::ExtendRight, with(Modifiers::SHIFT, Key::ArrowRight));
191 m.insert(Action::ExtendUp, with(Modifiers::SHIFT, Key::ArrowUp));
192 m.insert(Action::ExtendDown, with(Modifiers::SHIFT, Key::ArrowDown));
193 m.insert(Action::ZoomIn, cmd(Key::Plus));
194 m.insert(Action::ZoomOut, cmd(Key::Minus));
195 m.insert(Action::FitToView, cmd(Key::Num0));
196 m.insert(Action::FocusNext, with(Modifiers::CTRL, Key::Tab));
197 m.insert(Action::FocusPrev, with(Modifiers::CTRL | Modifiers::SHIFT, Key::Tab));
198 m.insert(Action::FocusHints, cmd_shift(Key::Space));
199 m
200 }
201
202 pub fn windows() -> Self {
205 let mut m = Self::common();
206 m.insert(Action::WordLeft, with(Modifiers::CTRL, Key::ArrowLeft));
207 m.insert(Action::WordRight, with(Modifiers::CTRL, Key::ArrowRight));
208 m.insert(Action::LineStart, bare(Key::Home));
209 m.insert(Action::LineEnd, bare(Key::End));
210 m.insert(Action::DocStart, with(Modifiers::CTRL, Key::Home));
211 m.insert(Action::DocEnd, with(Modifiers::CTRL, Key::End));
212 m.insert(Action::FocusRight, bare(Key::F6));
213 m.insert(Action::FocusLeft, with(Modifiers::SHIFT, Key::F6));
214 m.insert(Action::FocusUp, with(Modifiers::CTRL, Key::ArrowUp));
215 m.insert(Action::FocusDown, with(Modifiers::CTRL, Key::ArrowDown));
216 Self { bindings: m }
217 }
218
219 pub fn macos() -> Self {
221 let mut m = Self::common();
222 m.insert(Action::WordLeft, with(Modifiers::ALT, Key::ArrowLeft));
223 m.insert(Action::WordRight, with(Modifiers::ALT, Key::ArrowRight));
224 m.insert(Action::LineStart, cmd(Key::ArrowLeft));
225 m.insert(Action::LineEnd, cmd(Key::ArrowRight));
226 m.insert(Action::DocStart, cmd(Key::ArrowUp));
227 m.insert(Action::DocEnd, cmd(Key::ArrowDown));
228 m.insert(Action::FocusRight, with(Modifiers::CTRL, Key::F6));
229 m.insert(Action::FocusLeft, with(Modifiers::CTRL | Modifiers::SHIFT, Key::F6));
230 m.insert(Action::FocusUp, with(Modifiers::CTRL, Key::ArrowUp));
231 m.insert(Action::FocusDown, with(Modifiers::CTRL, Key::ArrowDown));
232 Self { bindings: m }
233 }
234
235 pub fn device() -> Self {
239 Self::windows()
241 }
242
243 pub fn shortcut(&self, action: Action) -> Option<KeyboardShortcut> {
245 self.bindings.get(&action).copied()
246 }
247
248 pub fn set(&mut self, action: Action, chord: KeyboardShortcut) {
250 self.bindings.insert(action, chord);
251 }
252
253 pub fn consume(&self, action: Action, ui: &mut egui::Ui) -> bool {
257 match self.shortcut(action) {
258 Some(sc) => ui.input_mut(|i| i.consume_shortcut(&sc)),
259 None => false,
260 }
261 }
262
263 pub fn label(&self, action: Action, ctx: &egui::Context) -> String {
265 match self.shortcut(action) {
266 Some(sc) => ctx.format_shortcut(&sc),
267 None => String::new(),
268 }
269 }
270}
271
272impl Default for KeyMap {
273 fn default() -> Self {
274 Self::windows()
275 }
276}
277
278const KEYMAP_ID: &str = "facett_keymap";
279
280pub fn publish_keymap(ctx: &egui::Context, km: KeyMap) {
284 ctx.data_mut(|d| d.insert_temp(egui::Id::new(KEYMAP_ID), km));
285}
286
287pub fn keymap(ui: &egui::Ui) -> KeyMap {
290 ui.data(|d| d.get_temp::<KeyMap>(egui::Id::new(KEYMAP_ID))).unwrap_or_default()
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn every_action_is_bound_in_every_preset() {
299 for (name, km) in [("win", KeyMap::windows()), ("mac", KeyMap::macos()), ("dev", KeyMap::device())] {
300 for &a in Action::ALL {
301 assert!(km.shortcut(a).is_some(), "{name}: action {a:?} is unbound");
302 }
303 }
304 }
305
306 #[test]
307 fn copy_is_the_same_chord_everywhere_coh2() {
308 let km = KeyMap::windows();
311 let a = km.shortcut(Action::Copy).unwrap();
312 let b = km.shortcut(Action::Copy).unwrap();
313 assert_eq!(a, b);
314 assert_eq!(a, cmd(Key::C));
315 }
316
317 #[test]
318 fn mac_and_win_differ_on_line_nav_but_share_copy() {
319 let w = KeyMap::windows();
320 let m = KeyMap::macos();
321 assert_eq!(w.shortcut(Action::Copy), m.shortcut(Action::Copy), "Copy is COMMAND on both");
322 assert_ne!(w.shortcut(Action::LineStart), m.shortcut(Action::LineStart), "line nav differs by OS");
323 assert_eq!(m.shortcut(Action::LineStart), Some(cmd(Key::ArrowLeft)));
324 assert_eq!(w.shortcut(Action::WordLeft), Some(with(Modifiers::CTRL, Key::ArrowLeft)));
325 assert_eq!(m.shortcut(Action::WordLeft), Some(with(Modifiers::ALT, Key::ArrowLeft)));
326 }
327
328 #[test]
329 fn keymap_serde_round_trips_including_a_remap() {
330 let mut km = KeyMap::windows();
331 km.set(Action::Find, cmd_shift(Key::F)); let json = serde_json::to_string(&km).unwrap();
333 let back: KeyMap = serde_json::from_str(&json).unwrap();
334 assert_eq!(km, back);
335 assert_eq!(back.shortcut(Action::Find), Some(cmd_shift(Key::F)));
336 }
337
338 #[test]
339 fn consume_fires_only_for_the_bound_chord() {
340 let km = KeyMap::windows();
341 let ctx = egui::Context::default();
342 let input = egui::RawInput {
344 events: vec![egui::Event::Key {
345 key: Key::C,
346 physical_key: None,
347 pressed: true,
348 repeat: false,
349 modifiers: Modifiers::COMMAND,
350 }],
351 modifiers: Modifiers::COMMAND,
352 ..Default::default()
353 };
354 let mut fired_copy = false;
355 let mut fired_paste = false;
356 let _ = ctx.run(input, |ctx| {
357 egui::CentralPanel::default().show(ctx, |ui| {
358 fired_copy = km.consume(Action::Copy, ui);
359 fired_paste = km.consume(Action::Paste, ui);
360 });
361 });
362 assert!(fired_copy, "Cmd+C should fire Copy");
363 assert!(!fired_paste, "Cmd+C must not fire Paste");
364 }
365}