use crate::event::{Key, Modifiers, NamedKey};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModifierScheme {
Mac,
Pc,
}
impl ModifierScheme {
pub fn native() -> Self {
if cfg!(target_os = "macos") {
ModifierScheme::Mac
} else {
ModifierScheme::Pc
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AccelMods {
pub primary: bool,
pub secondary: bool,
pub alt: bool,
pub shift: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Accel {
pub mods: AccelMods,
pub key: Key,
}
impl Accel {
pub fn primary(key: impl Into<Key>) -> Self {
Self {
mods: AccelMods {
primary: true,
..AccelMods::default()
},
key: key.into(),
}
}
pub fn shift(mut self) -> Self {
self.mods.shift = true;
self
}
pub fn alt(mut self) -> Self {
self.mods.alt = true;
self
}
pub fn secondary(mut self) -> Self {
self.mods.secondary = true;
self
}
pub fn matches(&self, key: Key, modifiers: Modifiers, scheme: ModifierScheme) -> bool {
if modifiers.alt_graph {
return false;
}
let (control, logo) = match scheme {
ModifierScheme::Mac => (self.mods.secondary, self.mods.primary),
ModifierScheme::Pc => (self.mods.primary, self.mods.secondary),
};
if modifiers.control != control
|| modifiers.logo != logo
|| modifiers.alt != self.mods.alt
|| modifiers.shift != self.mods.shift
{
return false;
}
match (self.key, key) {
(Key::Char(a), Key::Char(b)) => a.eq_ignore_ascii_case(&b),
(a, b) => a == b,
}
}
pub fn label(&self, scheme: ModifierScheme) -> String {
let mut parts: Vec<&str> = Vec::new();
match scheme {
ModifierScheme::Pc => {
if self.mods.primary {
parts.push("Ctrl");
}
if self.mods.secondary {
parts.push("Super");
}
if self.mods.alt {
parts.push("Alt");
}
if self.mods.shift {
parts.push("Shift");
}
}
ModifierScheme::Mac => {
if self.mods.secondary {
parts.push("Ctrl");
}
if self.mods.alt {
parts.push("Opt");
}
if self.mods.shift {
parts.push("Shift");
}
if self.mods.primary {
parts.push("Cmd");
}
}
}
let key = key_label(self.key);
parts.push(&key);
parts.join("+")
}
}
fn key_label(key: Key) -> String {
match key {
Key::Char(c) => c.to_ascii_uppercase().to_string(),
Key::Named(named) => match named {
NamedKey::Enter => "Enter",
NamedKey::Backspace => "Backspace",
NamedKey::Delete => "Delete",
NamedKey::Tab => "Tab",
NamedKey::Escape => "Esc",
NamedKey::Space => "Space",
NamedKey::Left => "Left",
NamedKey::Right => "Right",
NamedKey::Up => "Up",
NamedKey::Down => "Down",
NamedKey::Home => "Home",
NamedKey::End => "End",
NamedKey::PageUp => "PageUp",
NamedKey::PageDown => "PageDown",
}
.to_string(),
}
}
impl From<&str> for Accel {
fn from(s: &str) -> Self {
let mut mods = AccelMods::default();
let mut tokens = s.split('+').peekable();
loop {
let token = tokens
.next()
.unwrap_or_else(|| panic!("empty accelerator string {s:?}"));
if tokens.peek().is_none() {
return Accel {
mods,
key: parse_key(token, s),
};
}
match token.to_ascii_lowercase().as_str() {
"ctrl" | "control" | "cmd" | "command" | "primary" => mods.primary = true,
"super" | "win" | "secondary" => mods.secondary = true,
"alt" | "opt" | "option" => mods.alt = true,
"shift" => mods.shift = true,
_ => panic!("unknown modifier {token:?} in accelerator {s:?}"),
}
}
}
}
fn parse_key(token: &str, accel: &str) -> Key {
let mut chars = token.chars();
if let (Some(c), None) = (chars.next(), chars.next()) {
return Key::Char(c.to_ascii_lowercase());
}
let named = match token.to_ascii_lowercase().as_str() {
"enter" | "return" => NamedKey::Enter,
"backspace" => NamedKey::Backspace,
"delete" | "del" => NamedKey::Delete,
"tab" => NamedKey::Tab,
"escape" | "esc" => NamedKey::Escape,
"space" => NamedKey::Space,
"left" => NamedKey::Left,
"right" => NamedKey::Right,
"up" => NamedKey::Up,
"down" => NamedKey::Down,
"home" => NamedKey::Home,
"end" => NamedKey::End,
"pageup" | "pgup" => NamedKey::PageUp,
"pagedown" | "pgdn" => NamedKey::PageDown,
_ => panic!("unknown key {token:?} in accelerator {accel:?}"),
};
Key::Named(named)
}
#[cfg(test)]
mod tests {
use super::*;
fn mods(control: bool, logo: bool, alt: bool, shift: bool) -> Modifiers {
Modifiers {
control,
logo,
alt,
shift,
alt_graph: false,
}
}
#[test]
fn primary_resolves_to_ctrl_on_pc_and_cmd_on_mac() {
let accel = Accel::primary('r');
let ctrl_r = mods(true, false, false, false);
let cmd_r = mods(false, true, false, false);
assert!(accel.matches(Key::Char('r'), ctrl_r, ModifierScheme::Pc));
assert!(!accel.matches(Key::Char('r'), cmd_r, ModifierScheme::Pc));
assert!(accel.matches(Key::Char('r'), cmd_r, ModifierScheme::Mac));
assert!(!accel.matches(Key::Char('r'), ctrl_r, ModifierScheme::Mac));
}
#[test]
fn secondary_resolves_to_super_on_pc_and_ctrl_on_mac() {
let accel = Accel::primary('r').secondary();
assert!(accel.matches(
Key::Char('r'),
mods(true, true, false, false),
ModifierScheme::Pc
));
assert!(accel.matches(
Key::Char('r'),
mods(true, true, false, false),
ModifierScheme::Mac
));
assert!(!accel.matches(
Key::Char('r'),
mods(true, false, false, false),
ModifierScheme::Pc
));
}
#[test]
fn modifiers_must_match_exactly() {
let accel = Accel::primary('r');
assert!(!accel.matches(
Key::Char('r'),
mods(true, false, false, true),
ModifierScheme::Pc
));
assert!(!accel.matches(
Key::Char('r'),
mods(true, false, true, false),
ModifierScheme::Pc
));
let shifted = Accel::primary('r').shift();
assert!(shifted.matches(
Key::Char('r'),
mods(true, false, false, true),
ModifierScheme::Pc
));
assert!(!shifted.matches(
Key::Char('r'),
mods(true, false, false, false),
ModifierScheme::Pc
));
}
#[test]
fn char_keys_match_case_insensitively() {
let accel = Accel::primary('r');
assert!(accel.matches(
Key::Char('R'),
mods(true, false, false, false),
ModifierScheme::Pc
));
assert!(Accel::from("Ctrl+R").matches(
Key::Char('r'),
mods(true, false, false, false),
ModifierScheme::Pc
));
}
#[test]
fn altgr_chords_never_match() {
let accel = Accel::primary('q').alt();
let altgr = Modifiers {
control: true,
alt: true,
alt_graph: true,
..Modifiers::default()
};
assert!(!accel.matches(Key::Char('q'), altgr, ModifierScheme::Pc));
}
#[test]
fn parses_the_conventional_string_forms() {
assert_eq!(Accel::from("Ctrl+R"), Accel::primary('r'));
assert_eq!(Accel::from("Cmd+R"), Accel::primary('r'));
assert_eq!(Accel::from("Primary+R"), Accel::primary('r'));
assert_eq!(Accel::from("Ctrl+Shift+T"), Accel::primary('t').shift());
assert_eq!(Accel::from("Ctrl+Enter"), Accel::primary(NamedKey::Enter));
assert_eq!(Accel::from("Ctrl+Left"), Accel::primary(NamedKey::Left));
assert_eq!(
Accel::from("Ctrl+Super+Alt+R"),
Accel::primary('r').secondary().alt()
);
}
#[test]
#[should_panic(expected = "unknown modifier")]
fn parsing_an_unknown_modifier_panics() {
let _ = Accel::from("Hyper+R");
}
#[test]
#[should_panic(expected = "unknown key")]
fn parsing_an_unknown_key_panics() {
let _ = Accel::from("Ctrl+Klick");
}
#[test]
fn labels_follow_each_platforms_convention() {
let accel = Accel::primary('r').shift();
assert_eq!(accel.label(ModifierScheme::Pc), "Ctrl+Shift+R");
assert_eq!(accel.label(ModifierScheme::Mac), "Shift+Cmd+R");
let full = Accel::primary('r').secondary().alt().shift();
assert_eq!(full.label(ModifierScheme::Pc), "Ctrl+Super+Alt+Shift+R");
assert_eq!(full.label(ModifierScheme::Mac), "Ctrl+Opt+Shift+Cmd+R");
}
#[test]
fn pc_labels_round_trip_through_parsing() {
for s in ["Ctrl+R", "Ctrl+Enter", "Ctrl+Left", "Ctrl+Right", "Ctrl+Q"] {
assert_eq!(Accel::from(s).label(ModifierScheme::Pc), s);
}
}
}