#![cfg(target_os = "macos")]
#![allow(dead_code)]
use crate::compat::{String, ToString};
pub(crate) const MOD_SHIFT: u64 = 1 << 17;
pub(crate) const MOD_CONTROL: u64 = 1 << 18;
pub(crate) const MOD_OPTION: u64 = 1 << 19;
pub(crate) const MOD_COMMAND: u64 = 1 << 20;
#[cfg(widgets_unstripped)]
pub(crate) fn map_key_modifiers(appkit_flags: u64, key_code: u16) -> (u32, u32) {
(key_code as u32, map_modifiers(appkit_flags))
}
#[cfg(widgets_unstripped)]
pub(crate) fn logical_key(appkit_code: u16, characters: Option<&str>) -> u32 {
use crate::event::key_codes as keys;
let named = match appkit_code {
36 | 76 => keys::ENTER,
53 => keys::ESCAPE,
51 => keys::BACKSPACE,
117 => keys::DELETE,
123 => keys::LEFT,
124 => keys::RIGHT,
125 => keys::DOWN,
126 => keys::UP,
115 => keys::HOME,
119 => keys::END,
116 => keys::PAGE_UP,
121 => keys::PAGE_DOWN,
48 => keys::TAB,
122 => keys::F1,
120 => keys::F2,
99 => keys::F3,
118 => keys::F4,
96 => keys::F5,
97 => keys::F6,
98 => keys::F7,
100 => keys::F8,
101 => keys::F9,
109 => keys::F10,
103 => keys::F11,
111 => keys::F12,
_ => 0,
};
if named != 0 {
return named;
}
if let Some(ch) = characters.and_then(|text| text.chars().next()) {
if let Some(code) = keys::ascii_key_code(ch) {
return code;
}
}
appkit_code as u32
}
#[cfg(widgets_unstripped)]
pub(crate) fn map_modifiers(appkit_flags: u64) -> u32 {
const WIDGET_SHIFT: u32 = 1;
const WIDGET_CONTROL: u32 = 2;
const WIDGET_ALT: u32 = 4;
const WIDGET_META: u32 = 8;
let mut modifiers = 0u32;
if appkit_flags & MOD_SHIFT != 0 {
modifiers |= WIDGET_SHIFT;
}
if appkit_flags & MOD_CONTROL != 0 {
modifiers |= WIDGET_CONTROL;
}
if appkit_flags & MOD_OPTION != 0 {
modifiers |= WIDGET_ALT;
}
if appkit_flags & MOD_COMMAND != 0 {
modifiers |= WIDGET_META;
}
modifiers
}
pub(crate) fn parse_shortcut(shortcut: Option<&str>) -> (String, u64) {
let Some(raw) = shortcut.map(|s| s.trim()).filter(|s| !s.is_empty()) else {
return (String::new(), 0);
};
let mut modifiers: u64 = 0;
let mut key = String::new();
for ch in raw.chars() {
match ch {
'⌘' => modifiers |= MOD_COMMAND,
'⌃' | '^' => modifiers |= MOD_CONTROL,
'⌥' => modifiers |= MOD_OPTION,
'⇧' => modifiers |= MOD_SHIFT,
_ => {}
}
}
for part in raw.split('+') {
let token = part.trim().to_lowercase();
let token = token.trim_start_matches(['⌘', '⌃', '^', '⌥', '⇧']).to_string();
match token.as_str() {
"primary" | "cmdorctrl" | "cmd" | "command" | "meta" => modifiers |= MOD_COMMAND,
"ctrl" | "control" => modifiers |= MOD_CONTROL,
"alt" | "option" => modifiers |= MOD_OPTION,
"shift" => modifiers |= MOD_SHIFT,
"enter" | "return" => key = "\r".to_string(),
"tab" => key = "\t".to_string(),
"space" => key = " ".to_string(),
"delete" | "del" | "backspace" => key = "\u{8}".to_string(),
"esc" | "escape" => key = "\u{1b}".to_string(),
"left" => key = "\u{f702}".to_string(),
"right" => key = "\u{f703}".to_string(),
"up" => key = "\u{f700}".to_string(),
"down" => key = "\u{f701}".to_string(),
"pageup" | "pgup" => key = "\u{f72c}".to_string(),
"pagedown" | "pgdn" => key = "\u{f72d}".to_string(),
"home" => key = "\u{f729}".to_string(),
"end" => key = "\u{f72b}".to_string(),
"⌘" | "⌃" | "⌥" | "⇧" | "↩" | "⇥" | "⎋" | "⌫" | "←" | "→" | "↑" | "↓" | "⇞" | "⇟"
| "↖" | "↘" => {
}
_ if !token.is_empty() => {
key = token;
}
_ => { }
}
}
if key.is_empty() {
let stripped: String =
raw.chars().filter(|ch| !matches!(ch, '⌘' | '⌃' | '^' | '⌥' | '⇧')).collect();
let stripped = stripped.trim().to_string();
if !stripped.is_empty() {
key = match stripped.as_str() {
"↩" => "\r".to_string(),
"⇥" => "\t".to_string(),
"⎋" => "\u{1b}".to_string(),
"⌫" => "\u{8}".to_string(),
"←" => "\u{f702}".to_string(),
"→" => "\u{f703}".to_string(),
"↑" => "\u{f700}".to_string(),
"↓" => "\u{f701}".to_string(),
"⇞" => "\u{f72c}".to_string(),
"⇟" => "\u{f72d}".to_string(),
"↖" => "\u{f729}".to_string(),
"↘" => "\u{f72b}".to_string(),
other => other.to_string(),
};
}
}
if !key.is_empty() && modifiers == 0 {
modifiers = MOD_COMMAND;
}
(key, modifiers)
}
#[cfg(test)]
mod parse_shortcut_tests {
use super::{parse_shortcut, MOD_COMMAND, MOD_CONTROL, MOD_OPTION, MOD_SHIFT};
use crate::compat::String;
#[test]
fn absent_shortcut_produces_no_key() {
assert_eq!(parse_shortcut(None), (String::new(), 0));
assert_eq!(parse_shortcut(Some("")), (String::new(), 0));
assert_eq!(parse_shortcut(Some(" ")), (String::new(), 0));
}
#[test]
fn primary_spelling_maps_to_command() {
let (key, mask) = parse_shortcut(Some("Primary+S"));
assert_eq!(key, "s");
assert_eq!(mask, MOD_COMMAND);
}
#[test]
fn glyph_form_maps_to_command() {
let (key, mask) = parse_shortcut(Some("⌘S"));
assert_eq!(key, "s");
assert_eq!(mask, MOD_COMMAND);
}
#[test]
fn control_and_shift_are_distinct_from_command() {
let (key, mask) = parse_shortcut(Some("Ctrl+Shift+Z"));
assert_eq!(key, "z");
assert_eq!(mask, MOD_CONTROL | MOD_SHIFT);
}
#[test]
fn glyph_combo_recovers_named_key() {
let (key, mask) = parse_shortcut(Some("⇧⌘Z"));
assert_eq!(key, "z");
assert_eq!(mask, MOD_SHIFT | MOD_COMMAND);
}
#[test]
fn option_spelling_maps_to_option() {
let (key, mask) = parse_shortcut(Some("Alt+F4"));
assert_eq!(key, "f4");
assert_eq!(mask, MOD_OPTION);
}
#[test]
fn bare_key_defaults_to_command() {
let (key, mask) = parse_shortcut(Some("F1"));
assert_eq!(key, "f1");
assert_eq!(mask, MOD_COMMAND);
}
}
#[cfg(all(test, widgets_unstripped))]
mod logical_key_tests {
use super::logical_key;
use crate::event::key_codes as keys;
#[test]
fn editing_and_navigation_keys_are_normalised() {
assert_eq!(logical_key(36, None), keys::ENTER);
assert_eq!(logical_key(76, None), keys::ENTER, "keypad Enter is Enter");
assert_eq!(logical_key(53, None), keys::ESCAPE);
assert_eq!(logical_key(51, None), keys::BACKSPACE);
assert_eq!(logical_key(117, None), keys::DELETE);
assert_eq!(logical_key(123, None), keys::LEFT);
assert_eq!(logical_key(124, None), keys::RIGHT);
assert_eq!(logical_key(125, None), keys::DOWN);
assert_eq!(logical_key(126, None), keys::UP);
assert_eq!(logical_key(115, None), keys::HOME);
assert_eq!(logical_key(119, None), keys::END);
assert_eq!(logical_key(116, None), keys::PAGE_UP);
assert_eq!(logical_key(121, None), keys::PAGE_DOWN);
}
#[test]
fn function_keys_map_to_the_conventional_range() {
assert_eq!(logical_key(122, None), keys::F1);
assert_eq!(logical_key(120, None), keys::F2);
assert_eq!(logical_key(99, None), keys::F3);
assert_eq!(logical_key(118, None), keys::F4);
assert_eq!(logical_key(96, None), keys::F5);
assert_eq!(logical_key(97, None), keys::F6);
assert_eq!(logical_key(98, None), keys::F7);
assert_eq!(logical_key(100, None), keys::F8);
assert_eq!(logical_key(101, None), keys::F9);
assert_eq!(logical_key(109, None), keys::F10);
assert_eq!(logical_key(103, None), keys::F11);
assert_eq!(logical_key(111, None), keys::F12);
}
#[test]
fn a_chord_reports_the_letters_upper_case_code() {
assert_eq!(logical_key(0, Some("a")), 65, "⌘A reaches LineEdit as 65");
assert_eq!(logical_key(8, Some("c")), 67, "⌘C reaches LineEdit as 67");
assert_eq!(logical_key(7, Some("x")), 88, "⌘X reaches LineEdit as 88");
assert_eq!(logical_key(6, Some("z")), 90, "⌘Z reaches LineEdit as 90");
assert_eq!(logical_key(16, Some("y")), 89, "⌘Y reaches LineEdit as 89");
}
#[test]
fn an_unnamed_key_keeps_its_hardware_code() {
assert_eq!(logical_key(10, None), 10, "§ has no framework name");
}
}