1use egui::{Key, Modifiers};
2use std::collections::HashMap;
3use std::sync::Mutex;
4
5lazy_static::lazy_static! {
7 static ref SHORTCUT_STATES: Mutex<HashMap<String, bool>> = Mutex::new(HashMap::new());
8}
9
10#[derive(Debug, Clone)]
16pub struct KeyboardShortcut {
17 pub key: Key,
19 pub modifiers: Modifiers,
21}
22
23#[derive(Debug, Clone)]
28pub enum ShortcutParseError {
29 InvalidKey(String),
31 InvalidModifier(String),
33 InvalidFormat(String),
35}
36
37impl KeyboardShortcut {
38 pub fn new(key: Key) -> Self {
40 Self {
41 key,
42 modifiers: Modifiers::default(),
43 }
44 }
45
46 pub fn from_string(shortcut: &str) -> Result<Self, ShortcutParseError> {
56 let parts: Vec<&str> = shortcut.split('+').collect();
57
58 if parts.is_empty() {
59 return Err(ShortcutParseError::InvalidFormat(shortcut.to_string()));
60 }
61
62 let mut modifiers = Modifiers::default();
63 let key_str = parts.last().unwrap().to_lowercase();
64
65 for part in &parts[..parts.len() - 1] {
67 let modifier = part.to_lowercase();
68 match modifier.as_str() {
69 "ctrl" | "control" => modifiers.ctrl = true,
70 "alt" => modifiers.alt = true,
71 "shift" => modifiers.shift = true,
72 "cmd" | "meta" | "super" => modifiers.command = true,
73 _ => return Err(ShortcutParseError::InvalidModifier(part.to_string())),
74 }
75 }
76
77 let key = match key_str.as_str() {
79 "a" => Key::A,
81 "b" => Key::B,
82 "c" => Key::C,
83 "d" => Key::D,
84 "e" => Key::E,
85 "f" => Key::F,
86 "g" => Key::G,
87 "h" => Key::H,
88 "i" => Key::I,
89 "j" => Key::J,
90 "k" => Key::K,
91 "l" => Key::L,
92 "m" => Key::M,
93 "n" => Key::N,
94 "o" => Key::O,
95 "p" => Key::P,
96 "q" => Key::Q,
97 "r" => Key::R,
98 "s" => Key::S,
99 "t" => Key::T,
100 "u" => Key::U,
101 "v" => Key::V,
102 "w" => Key::W,
103 "x" => Key::X,
104 "y" => Key::Y,
105 "z" => Key::Z,
106
107 "0" => Key::Num0,
109 "1" => Key::Num1,
110 "2" => Key::Num2,
111 "3" => Key::Num3,
112 "4" => Key::Num4,
113 "5" => Key::Num5,
114 "6" => Key::Num6,
115 "7" => Key::Num7,
116 "8" => Key::Num8,
117 "9" => Key::Num9,
118
119 "f1" => Key::F1,
121 "f2" => Key::F2,
122 "f3" => Key::F3,
123 "f4" => Key::F4,
124 "f5" => Key::F5,
125 "f6" => Key::F6,
126 "f7" => Key::F7,
127 "f8" => Key::F8,
128 "f9" => Key::F9,
129 "f10" => Key::F10,
130 "f11" => Key::F11,
131 "f12" => Key::F12,
132
133 "enter" | "return" => Key::Enter,
135 "space" => Key::Space,
136 "tab" => Key::Tab,
137 "escape" | "esc" => Key::Escape,
138 "backspace" => Key::Backspace,
139 "delete" | "del" => Key::Delete,
140 "home" => Key::Home,
141 "end" => Key::End,
142 "pageup" | "pgup" => Key::PageUp,
143 "pagedown" | "pgdown" => Key::PageDown,
144 "up" => Key::ArrowUp,
145 "down" => Key::ArrowDown,
146 "left" => Key::ArrowLeft,
147 "right" => Key::ArrowRight,
148
149 "-" | "minus" => Key::Minus,
151 "=" | "plus" => Key::Equals,
152 "[" => Key::OpenBracket,
153 "]" => Key::CloseBracket,
154 ";" => Key::Semicolon,
155 "'" => Key::Quote,
156 "`" => Key::Backtick,
157 "\\" => Key::Backslash,
158 "," => Key::Comma,
159 "." => Key::Period,
160 "/" => Key::Slash,
161
162 _ => return Err(ShortcutParseError::InvalidKey(key_str)),
163 };
164
165 Ok(Self { key, modifiers })
166 }
167
168 pub fn parse(shortcut: &str) -> Self {
177 Self::from_string(shortcut).expect(&format!("Invalid shortcut: {}", shortcut))
178 }
179
180 pub fn matches(&self, key: Key, modifiers: Modifiers) -> bool {
182 self.key == key && self.modifiers == modifiers
183 }
184
185 pub fn just_pressed(&self, ctx: &egui::Context) -> bool {
187 let shortcut_key = format!(
189 "{:?}_{}_{}_{}_{}",
190 self.key,
191 self.modifiers.ctrl,
192 self.modifiers.alt,
193 self.modifiers.shift,
194 self.modifiers.command
195 );
196
197 let current_frame_pressed = ctx.input(|i| {
199 let key_pressed = i.key_pressed(self.key);
200
201 let ctrl_held = i.modifiers.ctrl || i.modifiers.command;
203 let ctrl_match = if self.modifiers.ctrl {
204 ctrl_held } else {
206 !ctrl_held };
208
209 let alt_match = i.modifiers.alt == self.modifiers.alt;
210 let shift_match = i.modifiers.shift == self.modifiers.shift;
211
212 key_pressed && ctrl_match && alt_match && shift_match
213 });
214
215 let mut states = SHORTCUT_STATES.lock().unwrap();
217 let was_pressed = states.get(&shortcut_key).copied().unwrap_or(false);
218
219 states.insert(shortcut_key.clone(), current_frame_pressed);
221
222 current_frame_pressed && !was_pressed
224 }
225
226 pub fn display_string(&self) -> String {
228 let mut result = String::new();
229
230 if self.modifiers.ctrl {
231 result.push_str("Ctrl+");
232 }
233 if self.modifiers.alt {
234 result.push_str("Alt+");
235 }
236 if self.modifiers.shift {
237 result.push_str("Shift+");
238 }
239 if self.modifiers.command {
240 result.push_str("Cmd+");
241 }
242
243 result.push_str(&self.key.name());
244 result
245 }
246}