use crcbl_core::KeyCode;
use crcbl_core::input::{Keysym, Modifiers};
use super::ffi::{NSUInteger, modifier};
#[must_use]
pub const fn key_code(keycode: u16) -> Option<KeyCode> {
Some(match keycode {
0x00 => KeyCode::KeyA,
0x01 => KeyCode::KeyS,
0x02 => KeyCode::KeyD,
0x03 => KeyCode::KeyF,
0x04 => KeyCode::KeyH,
0x05 => KeyCode::KeyG,
0x06 => KeyCode::KeyZ,
0x07 => KeyCode::KeyX,
0x08 => KeyCode::KeyC,
0x09 => KeyCode::KeyV,
0x0A => KeyCode::IntlBackslash,
0x0B => KeyCode::KeyB,
0x0C => KeyCode::KeyQ,
0x0D => KeyCode::KeyW,
0x0E => KeyCode::KeyE,
0x0F => KeyCode::KeyR,
0x10 => KeyCode::KeyY,
0x11 => KeyCode::KeyT,
0x12 => KeyCode::Digit1,
0x13 => KeyCode::Digit2,
0x14 => KeyCode::Digit3,
0x15 => KeyCode::Digit4,
0x16 => KeyCode::Digit6,
0x17 => KeyCode::Digit5,
0x18 => KeyCode::Equal,
0x19 => KeyCode::Digit9,
0x1A => KeyCode::Digit7,
0x1B => KeyCode::Minus,
0x1C => KeyCode::Digit8,
0x1D => KeyCode::Digit0,
0x1E => KeyCode::BracketRight,
0x1F => KeyCode::KeyO,
0x20 => KeyCode::KeyU,
0x21 => KeyCode::BracketLeft,
0x22 => KeyCode::KeyI,
0x23 => KeyCode::KeyP,
0x24 => KeyCode::Enter,
0x25 => KeyCode::KeyL,
0x26 => KeyCode::KeyJ,
0x27 => KeyCode::Quote,
0x28 => KeyCode::KeyK,
0x29 => KeyCode::Semicolon,
0x2A => KeyCode::Backslash,
0x2B => KeyCode::Comma,
0x2C => KeyCode::Slash,
0x2D => KeyCode::KeyN,
0x2E => KeyCode::KeyM,
0x2F => KeyCode::Period,
0x30 => KeyCode::Tab,
0x31 => KeyCode::Space,
0x32 => KeyCode::Backquote,
0x33 => KeyCode::Backspace,
0x35 => KeyCode::Escape,
0x36 => KeyCode::SuperRight,
0x37 => KeyCode::SuperLeft,
0x38 => KeyCode::ShiftLeft,
0x39 => KeyCode::CapsLock,
0x3A => KeyCode::AltLeft,
0x3B => KeyCode::ControlLeft,
0x3C => KeyCode::ShiftRight,
0x3D => KeyCode::AltRight,
0x3E => KeyCode::ControlRight,
0x41 => KeyCode::NumpadDecimal,
0x43 => KeyCode::NumpadMultiply,
0x45 => KeyCode::NumpadAdd,
0x47 => KeyCode::NumLock,
0x4B => KeyCode::NumpadDivide,
0x4C => KeyCode::NumpadEnter,
0x4E => KeyCode::NumpadSubtract,
0x52 => KeyCode::Numpad0,
0x53 => KeyCode::Numpad1,
0x54 => KeyCode::Numpad2,
0x55 => KeyCode::Numpad3,
0x56 => KeyCode::Numpad4,
0x57 => KeyCode::Numpad5,
0x58 => KeyCode::Numpad6,
0x59 => KeyCode::Numpad7,
0x5B => KeyCode::Numpad8,
0x5C => KeyCode::Numpad9,
0x5D => KeyCode::IntlYen,
0x5E => KeyCode::IntlRo,
0x60 => KeyCode::F5,
0x61 => KeyCode::F6,
0x62 => KeyCode::F7,
0x63 => KeyCode::F3,
0x64 => KeyCode::F8,
0x65 => KeyCode::F9,
0x67 => KeyCode::F11,
0x6D => KeyCode::F10,
0x6F => KeyCode::F12,
0x72 => KeyCode::Insert,
0x73 => KeyCode::Home,
0x74 => KeyCode::PageUp,
0x75 => KeyCode::Delete,
0x76 => KeyCode::F4,
0x77 => KeyCode::End,
0x78 => KeyCode::F2,
0x79 => KeyCode::PageDown,
0x7A => KeyCode::F1,
0x7B => KeyCode::ArrowLeft,
0x7C => KeyCode::ArrowRight,
0x7D => KeyCode::ArrowDown,
0x7E => KeyCode::ArrowUp,
_ => return None,
})
}
pub use crate::keysym::named_keysym;
const FUNCTION_KEY_START: u32 = 0xF700;
const FUNCTION_KEY_END: u32 = 0xF900;
fn unshifted(character: char) -> char {
let mut lowered = character.to_lowercase();
match (lowered.next(), lowered.next()) {
(Some(single), None) => single,
_ => character,
}
}
#[must_use]
pub fn keysym_from_character(character: char) -> Keysym {
let codepoint = character as u32;
if (FUNCTION_KEY_START..FUNCTION_KEY_END).contains(&codepoint) || character.is_control() {
return Keysym::NONE;
}
Keysym::from_char(unshifted(character))
}
#[must_use]
pub fn is_text(character: char) -> bool {
!character.is_control()
}
#[must_use]
pub fn modifiers(flags: NSUInteger) -> Modifiers {
let mut modifiers = Modifiers::empty();
modifiers.set(Modifiers::SHIFT, flags & modifier::SHIFT != 0);
modifiers.set(Modifiers::CTRL, flags & modifier::CONTROL != 0);
modifiers.set(Modifiers::ALT, flags & modifier::OPTION != 0);
modifiers.set(Modifiers::SUPER, flags & modifier::COMMAND != 0);
modifiers.set(Modifiers::CAPS_LOCK, flags & modifier::CAPS_LOCK != 0);
modifiers
}
#[must_use]
pub const fn modifier_is_down(key: KeyCode, flags: NSUInteger) -> Option<bool> {
let bit = match key {
KeyCode::ShiftLeft => modifier::DEVICE_LEFT_SHIFT,
KeyCode::ShiftRight => modifier::DEVICE_RIGHT_SHIFT,
KeyCode::ControlLeft => modifier::DEVICE_LEFT_CONTROL,
KeyCode::ControlRight => modifier::DEVICE_RIGHT_CONTROL,
KeyCode::AltLeft => modifier::DEVICE_LEFT_OPTION,
KeyCode::AltRight => modifier::DEVICE_RIGHT_OPTION,
KeyCode::SuperLeft => modifier::DEVICE_LEFT_COMMAND,
KeyCode::SuperRight => modifier::DEVICE_RIGHT_COMMAND,
KeyCode::CapsLock => modifier::CAPS_LOCK,
_ => return None,
};
Some(flags & bit != 0)
}
#[cfg(test)]
mod tests {
use super::*;
const NUMERIC_PAD: NSUInteger = 1 << 21;
const FUNCTION: NSUInteger = 1 << 23;
fn all_keycodes() -> impl Iterator<Item = u16> {
0..=0xFFu16
}
#[test]
fn the_alphabetic_block_is_a_key_matrix_and_not_an_alphabet() {
assert_eq!(key_code(0x00), Some(KeyCode::KeyA));
assert_eq!(key_code(0x01), Some(KeyCode::KeyS));
assert_eq!(key_code(0x02), Some(KeyCode::KeyD));
assert_eq!(key_code(0x03), Some(KeyCode::KeyF));
assert_eq!(key_code(0x16), Some(KeyCode::Digit6));
assert_eq!(key_code(0x17), Some(KeyCode::Digit5));
assert_eq!(key_code(0x18), Some(KeyCode::Equal));
assert_eq!(key_code(0x19), Some(KeyCode::Digit9));
}
#[test]
fn wasd_and_the_gameplay_keys_map_to_their_apple_codes() {
assert_eq!(key_code(0x0D), Some(KeyCode::KeyW));
assert_eq!(key_code(0x00), Some(KeyCode::KeyA));
assert_eq!(key_code(0x01), Some(KeyCode::KeyS));
assert_eq!(key_code(0x02), Some(KeyCode::KeyD));
assert_eq!(key_code(0x31), Some(KeyCode::Space));
assert_eq!(key_code(0x35), Some(KeyCode::Escape));
assert_eq!(key_code(0x67), Some(KeyCode::F11), "the sample's mode key");
}
#[test]
fn apples_delete_is_backspace_and_its_forward_delete_is_delete() {
assert_eq!(key_code(0x33), Some(KeyCode::Backspace));
assert_eq!(key_code(0x75), Some(KeyCode::Delete));
assert_eq!(key_code(0x72), Some(KeyCode::Insert));
}
#[test]
fn the_function_row_is_not_in_order_and_neither_is_this_table() {
for (code, key) in [
(0x7Au16, KeyCode::F1),
(0x78, KeyCode::F2),
(0x63, KeyCode::F3),
(0x76, KeyCode::F4),
(0x60, KeyCode::F5),
(0x61, KeyCode::F6),
(0x62, KeyCode::F7),
(0x64, KeyCode::F8),
(0x65, KeyCode::F9),
(0x6D, KeyCode::F10),
(0x67, KeyCode::F11),
(0x6F, KeyCode::F12),
] {
assert_eq!(key_code(code), Some(key), "{key} is {code:#04x}");
}
}
#[test]
fn every_named_key_is_reachable_except_the_four_macos_has_no_code_for() {
let reachable: Vec<KeyCode> = all_keycodes().filter_map(key_code).collect();
let missing: Vec<&'static str> = KeyCode::ALL
.iter()
.filter(|key| !reachable.contains(key))
.map(|key| key.as_str())
.collect();
assert_eq!(
missing,
["ContextMenu", "PrintScreen", "ScrollLock", "Pause"],
"macOS has a kVK_* code for every other key the seam names"
);
}
#[test]
fn no_two_virtual_key_codes_name_the_same_key() {
let mut seen: Vec<KeyCode> = all_keycodes().filter_map(key_code).collect();
let count = seen.len();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), count, "a kVK_* code is named twice");
assert_eq!(seen.len(), KeyCode::ALL.len() - 4, "the four above");
}
#[test]
fn the_holes_in_the_numbering_are_none_rather_than_a_wrong_key() {
for code in [
0x34u16, 0x3F, 0x40, 0x42, 0x44, 0x46, 0x48, 0x49, 0x4A, 0x4F, 0x50, 0x51, 0x5A, 0x5F,
0x66, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6E, 0x70, 0x71, 0x7F, 0x80, 0xFF,
] {
assert_eq!(key_code(code), None, "virtual key {code:#04x}");
}
}
#[test]
fn a_layout_character_becomes_its_unshifted_keysym() {
assert_eq!(keysym_from_character('W'), Keysym::from_char('w'));
assert_eq!(keysym_from_character('w'), Keysym::from_char('w'));
assert_eq!(keysym_from_character('/'), Keysym(0x2F));
assert_eq!(keysym_from_character('é'), Keysym(0xE9));
assert_eq!(keysym_from_character('€'), Keysym(0x0100_20AC));
}
#[test]
fn appkits_private_function_key_codepoints_are_not_symbols() {
for private in ['\u{f700}', '\u{f704}', '\u{f8ff}'] {
assert_eq!(keysym_from_character(private), Keysym::NONE, "{private:?}");
}
for control in ['\r', '\t', '\u{1b}', '\u{8}', '\u{7f}'] {
assert_eq!(keysym_from_character(control), Keysym::NONE, "{control:?}");
}
assert!(!keysym_from_character('\u{f6ff}').is_none());
}
#[test]
fn control_characters_are_not_text() {
for control in ['\u{8}', '\r', '\n', '\t', '\u{1b}', '\u{7f}'] {
assert!(!is_text(control), "{control:?}");
}
for text in ['a', ' ', 'é', '🎮', '日'] {
assert!(is_text(text), "{text:?}");
}
}
#[test]
fn the_numeric_pad_flag_is_never_read_as_num_lock() {
let arrow = modifiers(NUMERIC_PAD);
assert_eq!(arrow, Modifiers::empty(), "{arrow:?}");
assert!(!modifiers(NUMERIC_PAD | modifier::SHIFT).contains(Modifiers::NUM_LOCK));
}
#[test]
fn option_is_alt_and_never_alt_gr() {
let option = modifiers(modifier::OPTION);
assert!(option.contains(Modifiers::ALT));
assert!(!option.contains(Modifiers::ALT_GR), "{option:?}");
assert_eq!(option.chord(), Modifiers::ALT);
}
#[test]
fn command_is_super_and_caps_lock_is_latched() {
assert_eq!(modifiers(modifier::COMMAND), Modifiers::SUPER);
assert_eq!(modifiers(modifier::CONTROL), Modifiers::CTRL);
assert_eq!(modifiers(modifier::SHIFT), Modifiers::SHIFT);
assert_eq!(modifiers(modifier::CAPS_LOCK), Modifiers::CAPS_LOCK);
assert_eq!(modifiers(0), Modifiers::empty());
assert_eq!(modifiers(FUNCTION), Modifiers::empty(), "no fn");
let chord = modifiers(modifier::COMMAND | modifier::SHIFT | modifier::CAPS_LOCK);
assert_eq!(chord.chord(), Modifiers::SUPER | Modifiers::SHIFT);
}
#[test]
fn releasing_one_shift_of_a_two_shift_chord_reads_as_released() {
let both = modifier::SHIFT | modifier::DEVICE_LEFT_SHIFT | modifier::DEVICE_RIGHT_SHIFT;
assert_eq!(modifier_is_down(KeyCode::ShiftLeft, both), Some(true));
assert_eq!(modifier_is_down(KeyCode::ShiftRight, both), Some(true));
let right_only = modifier::SHIFT | modifier::DEVICE_RIGHT_SHIFT;
assert_eq!(
modifier_is_down(KeyCode::ShiftLeft, right_only),
Some(false)
);
assert_eq!(
modifier_is_down(KeyCode::ShiftRight, right_only),
Some(true)
);
assert!(
modifiers(right_only).contains(Modifiers::SHIFT),
"and Shift is still held, which is the whole difficulty"
);
}
#[test]
fn every_modifier_key_has_its_own_device_bit_and_nothing_else_has_one() {
let keys = [
KeyCode::ShiftLeft,
KeyCode::ShiftRight,
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::AltLeft,
KeyCode::AltRight,
KeyCode::SuperLeft,
KeyCode::SuperRight,
];
for held in keys {
for other in keys {
let down = modifier_is_down(other, device_bit_of(held))
.expect("every entry here is a modifier");
assert_eq!(
down,
other == held,
"{held} down reported {other} as {down}"
);
}
}
assert_eq!(modifier_is_down(KeyCode::CapsLock, 0), Some(false));
assert_eq!(
modifier_is_down(KeyCode::CapsLock, modifier::CAPS_LOCK),
Some(true)
);
assert_eq!(modifier_is_down(KeyCode::KeyW, NSUInteger::MAX), None);
assert_eq!(modifier_is_down(KeyCode::Space, 0), None);
}
fn device_bit_of(key: KeyCode) -> NSUInteger {
match key {
KeyCode::ShiftLeft => modifier::DEVICE_LEFT_SHIFT,
KeyCode::ShiftRight => modifier::DEVICE_RIGHT_SHIFT,
KeyCode::ControlLeft => modifier::DEVICE_LEFT_CONTROL,
KeyCode::ControlRight => modifier::DEVICE_RIGHT_CONTROL,
KeyCode::AltLeft => modifier::DEVICE_LEFT_OPTION,
KeyCode::AltRight => modifier::DEVICE_RIGHT_OPTION,
KeyCode::SuperLeft => modifier::DEVICE_LEFT_COMMAND,
KeyCode::SuperRight => modifier::DEVICE_RIGHT_COMMAND,
other => panic!("{other} is not a modifier with a device bit"),
}
}
}