use crcbl_core::KeyCode;
use crcbl_core::input::Modifiers;
use super::ffi::value;
use crate::WindowId;
const EXTENDED_BIT: isize = 1 << 24;
const PREVIOUS_STATE_BIT: isize = 1 << 30;
const EXTENDED_PREFIX: u32 = 0xE000;
#[must_use]
pub const fn scancode(l_param: isize) -> u32 {
let code = ((l_param >> 16) & 0xFF) as u32;
if l_param & EXTENDED_BIT == 0 {
code
} else {
EXTENDED_PREFIX | code
}
}
#[must_use]
pub const fn was_already_down(l_param: isize) -> bool {
l_param & PREVIOUS_STATE_BIT != 0
}
#[must_use]
pub const fn key_code(scancode: u32) -> Option<KeyCode> {
Some(match scancode {
0x0001 => KeyCode::Escape,
0x0002 => KeyCode::Digit1,
0x0003 => KeyCode::Digit2,
0x0004 => KeyCode::Digit3,
0x0005 => KeyCode::Digit4,
0x0006 => KeyCode::Digit5,
0x0007 => KeyCode::Digit6,
0x0008 => KeyCode::Digit7,
0x0009 => KeyCode::Digit8,
0x000A => KeyCode::Digit9,
0x000B => KeyCode::Digit0,
0x000C => KeyCode::Minus,
0x000D => KeyCode::Equal,
0x000E => KeyCode::Backspace,
0x000F => KeyCode::Tab,
0x0010 => KeyCode::KeyQ,
0x0011 => KeyCode::KeyW,
0x0012 => KeyCode::KeyE,
0x0013 => KeyCode::KeyR,
0x0014 => KeyCode::KeyT,
0x0015 => KeyCode::KeyY,
0x0016 => KeyCode::KeyU,
0x0017 => KeyCode::KeyI,
0x0018 => KeyCode::KeyO,
0x0019 => KeyCode::KeyP,
0x001A => KeyCode::BracketLeft,
0x001B => KeyCode::BracketRight,
0x001C => KeyCode::Enter,
0x001D => KeyCode::ControlLeft,
0x001E => KeyCode::KeyA,
0x001F => KeyCode::KeyS,
0x0020 => KeyCode::KeyD,
0x0021 => KeyCode::KeyF,
0x0022 => KeyCode::KeyG,
0x0023 => KeyCode::KeyH,
0x0024 => KeyCode::KeyJ,
0x0025 => KeyCode::KeyK,
0x0026 => KeyCode::KeyL,
0x0027 => KeyCode::Semicolon,
0x0028 => KeyCode::Quote,
0x0029 => KeyCode::Backquote,
0x002A => KeyCode::ShiftLeft,
0x002B => KeyCode::Backslash,
0x002C => KeyCode::KeyZ,
0x002D => KeyCode::KeyX,
0x002E => KeyCode::KeyC,
0x002F => KeyCode::KeyV,
0x0030 => KeyCode::KeyB,
0x0031 => KeyCode::KeyN,
0x0032 => KeyCode::KeyM,
0x0033 => KeyCode::Comma,
0x0034 => KeyCode::Period,
0x0035 => KeyCode::Slash,
0x0036 => KeyCode::ShiftRight,
0x0037 => KeyCode::NumpadMultiply,
0x0038 => KeyCode::AltLeft,
0x0039 => KeyCode::Space,
0x003A => KeyCode::CapsLock,
0x003B => KeyCode::F1,
0x003C => KeyCode::F2,
0x003D => KeyCode::F3,
0x003E => KeyCode::F4,
0x003F => KeyCode::F5,
0x0040 => KeyCode::F6,
0x0041 => KeyCode::F7,
0x0042 => KeyCode::F8,
0x0043 => KeyCode::F9,
0x0044 => KeyCode::F10,
0x0045 => KeyCode::Pause,
0x0046 => KeyCode::ScrollLock,
0x0047 => KeyCode::Numpad7,
0x0048 => KeyCode::Numpad8,
0x0049 => KeyCode::Numpad9,
0x004A => KeyCode::NumpadSubtract,
0x004B => KeyCode::Numpad4,
0x004C => KeyCode::Numpad5,
0x004D => KeyCode::Numpad6,
0x004E => KeyCode::NumpadAdd,
0x004F => KeyCode::Numpad1,
0x0050 => KeyCode::Numpad2,
0x0051 => KeyCode::Numpad3,
0x0052 => KeyCode::Numpad0,
0x0053 => KeyCode::NumpadDecimal,
0x0054 => KeyCode::PrintScreen,
0x0056 => KeyCode::IntlBackslash,
0x0057 => KeyCode::F11,
0x0058 => KeyCode::F12,
0x0073 => KeyCode::IntlRo,
0x007D => KeyCode::IntlYen,
0xE01C => KeyCode::NumpadEnter,
0xE01D => KeyCode::ControlRight,
0xE035 => KeyCode::NumpadDivide,
0xE037 => KeyCode::PrintScreen,
0xE038 => KeyCode::AltRight,
0xE045 => KeyCode::NumLock,
0xE047 => KeyCode::Home,
0xE048 => KeyCode::ArrowUp,
0xE049 => KeyCode::PageUp,
0xE04B => KeyCode::ArrowLeft,
0xE04D => KeyCode::ArrowRight,
0xE04F => KeyCode::End,
0xE050 => KeyCode::ArrowDown,
0xE051 => KeyCode::PageDown,
0xE052 => KeyCode::Insert,
0xE053 => KeyCode::Delete,
0xE05B => KeyCode::SuperLeft,
0xE05C => KeyCode::SuperRight,
0xE05D => KeyCode::ContextMenu,
_ => return None,
})
}
pub use crate::keysym::named_keysym;
const fn is_down(keys: &[u8; 256], key: usize) -> bool {
keys[key] & 0x80 != 0
}
const fn is_toggled(keys: &[u8; 256], key: usize) -> bool {
keys[key] & 0x01 != 0
}
#[must_use]
pub fn modifiers(keys: &[u8; 256]) -> Modifiers {
let left_control = is_down(keys, value::VK_L_CONTROL);
let right_control = is_down(keys, value::VK_R_CONTROL);
let left_alt = is_down(keys, value::VK_L_MENU);
let right_alt = is_down(keys, value::VK_R_MENU);
let alt_gr = right_alt && left_control;
let mut modifiers = Modifiers::empty();
modifiers.set(
Modifiers::SHIFT,
is_down(keys, value::VK_L_SHIFT) || is_down(keys, value::VK_R_SHIFT),
);
modifiers.set(Modifiers::CTRL, right_control || (left_control && !alt_gr));
modifiers.set(Modifiers::ALT, left_alt || (right_alt && !alt_gr));
modifiers.set(
Modifiers::SUPER,
is_down(keys, value::VK_L_WIN) || is_down(keys, value::VK_R_WIN),
);
modifiers.set(Modifiers::ALT_GR, alt_gr);
modifiers.set(Modifiers::CAPS_LOCK, is_toggled(keys, value::VK_CAPITAL));
modifiers.set(Modifiers::NUM_LOCK, is_toggled(keys, value::VK_NUM_LOCK));
modifiers
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Utf16 {
pending: Option<u16>,
}
const HIGH_SURROGATE_START: u16 = 0xD800;
const LOW_SURROGATE_START: u16 = 0xDC00;
const SURROGATE_END: u16 = 0xE000;
const ASTRAL_START: u32 = 0x1_0000;
impl Utf16 {
pub fn push(&mut self, unit: u16) -> Option<char> {
let high = self.pending.take();
if let Some(high) = high
&& (LOW_SURROGATE_START..SURROGATE_END).contains(&unit)
{
let codepoint = ASTRAL_START
+ (((high - HIGH_SURROGATE_START) as u32) << 10)
+ (unit - LOW_SURROGATE_START) as u32;
return char::from_u32(codepoint);
}
if (HIGH_SURROGATE_START..LOW_SURROGATE_START).contains(&unit) {
self.pending = Some(unit);
return None;
}
char::from_u32(unit as u32)
}
}
#[must_use]
pub fn is_text(character: char) -> bool {
!character.is_control()
}
#[must_use]
pub fn preedit(units: &[u16], cursor: Option<usize>) -> (String, Option<usize>) {
let mut text = String::with_capacity(units.len());
let mut at = None;
let mut consumed = 0;
for character in char::decode_utf16(units.iter().copied()) {
let character = character.unwrap_or(char::REPLACEMENT_CHARACTER);
if at.is_none() && cursor.is_some_and(|cursor| consumed + character.len_utf16() > cursor) {
at = Some(text.len());
}
consumed += character.len_utf16();
text.push(character);
}
if cursor.is_some() && at.is_none() {
at = Some(text.len());
}
if text.is_empty() {
return (text, None);
}
(text, at)
}
#[must_use]
pub const fn composition_changed(l_param: isize) -> bool {
(l_param as u32) & value::GCS_COMP_STR != 0
}
#[derive(Debug, Default)]
pub struct Composing {
windows: Vec<WindowId>,
}
impl Composing {
pub fn report(&mut self, window: WindowId, text: &str) -> bool {
let showing = self.windows.contains(&window);
if text.is_empty() {
self.forget(window);
return showing;
}
if !showing {
self.windows.push(window);
}
true
}
pub fn forget(&mut self, window: WindowId) {
self.windows.retain(|&known| known != window);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_composition_cursor_is_counted_in_code_units_and_reported_in_bytes() {
let ka: Vec<u16> = "かな".encode_utf16().collect();
assert_eq!(preedit(&ka, Some(0)), ("かな".to_owned(), Some(0)));
assert_eq!(preedit(&ka, Some(1)), ("かな".to_owned(), Some(3)));
assert_eq!(preedit(&ka, Some(2)), ("かな".to_owned(), Some(6)));
assert_eq!(preedit(&ka, Some(9)), ("かな".to_owned(), Some(6)));
assert_eq!(preedit(&ka, None), ("かな".to_owned(), None));
let astral: Vec<u16> = "a𠀋b".encode_utf16().collect();
assert_eq!(preedit(&astral, Some(3)), ("a𠀋b".to_owned(), Some(5)));
assert_eq!(preedit(&astral, Some(2)), ("a𠀋b".to_owned(), Some(1)));
assert_eq!(preedit(&astral, Some(1)), ("a𠀋b".to_owned(), Some(1)));
assert_eq!(preedit(&[], Some(0)), (String::new(), None));
assert_eq!(
preedit(&[0xD800, 0x61], None),
("\u{FFFD}a".to_owned(), None)
);
}
#[test]
fn only_a_changed_composition_string_is_a_pre_edit() {
const GCS_RESULT_STR: isize = 0x0800;
const GCS_COMP_ATTR: isize = 0x0010;
let comp_str = value::GCS_COMP_STR as isize;
assert!(composition_changed(comp_str));
assert!(composition_changed(comp_str | GCS_COMP_ATTR));
assert!(composition_changed(comp_str | GCS_RESULT_STR));
assert!(!composition_changed(GCS_RESULT_STR));
assert!(!composition_changed(0));
}
#[test]
fn a_composition_ends_once_and_only_where_one_was_shown() {
let window = |index: u64| WindowId::from_bits(1 << 32 | index).expect("a valid handle");
let (a, b) = (window(0), window(1));
let mut composing = Composing::default();
assert!(!composing.report(a, ""));
assert!(composing.report(a, "か"));
assert!(composing.report(a, "かな"), "each change is reported");
assert!(composing.report(b, "な"));
assert!(composing.report(a, ""));
assert!(!composing.report(a, ""), "the second end is dropped");
composing.forget(b);
assert!(!composing.report(b, ""));
}
const fn l_param(code: isize, extended: bool, previous: bool) -> isize {
let mut packed = code << 16;
if extended {
packed |= EXTENDED_BIT;
}
if previous {
packed |= PREVIOUS_STATE_BIT;
}
packed | 1
}
fn held(keys: &[usize]) -> [u8; 256] {
let mut state = [0u8; 256];
for key in keys {
state[*key] = 0x80;
}
state
}
#[test]
fn the_extended_bit_is_folded_into_the_scan_code_rather_than_dropped() {
assert_eq!(scancode(l_param(0x1C, false, false)), 0x001C);
assert_eq!(scancode(l_param(0x1C, true, false)), 0xE01C);
assert_eq!(key_code(0x001C), Some(KeyCode::Enter));
assert_eq!(key_code(0xE01C), Some(KeyCode::NumpadEnter));
assert_eq!(key_code(0x001D), Some(KeyCode::ControlLeft));
assert_eq!(key_code(0xE01D), Some(KeyCode::ControlRight));
assert_eq!(key_code(0x0038), Some(KeyCode::AltLeft));
assert_eq!(key_code(0xE038), Some(KeyCode::AltRight));
assert_eq!(key_code(0x0047), Some(KeyCode::Numpad7));
assert_eq!(key_code(0xE047), Some(KeyCode::Home));
assert_eq!(scancode(l_param(0x39, false, true)), 0x0039);
assert_eq!(scancode(-1), 0xE0FF, "every bit set is still one byte");
}
#[test]
fn pause_is_the_bare_forty_five_and_num_lock_is_the_extended_one() {
assert_eq!(key_code(0x0045), Some(KeyCode::Pause));
assert_eq!(key_code(0xE045), Some(KeyCode::NumLock));
}
#[test]
fn the_previous_state_bit_is_what_marks_an_auto_repeat() {
assert!(!was_already_down(l_param(0x11, false, false)));
assert!(was_already_down(l_param(0x11, false, true)));
}
#[test]
fn wasd_and_the_gameplay_keys_map_to_their_set_one_codes() {
assert_eq!(key_code(0x0011), Some(KeyCode::KeyW));
assert_eq!(key_code(0x001E), Some(KeyCode::KeyA));
assert_eq!(key_code(0x001F), Some(KeyCode::KeyS));
assert_eq!(key_code(0x0020), Some(KeyCode::KeyD));
assert_eq!(key_code(0x0039), Some(KeyCode::Space));
assert_eq!(key_code(0x0001), Some(KeyCode::Escape));
assert_eq!(key_code(0xE05B), Some(KeyCode::SuperLeft));
}
#[cfg(target_os = "linux")]
#[test]
fn the_two_numberings_coincide_for_the_letters_and_for_nothing_above_them() {
assert_eq!(crate::linux::keymap::key_code(17), Some(KeyCode::KeyW));
assert_eq!(key_code(17), Some(KeyCode::KeyW), "0x11 is W in both");
assert_eq!(crate::linux::keymap::key_code(30), Some(KeyCode::KeyA));
assert_eq!(key_code(30), Some(KeyCode::KeyA), "0x1E is A in both");
assert_eq!(
crate::linux::keymap::key_code(103),
Some(KeyCode::ArrowUp),
"evdev continues flat"
);
assert_eq!(key_code(103), None, "set 1 assigns nothing at 0x67");
assert_eq!(key_code(0xE048), Some(KeyCode::ArrowUp), "it is prefixed");
assert_eq!(
crate::linux::keymap::key_code(96),
Some(KeyCode::NumpadEnter)
);
assert_eq!(key_code(96), None);
assert_eq!(key_code(0xE01C), Some(KeyCode::NumpadEnter));
}
fn all_scancodes() -> impl Iterator<Item = u32> {
(0..=0xFFu32).chain((0..=0xFFu32).map(|code| EXTENDED_PREFIX | code))
}
#[test]
fn every_named_key_is_reachable_from_some_scan_code() {
let reachable: Vec<KeyCode> = all_scancodes().filter_map(key_code).collect();
let mut missing: Vec<&'static str> = Vec::new();
for key in KeyCode::ALL {
if !reachable.contains(key) {
missing.push(key.as_str());
}
}
assert!(missing.is_empty(), "unreachable KeyCodes: {missing:?}");
}
#[test]
fn print_screen_is_the_only_key_two_scan_codes_name() {
let mut seen: Vec<KeyCode> = all_scancodes().filter_map(key_code).collect();
let count = seen.len();
seen.sort_unstable();
seen.dedup();
assert_eq!(
count - seen.len(),
1,
"exactly one scan code pair may share a KeyCode"
);
assert_eq!(
seen.len(),
KeyCode::ALL.len(),
"the table is the vocabulary"
);
assert_eq!(key_code(0x0054), Some(KeyCode::PrintScreen));
assert_eq!(key_code(0xE037), Some(KeyCode::PrintScreen));
}
#[test]
fn unnamed_keys_degrade_to_none_rather_than_to_a_wrong_key() {
for code in [
0x0000, 0x0055, 0x0059, 0x0070, 0x0079, 0xE010, 0xE022, 0xE06F,
] {
assert_eq!(key_code(code), None, "scan code {code:#06x}");
}
}
#[test]
fn alt_gr_is_reported_as_itself_and_not_as_control_plus_alt() {
let alt_gr = modifiers(&held(&[value::VK_R_MENU, value::VK_L_CONTROL]));
assert!(alt_gr.contains(Modifiers::ALT_GR));
assert!(!alt_gr.contains(Modifiers::CTRL), "{alt_gr:?}");
assert!(!alt_gr.contains(Modifiers::ALT), "{alt_gr:?}");
assert_eq!(alt_gr.chord(), Modifiers::empty());
let plain = modifiers(&held(&[value::VK_R_MENU]));
assert_eq!(plain, Modifiers::ALT);
let real = modifiers(&held(&[value::VK_L_CONTROL, value::VK_L_MENU]));
assert_eq!(real, Modifiers::CTRL | Modifiers::ALT);
let both = modifiers(&held(&[
value::VK_R_MENU,
value::VK_L_CONTROL,
value::VK_R_CONTROL,
]));
assert!(both.contains(Modifiers::ALT_GR | Modifiers::CTRL));
}
#[test]
fn shift_and_super_come_from_either_side_and_the_locks_from_the_toggle_bit() {
assert_eq!(modifiers(&held(&[value::VK_L_SHIFT])), Modifiers::SHIFT);
assert_eq!(modifiers(&held(&[value::VK_R_SHIFT])), Modifiers::SHIFT);
assert_eq!(modifiers(&held(&[value::VK_L_WIN])), Modifiers::SUPER);
assert_eq!(modifiers(&held(&[value::VK_R_WIN])), Modifiers::SUPER);
assert_eq!(modifiers(&[0u8; 256]), Modifiers::empty());
let mut state = [0u8; 256];
state[value::VK_CAPITAL] = 0x01;
state[value::VK_NUM_LOCK] = 0x01;
assert_eq!(
modifiers(&state),
Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK
);
let mut pressing = [0u8; 256];
pressing[value::VK_CAPITAL] = 0x80;
assert_eq!(modifiers(&pressing), Modifiers::empty());
}
#[test]
fn a_surrogate_pair_becomes_one_character_and_a_lone_half_becomes_none() {
let mut text = Utf16::default();
assert_eq!(text.push(0xD83C), None, "the high half commits nothing");
assert_eq!(text.push(0xDFAE), Some('🎮'));
assert_eq!(text, Utf16::default(), "and the pair is not held on to");
assert_eq!(text.push(0x3042), Some('あ'));
assert_eq!(text.push(u16::from(b'q')), Some('q'));
assert_eq!(text.push(0xD83C), None);
assert_eq!(text.push(u16::from(b'a')), Some('a'));
assert_eq!(text.push(0xDFAE), None);
assert_eq!(text.push(u16::from(b'b')), Some('b'));
}
#[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:?}");
}
}
}