use egui::{Key, Modifiers};
use std::collections::HashMap;
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref SHORTCUT_STATES: Mutex<HashMap<String, bool>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone)]
pub struct KeyboardShortcut {
pub key: Key,
pub modifiers: Modifiers,
}
#[derive(Debug, Clone)]
pub enum ShortcutParseError {
InvalidKey(String),
InvalidModifier(String),
InvalidFormat(String),
}
impl KeyboardShortcut {
pub fn new(key: Key) -> Self {
Self {
key,
modifiers: Modifiers::default(),
}
}
pub fn from_string(shortcut: &str) -> Result<Self, ShortcutParseError> {
let parts: Vec<&str> = shortcut.split('+').collect();
if parts.is_empty() {
return Err(ShortcutParseError::InvalidFormat(shortcut.to_string()));
}
let mut modifiers = Modifiers::default();
let key_str = parts.last().unwrap().to_lowercase();
for part in &parts[..parts.len() - 1] {
let modifier = part.to_lowercase();
match modifier.as_str() {
"ctrl" | "control" => modifiers.ctrl = true,
"alt" => modifiers.alt = true,
"shift" => modifiers.shift = true,
"cmd" | "meta" | "super" => modifiers.command = true,
_ => return Err(ShortcutParseError::InvalidModifier(part.to_string())),
}
}
let key = match key_str.as_str() {
"a" => Key::A,
"b" => Key::B,
"c" => Key::C,
"d" => Key::D,
"e" => Key::E,
"f" => Key::F,
"g" => Key::G,
"h" => Key::H,
"i" => Key::I,
"j" => Key::J,
"k" => Key::K,
"l" => Key::L,
"m" => Key::M,
"n" => Key::N,
"o" => Key::O,
"p" => Key::P,
"q" => Key::Q,
"r" => Key::R,
"s" => Key::S,
"t" => Key::T,
"u" => Key::U,
"v" => Key::V,
"w" => Key::W,
"x" => Key::X,
"y" => Key::Y,
"z" => Key::Z,
"0" => Key::Num0,
"1" => Key::Num1,
"2" => Key::Num2,
"3" => Key::Num3,
"4" => Key::Num4,
"5" => Key::Num5,
"6" => Key::Num6,
"7" => Key::Num7,
"8" => Key::Num8,
"9" => Key::Num9,
"f1" => Key::F1,
"f2" => Key::F2,
"f3" => Key::F3,
"f4" => Key::F4,
"f5" => Key::F5,
"f6" => Key::F6,
"f7" => Key::F7,
"f8" => Key::F8,
"f9" => Key::F9,
"f10" => Key::F10,
"f11" => Key::F11,
"f12" => Key::F12,
"enter" | "return" => Key::Enter,
"space" => Key::Space,
"tab" => Key::Tab,
"escape" | "esc" => Key::Escape,
"backspace" => Key::Backspace,
"delete" | "del" => Key::Delete,
"home" => Key::Home,
"end" => Key::End,
"pageup" | "pgup" => Key::PageUp,
"pagedown" | "pgdown" => Key::PageDown,
"up" => Key::ArrowUp,
"down" => Key::ArrowDown,
"left" => Key::ArrowLeft,
"right" => Key::ArrowRight,
"-" | "minus" => Key::Minus,
"=" | "plus" => Key::Equals,
"[" => Key::OpenBracket,
"]" => Key::CloseBracket,
";" => Key::Semicolon,
"'" => Key::Quote,
"`" => Key::Backtick,
"\\" => Key::Backslash,
"," => Key::Comma,
"." => Key::Period,
"/" => Key::Slash,
_ => return Err(ShortcutParseError::InvalidKey(key_str)),
};
Ok(Self { key, modifiers })
}
pub fn parse(shortcut: &str) -> Self {
Self::from_string(shortcut).expect(&format!("Invalid shortcut: {}", shortcut))
}
pub fn matches(&self, key: Key, modifiers: Modifiers) -> bool {
self.key == key && self.modifiers == modifiers
}
pub fn just_pressed(&self, ctx: &egui::Context) -> bool {
let shortcut_key = format!(
"{:?}_{}_{}_{}_{}",
self.key,
self.modifiers.ctrl,
self.modifiers.alt,
self.modifiers.shift,
self.modifiers.command
);
let current_frame_pressed = ctx.input(|i| {
let key_pressed = i.key_pressed(self.key);
let ctrl_held = i.modifiers.ctrl || i.modifiers.command;
let ctrl_match = if self.modifiers.ctrl {
ctrl_held } else {
!ctrl_held };
let alt_match = i.modifiers.alt == self.modifiers.alt;
let shift_match = i.modifiers.shift == self.modifiers.shift;
key_pressed && ctrl_match && alt_match && shift_match
});
let mut states = SHORTCUT_STATES.lock().unwrap();
let was_pressed = states.get(&shortcut_key).copied().unwrap_or(false);
states.insert(shortcut_key.clone(), current_frame_pressed);
current_frame_pressed && !was_pressed
}
pub fn display_string(&self) -> String {
let mut result = String::new();
if self.modifiers.ctrl {
result.push_str("Ctrl+");
}
if self.modifiers.alt {
result.push_str("Alt+");
}
if self.modifiers.shift {
result.push_str("Shift+");
}
if self.modifiers.command {
result.push_str("Cmd+");
}
result.push_str(&self.key.name());
result
}
}