use crate::key::{Key, KeyChord, Modifiers, NamedKey};
use winit::event::{ElementState, KeyEvent};
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey as WNamed};
pub fn key_event_to_chord(event: &KeyEvent, modifiers: ModifiersState) -> Option<KeyChord> {
if event.state != ElementState::Pressed {
return None;
}
if event.repeat {
return None;
}
chord_from_logical(&event.logical_key, modifiers)
}
pub fn key_event_to_chord_with_repeat(
event: &KeyEvent,
modifiers: ModifiersState,
) -> Option<KeyChord> {
if event.state != ElementState::Pressed {
return None;
}
chord_from_logical(&event.logical_key, modifiers)
}
fn chord_from_logical(logical: &WKey, modifiers: ModifiersState) -> Option<KeyChord> {
let mods = modifiers_to_internal(modifiers);
match logical {
WKey::Character(s) => {
let mut chars = s.chars();
let first = chars.next()?;
if chars.next().is_some() {
return None;
}
let mut effective = mods;
let mut ch = first;
if effective.contains(Modifiers::SHIFT)
&& first.is_ascii()
&& !first.is_ascii_alphabetic()
{
effective.remove(Modifiers::SHIFT);
}
if effective.contains(Modifiers::CTRL) && ch.is_ascii_alphabetic() {
ch = ch.to_ascii_lowercase();
}
Some(KeyChord {
modifiers: effective,
key: Key::Char(ch),
})
}
WKey::Named(named) => {
if matches!(named, WNamed::Space) {
return Some(KeyChord {
modifiers: mods,
key: Key::Char(' '),
});
}
let mapped = map_named(*named)?;
Some(KeyChord {
modifiers: mods,
key: Key::Named(mapped),
})
}
_ => None,
}
}
fn modifiers_to_internal(m: ModifiersState) -> Modifiers {
let mut out = Modifiers::empty();
if m.shift_key() {
out |= Modifiers::SHIFT;
}
if m.control_key() {
out |= Modifiers::CTRL;
}
if m.alt_key() {
out |= Modifiers::ALT;
}
if m.super_key() {
out |= Modifiers::SUPER;
}
out
}
fn map_named(n: WNamed) -> Option<NamedKey> {
Some(match n {
WNamed::Escape => NamedKey::Esc,
WNamed::Enter => NamedKey::CR,
WNamed::Tab => NamedKey::Tab,
WNamed::Backspace => NamedKey::BS,
WNamed::Space => NamedKey::Space,
WNamed::ArrowUp => NamedKey::Up,
WNamed::ArrowDown => NamedKey::Down,
WNamed::ArrowLeft => NamedKey::Left,
WNamed::ArrowRight => NamedKey::Right,
WNamed::Home => NamedKey::Home,
WNamed::End => NamedKey::End,
WNamed::PageUp => NamedKey::PageUp,
WNamed::PageDown => NamedKey::PageDown,
WNamed::Insert => NamedKey::Insert,
WNamed::Delete => NamedKey::Delete,
WNamed::F1 => NamedKey::F(1),
WNamed::F2 => NamedKey::F(2),
WNamed::F3 => NamedKey::F(3),
WNamed::F4 => NamedKey::F(4),
WNamed::F5 => NamedKey::F(5),
WNamed::F6 => NamedKey::F(6),
WNamed::F7 => NamedKey::F(7),
WNamed::F8 => NamedKey::F(8),
WNamed::F9 => NamedKey::F(9),
WNamed::F10 => NamedKey::F(10),
WNamed::F11 => NamedKey::F(11),
WNamed::F12 => NamedKey::F(12),
_ => return None,
})
}
#[cfg(test)]
fn translate_key_test_only(logical_key: &WKey, modifiers: ModifiersState) -> Option<KeyChord> {
let mods = modifiers_to_internal(modifiers);
match logical_key {
WKey::Character(s) => {
let mut chars = s.chars();
let first = chars.next()?;
if chars.next().is_some() {
return None;
}
Some(KeyChord {
modifiers: mods,
key: Key::Char(first),
})
}
WKey::Named(named) => {
if matches!(named, WNamed::Space) {
return Some(KeyChord {
modifiers: mods,
key: Key::Char(' '),
});
}
let mapped = map_named(*named)?;
Some(KeyChord {
modifiers: mods,
key: Key::Named(mapped),
})
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use winit::keyboard::SmolStr;
fn translate(k: WKey, m: ModifiersState) -> Option<KeyChord> {
translate_key_test_only(&k, m)
}
#[test]
fn plain_j_is_char_j_no_modifiers() {
let chord =
translate(WKey::Character(SmolStr::new("j")), ModifiersState::empty()).expect("some");
assert_eq!(chord.key, Key::Char('j'));
assert!(chord.modifiers.is_empty());
}
#[test]
fn shift_j_carries_uppercase_and_shift_flag() {
let chord =
translate(WKey::Character(SmolStr::new("J")), ModifiersState::SHIFT).expect("some");
assert_eq!(chord.key, Key::Char('J'));
assert!(chord.modifiers.contains(Modifiers::SHIFT));
}
#[test]
fn ctrl_w_includes_ctrl_modifier() {
let chord =
translate(WKey::Character(SmolStr::new("w")), ModifiersState::CONTROL).expect("some");
assert!(chord.modifiers.contains(Modifiers::CTRL));
assert_eq!(chord.key, Key::Char('w'));
}
#[test]
fn escape_named_key() {
let chord = translate(WKey::Named(WNamed::Escape), ModifiersState::empty()).expect("some");
assert_eq!(chord.key, Key::Named(NamedKey::Esc));
}
#[test]
fn arrow_keys_map() {
for (named, expect) in [
(WNamed::ArrowUp, NamedKey::Up),
(WNamed::ArrowDown, NamedKey::Down),
(WNamed::ArrowLeft, NamedKey::Left),
(WNamed::ArrowRight, NamedKey::Right),
] {
let chord = translate(WKey::Named(named), ModifiersState::empty()).unwrap();
assert_eq!(chord.key, Key::Named(expect));
}
}
#[test]
fn function_keys_f1_through_f12() {
let pairs = [
(WNamed::F1, 1u8),
(WNamed::F2, 2),
(WNamed::F3, 3),
(WNamed::F4, 4),
(WNamed::F5, 5),
(WNamed::F6, 6),
(WNamed::F7, 7),
(WNamed::F8, 8),
(WNamed::F9, 9),
(WNamed::F10, 10),
(WNamed::F11, 11),
(WNamed::F12, 12),
];
for (named, n) in pairs {
let chord = translate(WKey::Named(named), ModifiersState::empty()).unwrap();
assert_eq!(chord.key, Key::Named(NamedKey::F(n)));
}
}
#[test]
fn multi_codepoint_character_drops() {
let chord = translate(
WKey::Character(SmolStr::new("a\u{0302}")),
ModifiersState::empty(),
);
assert!(chord.is_none());
}
#[test]
fn unmapped_named_returns_none() {
let chord = translate(WKey::Named(WNamed::CapsLock), ModifiersState::empty());
assert!(chord.is_none());
}
#[test]
fn ctrl_modifier_combined_with_named() {
let chord = translate(
WKey::Named(WNamed::Tab),
ModifiersState::CONTROL | ModifiersState::SHIFT,
)
.unwrap();
assert_eq!(chord.key, Key::Named(NamedKey::Tab));
assert!(chord.modifiers.contains(Modifiers::CTRL));
assert!(chord.modifiers.contains(Modifiers::SHIFT));
}
}