use crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers as CtKeyModifiers, MediaKeyCode, ModifierKeyCode,
MouseButton as CtMouseButton, MouseEvent, MouseEventKind,
};
use rdom_core::{KeyboardDetail, KeyboardModifiers, MouseButton, MouseDetail};
pub fn translate_key_event(ev: KeyEvent) -> KeyboardDetail {
KeyboardDetail {
key: key_code_to_string(ev.code),
modifiers: translate_modifiers(ev.modifiers),
repeat: matches!(ev.kind, KeyEventKind::Repeat),
}
}
pub fn key_code_to_string(code: KeyCode) -> String {
match code {
KeyCode::Backspace => "Backspace".into(),
KeyCode::Enter => "Enter".into(),
KeyCode::Left => "ArrowLeft".into(),
KeyCode::Right => "ArrowRight".into(),
KeyCode::Up => "ArrowUp".into(),
KeyCode::Down => "ArrowDown".into(),
KeyCode::Home => "Home".into(),
KeyCode::End => "End".into(),
KeyCode::PageUp => "PageUp".into(),
KeyCode::PageDown => "PageDown".into(),
KeyCode::Tab => "Tab".into(),
KeyCode::BackTab => "Tab".into(),
KeyCode::Delete => "Delete".into(),
KeyCode::Insert => "Insert".into(),
KeyCode::F(n) => format!("F{n}"),
KeyCode::Char(c) => c.to_string(),
KeyCode::Null => "Unidentified".into(),
KeyCode::Esc => "Escape".into(),
KeyCode::CapsLock => "CapsLock".into(),
KeyCode::ScrollLock => "ScrollLock".into(),
KeyCode::NumLock => "NumLock".into(),
KeyCode::PrintScreen => "PrintScreen".into(),
KeyCode::Pause => "Pause".into(),
KeyCode::Menu => "ContextMenu".into(),
KeyCode::KeypadBegin => "Clear".into(),
KeyCode::Media(m) => media_key_to_string(m).into(),
KeyCode::Modifier(m) => modifier_key_to_string(m).into(),
}
}
fn media_key_to_string(m: MediaKeyCode) -> &'static str {
match m {
MediaKeyCode::Play => "MediaPlay",
MediaKeyCode::Pause => "MediaPause",
MediaKeyCode::PlayPause => "MediaPlayPause",
MediaKeyCode::Reverse => "MediaRewind",
MediaKeyCode::Stop => "MediaStop",
MediaKeyCode::FastForward => "MediaFastForward",
MediaKeyCode::Rewind => "MediaRewind",
MediaKeyCode::TrackNext => "MediaTrackNext",
MediaKeyCode::TrackPrevious => "MediaTrackPrevious",
MediaKeyCode::Record => "MediaRecord",
MediaKeyCode::LowerVolume => "AudioVolumeDown",
MediaKeyCode::RaiseVolume => "AudioVolumeUp",
MediaKeyCode::MuteVolume => "AudioVolumeMute",
}
}
fn modifier_key_to_string(m: ModifierKeyCode) -> &'static str {
match m {
ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => "Shift",
ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => "Control",
ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => "Alt",
ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => "Super",
ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => "Hyper",
ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => "Meta",
ModifierKeyCode::IsoLevel3Shift | ModifierKeyCode::IsoLevel5Shift => "AltGraph",
}
}
pub fn translate_modifiers(m: CtKeyModifiers) -> KeyboardModifiers {
KeyboardModifiers {
ctrl: m.contains(CtKeyModifiers::CONTROL),
shift: m.contains(CtKeyModifiers::SHIFT),
alt: m.contains(CtKeyModifiers::ALT),
meta: m.contains(CtKeyModifiers::SUPER) || m.contains(CtKeyModifiers::META),
}
}
pub fn translate_mouse_event(ev: MouseEvent) -> MouseDetail {
let (button, buttons) = mouse_kind_to_button(ev.kind);
let (delta_x, delta_y) = wheel_delta(ev.kind);
MouseDetail {
button,
buttons,
client_x: ev.column as i32,
client_y: ev.row as i32,
delta_x,
delta_y,
modifiers: translate_modifiers(ev.modifiers),
}
}
fn wheel_delta(kind: MouseEventKind) -> (i32, i32) {
match kind {
MouseEventKind::ScrollUp => (0, -1),
MouseEventKind::ScrollDown => (0, 1),
MouseEventKind::ScrollLeft => (-1, 0),
MouseEventKind::ScrollRight => (1, 0),
MouseEventKind::Down(_)
| MouseEventKind::Up(_)
| MouseEventKind::Drag(_)
| MouseEventKind::Moved => (0, 0),
}
}
fn mouse_kind_to_button(kind: MouseEventKind) -> (MouseButton, u8) {
match kind {
MouseEventKind::Down(b) => (translate_mouse_button(b), button_bit(b)),
MouseEventKind::Up(b) => (translate_mouse_button(b), 0),
MouseEventKind::Drag(b) => (translate_mouse_button(b), button_bit(b)),
MouseEventKind::Moved
| MouseEventKind::ScrollUp
| MouseEventKind::ScrollDown
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => (MouseButton::Left, 0),
}
}
fn translate_mouse_button(b: CtMouseButton) -> MouseButton {
match b {
CtMouseButton::Left => MouseButton::Left,
CtMouseButton::Middle => MouseButton::Middle,
CtMouseButton::Right => MouseButton::Right,
}
}
fn button_bit(b: CtMouseButton) -> u8 {
match b {
CtMouseButton::Left => 1, CtMouseButton::Right => 2, CtMouseButton::Middle => 4, }
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyEventKind, KeyEventState};
fn key(code: KeyCode, modifiers: CtKeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}
}
#[test]
fn navigation_keys() {
assert_eq!(key_code_to_string(KeyCode::Enter), "Enter");
assert_eq!(key_code_to_string(KeyCode::Esc), "Escape");
assert_eq!(key_code_to_string(KeyCode::Tab), "Tab");
assert_eq!(key_code_to_string(KeyCode::BackTab), "Tab");
assert_eq!(key_code_to_string(KeyCode::Backspace), "Backspace");
assert_eq!(key_code_to_string(KeyCode::Delete), "Delete");
assert_eq!(key_code_to_string(KeyCode::Insert), "Insert");
}
#[test]
fn arrow_keys() {
assert_eq!(key_code_to_string(KeyCode::Up), "ArrowUp");
assert_eq!(key_code_to_string(KeyCode::Down), "ArrowDown");
assert_eq!(key_code_to_string(KeyCode::Left), "ArrowLeft");
assert_eq!(key_code_to_string(KeyCode::Right), "ArrowRight");
}
#[test]
fn paging_keys() {
assert_eq!(key_code_to_string(KeyCode::Home), "Home");
assert_eq!(key_code_to_string(KeyCode::End), "End");
assert_eq!(key_code_to_string(KeyCode::PageUp), "PageUp");
assert_eq!(key_code_to_string(KeyCode::PageDown), "PageDown");
}
#[test]
fn lock_keys() {
assert_eq!(key_code_to_string(KeyCode::CapsLock), "CapsLock");
assert_eq!(key_code_to_string(KeyCode::NumLock), "NumLock");
assert_eq!(key_code_to_string(KeyCode::ScrollLock), "ScrollLock");
}
#[test]
fn screen_and_pause_keys() {
assert_eq!(key_code_to_string(KeyCode::PrintScreen), "PrintScreen");
assert_eq!(key_code_to_string(KeyCode::Pause), "Pause");
assert_eq!(key_code_to_string(KeyCode::Menu), "ContextMenu");
assert_eq!(key_code_to_string(KeyCode::KeypadBegin), "Clear");
}
#[test]
fn null_is_unidentified() {
assert_eq!(key_code_to_string(KeyCode::Null), "Unidentified");
}
#[test]
fn f_keys_one_through_twelve() {
for n in 1..=12u8 {
assert_eq!(key_code_to_string(KeyCode::F(n)), format!("F{n}"));
}
}
#[test]
fn f_keys_high_numbers() {
assert_eq!(key_code_to_string(KeyCode::F(24)), "F24");
}
#[test]
fn printable_chars() {
assert_eq!(key_code_to_string(KeyCode::Char('a')), "a");
assert_eq!(key_code_to_string(KeyCode::Char('A')), "A");
assert_eq!(key_code_to_string(KeyCode::Char('1')), "1");
assert_eq!(key_code_to_string(KeyCode::Char('!')), "!");
assert_eq!(key_code_to_string(KeyCode::Char(' ')), " ");
assert_eq!(key_code_to_string(KeyCode::Char('é')), "é");
}
#[test]
fn media_keys() {
use MediaKeyCode::*;
assert_eq!(key_code_to_string(KeyCode::Media(Play)), "MediaPlay");
assert_eq!(key_code_to_string(KeyCode::Media(Pause)), "MediaPause");
assert_eq!(
key_code_to_string(KeyCode::Media(PlayPause)),
"MediaPlayPause"
);
assert_eq!(key_code_to_string(KeyCode::Media(Stop)), "MediaStop");
assert_eq!(
key_code_to_string(KeyCode::Media(FastForward)),
"MediaFastForward"
);
assert_eq!(key_code_to_string(KeyCode::Media(Rewind)), "MediaRewind");
assert_eq!(key_code_to_string(KeyCode::Media(Reverse)), "MediaRewind");
assert_eq!(
key_code_to_string(KeyCode::Media(TrackNext)),
"MediaTrackNext"
);
assert_eq!(
key_code_to_string(KeyCode::Media(TrackPrevious)),
"MediaTrackPrevious"
);
assert_eq!(key_code_to_string(KeyCode::Media(Record)), "MediaRecord");
assert_eq!(
key_code_to_string(KeyCode::Media(LowerVolume)),
"AudioVolumeDown"
);
assert_eq!(
key_code_to_string(KeyCode::Media(RaiseVolume)),
"AudioVolumeUp"
);
assert_eq!(
key_code_to_string(KeyCode::Media(MuteVolume)),
"AudioVolumeMute"
);
}
#[test]
fn modifier_only_keys_collapse_left_and_right() {
use ModifierKeyCode::*;
assert_eq!(key_code_to_string(KeyCode::Modifier(LeftShift)), "Shift");
assert_eq!(key_code_to_string(KeyCode::Modifier(RightShift)), "Shift");
assert_eq!(
key_code_to_string(KeyCode::Modifier(LeftControl)),
"Control"
);
assert_eq!(
key_code_to_string(KeyCode::Modifier(RightControl)),
"Control"
);
assert_eq!(key_code_to_string(KeyCode::Modifier(LeftAlt)), "Alt");
assert_eq!(key_code_to_string(KeyCode::Modifier(RightAlt)), "Alt");
assert_eq!(key_code_to_string(KeyCode::Modifier(LeftSuper)), "Super");
assert_eq!(key_code_to_string(KeyCode::Modifier(RightSuper)), "Super");
assert_eq!(key_code_to_string(KeyCode::Modifier(LeftHyper)), "Hyper");
assert_eq!(key_code_to_string(KeyCode::Modifier(RightHyper)), "Hyper");
assert_eq!(key_code_to_string(KeyCode::Modifier(LeftMeta)), "Meta");
assert_eq!(key_code_to_string(KeyCode::Modifier(RightMeta)), "Meta");
assert_eq!(
key_code_to_string(KeyCode::Modifier(IsoLevel3Shift)),
"AltGraph"
);
assert_eq!(
key_code_to_string(KeyCode::Modifier(IsoLevel5Shift)),
"AltGraph"
);
}
#[test]
fn modifiers_default_when_no_flags() {
let m = translate_modifiers(CtKeyModifiers::empty());
assert!(!m.ctrl && !m.shift && !m.alt && !m.meta);
}
#[test]
fn modifiers_each_flag() {
let m = translate_modifiers(CtKeyModifiers::CONTROL);
assert!(m.ctrl && !m.shift && !m.alt && !m.meta);
let m = translate_modifiers(CtKeyModifiers::SHIFT);
assert!(!m.ctrl && m.shift && !m.alt && !m.meta);
let m = translate_modifiers(CtKeyModifiers::ALT);
assert!(!m.ctrl && !m.shift && m.alt && !m.meta);
}
#[test]
fn modifiers_super_and_meta_both_feed_meta() {
let from_super = translate_modifiers(CtKeyModifiers::SUPER);
assert!(from_super.meta);
let from_meta = translate_modifiers(CtKeyModifiers::META);
assert!(from_meta.meta);
let both = translate_modifiers(CtKeyModifiers::SUPER | CtKeyModifiers::META);
assert!(both.meta);
}
#[test]
fn modifiers_combined() {
let m = translate_modifiers(CtKeyModifiers::CONTROL | CtKeyModifiers::SHIFT);
assert!(m.ctrl && m.shift && !m.alt && !m.meta);
}
#[test]
fn repeat_unset_when_press_kind() {
let d = translate_key_event(key(KeyCode::Char('a'), CtKeyModifiers::empty()));
assert!(!d.repeat);
}
#[test]
fn repeat_set_when_repeat_kind() {
let mut k = key(KeyCode::Char('a'), CtKeyModifiers::empty());
k.kind = KeyEventKind::Repeat;
let d = translate_key_event(k);
assert!(d.repeat);
}
#[test]
fn repeat_unset_when_release_kind() {
let mut k = key(KeyCode::Char('a'), CtKeyModifiers::empty());
k.kind = KeyEventKind::Release;
let d = translate_key_event(k);
assert!(!d.repeat, "Release isn't a repeat");
}
#[test]
fn translate_enter_with_ctrl() {
let d = translate_key_event(key(KeyCode::Enter, CtKeyModifiers::CONTROL));
assert_eq!(d.key, "Enter");
assert!(d.modifiers.ctrl);
assert!(!d.modifiers.shift);
}
#[test]
fn translate_backtab_reports_shifted_tab() {
let d = translate_key_event(key(KeyCode::BackTab, CtKeyModifiers::SHIFT));
assert_eq!(d.key, "Tab");
assert!(d.modifiers.shift);
}
fn mouse(kind: MouseEventKind, column: u16, row: u16, m: CtKeyModifiers) -> MouseEvent {
MouseEvent {
kind,
column,
row,
modifiers: m,
}
}
#[test]
fn mouse_down_left_sets_bit_zero() {
let d = translate_mouse_event(mouse(
MouseEventKind::Down(CtMouseButton::Left),
5,
3,
CtKeyModifiers::empty(),
));
assert_eq!(d.button, MouseButton::Left);
assert_eq!(d.buttons, 1);
assert_eq!(d.client_x, 5);
assert_eq!(d.client_y, 3);
assert!(!d.modifiers.ctrl);
}
#[test]
fn mouse_down_right_sets_bit_one() {
let d = translate_mouse_event(mouse(
MouseEventKind::Down(CtMouseButton::Right),
0,
0,
CtKeyModifiers::empty(),
));
assert_eq!(d.button, MouseButton::Right);
assert_eq!(d.buttons, 2);
}
#[test]
fn mouse_down_middle_sets_bit_two() {
let d = translate_mouse_event(mouse(
MouseEventKind::Down(CtMouseButton::Middle),
0,
0,
CtKeyModifiers::empty(),
));
assert_eq!(d.button, MouseButton::Middle);
assert_eq!(d.buttons, 4);
}
#[test]
fn mouse_up_clears_buttons() {
let d = translate_mouse_event(mouse(
MouseEventKind::Up(CtMouseButton::Left),
0,
0,
CtKeyModifiers::empty(),
));
assert_eq!(d.button, MouseButton::Left);
assert_eq!(d.buttons, 0);
}
#[test]
fn mouse_drag_carries_button_bit() {
let d = translate_mouse_event(mouse(
MouseEventKind::Drag(CtMouseButton::Left),
7,
2,
CtKeyModifiers::empty(),
));
assert_eq!(d.button, MouseButton::Left);
assert_eq!(d.buttons, 1);
}
#[test]
fn mouse_moved_defaults_to_left_no_bits() {
let d = translate_mouse_event(mouse(MouseEventKind::Moved, 10, 4, CtKeyModifiers::empty()));
assert_eq!(d.button, MouseButton::Left);
assert_eq!(d.buttons, 0);
}
#[test]
fn mouse_scroll_directions_default_to_left() {
for kind in [
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollLeft,
MouseEventKind::ScrollRight,
] {
let d = translate_mouse_event(mouse(kind, 0, 0, CtKeyModifiers::empty()));
assert_eq!(d.button, MouseButton::Left);
assert_eq!(d.buttons, 0);
}
}
#[test]
fn mouse_scroll_deltas_match_direction() {
let cases = [
(MouseEventKind::ScrollUp, (0, -1)),
(MouseEventKind::ScrollDown, (0, 1)),
(MouseEventKind::ScrollLeft, (-1, 0)),
(MouseEventKind::ScrollRight, (1, 0)),
];
for (kind, (dx, dy)) in cases {
let d = translate_mouse_event(mouse(kind, 0, 0, CtKeyModifiers::empty()));
assert_eq!(d.delta_x, dx);
assert_eq!(d.delta_y, dy);
}
}
#[test]
fn non_wheel_events_have_zero_deltas() {
let d = translate_mouse_event(mouse(
MouseEventKind::Down(CtMouseButton::Left),
0,
0,
CtKeyModifiers::empty(),
));
assert_eq!(d.delta_x, 0);
assert_eq!(d.delta_y, 0);
}
#[test]
fn mouse_modifiers_propagate() {
let d = translate_mouse_event(mouse(
MouseEventKind::Down(CtMouseButton::Left),
0,
0,
CtKeyModifiers::CONTROL | CtKeyModifiers::SHIFT,
));
assert!(d.modifiers.ctrl);
assert!(d.modifiers.shift);
assert!(!d.modifiers.alt);
}
}