use crate::platform::event::{KeyCode, Modifiers};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Shortcut {
pub key: KeyCode,
pub command: bool,
pub shift: bool,
pub alt: bool,
pub ctrl: bool,
}
impl Shortcut {
pub fn key(key: KeyCode) -> Self {
let key = match key {
KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()),
other => other,
};
Self {
key,
command: false,
shift: false,
alt: false,
ctrl: false,
}
}
pub fn cmd(c: char) -> Self {
Self {
command: true,
..Self::key(KeyCode::Char(c))
}
}
pub fn cmd_key(key: KeyCode) -> Self {
Self {
command: true,
..Self::key(key)
}
}
pub fn shift(mut self) -> Self {
self.shift = true;
self
}
pub fn alt(mut self) -> Self {
self.alt = true;
self
}
pub fn ctrl(mut self) -> Self {
self.ctrl = true;
self
}
pub fn matches(&self, key: KeyCode, modifiers: Modifiers) -> bool {
self.matches_for(key, modifiers, cfg!(target_os = "macos"))
}
pub fn matches_for(&self, key: KeyCode, modifiers: Modifiers, mac: bool) -> bool {
let same_key = match (self.key, key) {
(KeyCode::Char(a), KeyCode::Char(b)) => a.eq_ignore_ascii_case(&b),
(a, b) => a == b,
};
let (command, ctrl) = if mac {
(modifiers.meta, modifiers.ctrl)
} else {
let ctrl = modifiers.ctrl && !modifiers.meta;
(ctrl, ctrl)
};
let wants_ctrl = if mac {
self.ctrl
} else {
self.command || self.ctrl
};
let wants_command = if mac {
self.command
} else {
self.command || self.ctrl
};
same_key
&& command == wants_command
&& ctrl == wants_ctrl
&& modifiers.shift == self.shift
&& modifiers.alt == self.alt
}
pub fn label(&self) -> String {
self.label_for(cfg!(target_os = "macos"))
}
pub fn label_for(&self, mac: bool) -> String {
let mut out = String::new();
if mac {
if self.ctrl {
out.push('\u{2303}');
}
if self.alt {
out.push('\u{2325}');
}
if self.shift {
out.push('\u{21E7}');
}
if self.command {
out.push('\u{2318}');
}
out.push_str(&key_name(self.key, true));
} else {
if self.command || self.ctrl {
out.push_str("Ctrl+");
}
if self.alt {
out.push_str("Alt+");
}
if self.shift {
out.push_str("Shift+");
}
out.push_str(&key_name(self.key, false));
}
out
}
}
fn key_name(key: KeyCode, mac: bool) -> String {
let named = |pc: &str, apple: &str| if mac { apple } else { pc }.to_string();
match key {
KeyCode::Char(' ') | KeyCode::Space => named("Space", "Space"),
KeyCode::Char(c) => c.to_uppercase().collect(),
KeyCode::Enter => named("Enter", "\u{21A9}"),
KeyCode::Escape => named("Esc", "\u{238B}"),
KeyCode::Backspace => named("Backspace", "\u{232B}"),
KeyCode::Delete => named("Del", "\u{2326}"),
KeyCode::Tab => named("Tab", "\u{21E5}"),
KeyCode::Insert => named("Ins", "Ins"),
KeyCode::Home => named("Home", "\u{2196}"),
KeyCode::End => named("End", "\u{2198}"),
KeyCode::PageUp => named("PgUp", "\u{21DE}"),
KeyCode::PageDown => named("PgDn", "\u{21DF}"),
KeyCode::ArrowLeft => named("Left", "\u{2190}"),
KeyCode::ArrowRight => named("Right", "\u{2192}"),
KeyCode::ArrowUp => named("Up", "\u{2191}"),
KeyCode::ArrowDown => named("Down", "\u{2193}"),
KeyCode::F1 => "F1".into(),
KeyCode::F2 => "F2".into(),
KeyCode::F3 => "F3".into(),
KeyCode::F4 => "F4".into(),
KeyCode::F5 => "F5".into(),
KeyCode::F6 => "F6".into(),
KeyCode::F7 => "F7".into(),
KeyCode::F8 => "F8".into(),
KeyCode::F9 => "F9".into(),
KeyCode::F10 => "F10".into(),
KeyCode::F11 => "F11".into(),
KeyCode::F12 => "F12".into(),
KeyCode::Alt => named("Alt", "\u{2325}"),
KeyCode::ContextMenu => "Menu".into(),
KeyCode::Unidentified(_) => "?".into(),
}
}
pub fn mnemonic(label: &str) -> (String, Option<char>) {
let (text, at) = mnemonic_at(label);
let key = at.and_then(|i| text[i..].chars().next()?.to_lowercase().next());
(text, key)
}
pub fn mnemonic_at(label: &str) -> (String, Option<usize>) {
let mut text = String::with_capacity(label.len());
let mut at = None;
let mut chars = label.chars();
while let Some(c) = chars.next() {
if c != '&' {
text.push(c);
continue;
}
match chars.next() {
Some('&') => text.push('&'),
Some(next) => {
if at.is_none() {
at = Some(text.len());
}
text.push(next);
}
None => {}
}
}
(text, at)
}
pub fn mnemonic_range(text: &str, at: Option<usize>) -> Option<(usize, usize)> {
let i = at?;
let c = text.get(i..)?.chars().next()?;
Some((i, i + c.len_utf8()))
}
#[derive(Clone, Debug, PartialEq)]
pub enum NativeMenuItem {
Action {
id: u32,
label: String,
shortcut: Option<Shortcut>,
enabled: bool,
checked: Option<bool>,
},
Separator,
Submenu(NativeMenu),
}
#[derive(Clone, Debug, PartialEq)]
pub struct NativeMenu {
pub label: String,
pub items: Vec<NativeMenuItem>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NativeMenuBar {
pub app_name: String,
pub menus: Vec<NativeMenu>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MenuPopup {
Unsupported,
Dismissed,
Picked(u32),
}
pub const TRAY_ID_BASE: u32 = 0x8000;
#[derive(Clone, Debug, PartialEq)]
pub struct NativeTray {
pub tooltip: String,
pub icon: Option<crate::render::Pixmap>,
pub menu: Vec<NativeMenuItem>,
}
#[cfg(test)]
mod tests {
use super::*;
fn mods(shift: bool, ctrl: bool, alt: bool, meta: bool) -> Modifiers {
Modifiers {
shift,
ctrl,
alt,
meta,
}
}
#[test]
fn command_is_ctrl_off_mac_and_cmd_on_mac() {
let s = Shortcut::cmd('S');
assert_eq!(s.key, KeyCode::Char('s'));
let c = KeyCode::Char('s');
assert!(s.matches_for(c, mods(false, true, false, false), false));
assert!(!s.matches_for(c, mods(false, false, false, true), false));
assert!(s.matches_for(c, mods(false, false, false, true), true));
assert!(!s.matches_for(c, mods(false, true, false, false), true));
assert!(!s.matches_for(c, mods(true, true, false, false), false));
let redo = Shortcut::cmd('z').shift();
assert!(redo.matches_for(KeyCode::Char('Z'), mods(true, true, false, false), false));
let f5 = Shortcut::key(KeyCode::F5);
assert!(f5.matches_for(KeyCode::F5, Modifiers::default(), false));
assert!(!f5.matches_for(KeyCode::F5, mods(false, true, false, false), false));
}
#[test]
fn labels_follow_the_platform() {
let s = Shortcut::cmd('q').alt();
assert_eq!(s.label_for(false), "Ctrl+Alt+Q");
assert_eq!(s.label_for(true), "\u{2325}\u{2318}Q");
assert_eq!(Shortcut::key(KeyCode::Delete).label_for(false), "Del");
assert_eq!(Shortcut::key(KeyCode::F2).label_for(true), "F2");
}
}