use eframe::egui;
use ratatui::crossterm::event::{
KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crate::grid::Metrics;
pub enum Input {
Key(KeyEvent),
Mouse(MouseEvent),
}
pub fn collect(input: &egui::InputState, origin: egui::Pos2, metrics: Metrics) -> Vec<Input> {
let mut out = Vec::new();
let modifiers = to_modifiers(&input.modifiers);
let at = |pos: egui::Pos2| -> (u16, u16) {
let col = ((pos.x - origin.x) / metrics.width).floor().max(0.0);
let row = ((pos.y - origin.y) / metrics.height).floor().max(0.0);
(
col.min(u16::MAX as f32) as u16,
row.min(u16::MAX as f32) as u16,
)
};
for event in &input.events {
match event {
egui::Event::Text(text) if !input.modifiers.alt && !input.modifiers.command => {
for c in text.chars() {
if matches!(c, '\n' | '\r' | '\t' | '\u{7f}') {
continue;
}
out.push(Input::Key(KeyEvent::new(
KeyCode::Char(c),
modifiers - KeyModifiers::SHIFT,
)));
}
}
egui::Event::Key {
key,
pressed: true,
modifiers: mods,
..
} => {
let mods_ct = to_modifiers(mods);
let Some(code) = to_code(*key) else {
continue;
};
if matches!(code, KeyCode::Char(_)) && !mods.ctrl && !mods.alt && !mods.command {
continue;
}
let (code, mods_ct) = match code {
KeyCode::Char(c) if mods.shift && c.is_alphabetic() => (
KeyCode::Char(c.to_ascii_uppercase()),
mods_ct - KeyModifiers::SHIFT,
),
_ => (code, mods_ct),
};
out.push(Input::Key(KeyEvent::new(code, mods_ct)));
}
egui::Event::PointerButton {
pos,
button,
pressed: true,
..
} => {
let (column, row) = at(*pos);
out.push(Input::Mouse(MouseEvent {
kind: MouseEventKind::Down(to_button(*button)),
column,
row,
modifiers,
}));
}
egui::Event::MouseWheel { unit, delta, .. } => {
let Some(pos) = input.pointer.latest_pos() else {
continue;
};
let lines = match unit {
egui::MouseWheelUnit::Line => delta.y.abs(),
egui::MouseWheelUnit::Page => delta.y.abs() * 10.0,
egui::MouseWheelUnit::Point => delta.y.abs() / metrics.height,
};
let notches = lines.ceil().min(5.0) as usize;
if notches == 0 || delta.y == 0.0 {
continue;
}
let kind = if delta.y > 0.0 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
let (column, row) = at(pos);
for _ in 0..notches {
out.push(Input::Mouse(MouseEvent {
kind,
column,
row,
modifiers,
}));
}
}
_ => {}
}
}
out
}
fn to_modifiers(mods: &egui::Modifiers) -> KeyModifiers {
let mut out = KeyModifiers::NONE;
if mods.shift {
out |= KeyModifiers::SHIFT;
}
if mods.ctrl || mods.mac_cmd {
out |= KeyModifiers::CONTROL;
}
if mods.alt {
out |= KeyModifiers::ALT;
}
out
}
fn to_button(button: egui::PointerButton) -> MouseButton {
match button {
egui::PointerButton::Secondary => MouseButton::Right,
egui::PointerButton::Middle => MouseButton::Middle,
_ => MouseButton::Left,
}
}
fn to_code(key: egui::Key) -> Option<KeyCode> {
use egui::Key as K;
let code = match key {
K::ArrowDown => KeyCode::Down,
K::ArrowLeft => KeyCode::Left,
K::ArrowRight => KeyCode::Right,
K::ArrowUp => KeyCode::Up,
K::Escape => KeyCode::Esc,
K::Tab => KeyCode::Tab,
K::Backspace => KeyCode::Backspace,
K::Enter => KeyCode::Enter,
K::Space => KeyCode::Char(' '),
K::Insert => KeyCode::Insert,
K::Delete => KeyCode::Delete,
K::Home => KeyCode::Home,
K::End => KeyCode::End,
K::PageUp => KeyCode::PageUp,
K::PageDown => KeyCode::PageDown,
K::Copy => return None,
K::Cut => return None,
K::Paste => return None,
K::Colon => KeyCode::Char(':'),
K::Comma => KeyCode::Char(','),
K::Backslash => KeyCode::Char('\\'),
K::Slash => KeyCode::Char('/'),
K::Pipe => KeyCode::Char('|'),
K::Questionmark => KeyCode::Char('?'),
K::Exclamationmark => KeyCode::Char('!'),
K::OpenBracket => KeyCode::Char('['),
K::CloseBracket => KeyCode::Char(']'),
K::OpenCurlyBracket => KeyCode::Char('{'),
K::CloseCurlyBracket => KeyCode::Char('}'),
K::Backtick => KeyCode::Char('`'),
K::Minus => KeyCode::Char('-'),
K::Period => KeyCode::Char('.'),
K::Plus => KeyCode::Char('+'),
K::Equals => KeyCode::Char('='),
K::Semicolon => KeyCode::Char(';'),
K::Quote => KeyCode::Char('\''),
K::F1 => KeyCode::F(1),
K::F2 => KeyCode::F(2),
K::F3 => KeyCode::F(3),
K::F4 => KeyCode::F(4),
K::F5 => KeyCode::F(5),
K::F6 => KeyCode::F(6),
K::F7 => KeyCode::F(7),
K::F8 => KeyCode::F(8),
K::F9 => KeyCode::F(9),
K::F10 => KeyCode::F(10),
K::F11 => KeyCode::F(11),
K::F12 => KeyCode::F(12),
K::F13 => KeyCode::F(13),
K::F14 => KeyCode::F(14),
K::F15 => KeyCode::F(15),
K::F16 => KeyCode::F(16),
K::F17 => KeyCode::F(17),
K::F18 => KeyCode::F(18),
K::F19 => KeyCode::F(19),
K::F20 => KeyCode::F(20),
K::F21 => KeyCode::F(21),
K::F22 => KeyCode::F(22),
K::F23 => KeyCode::F(23),
K::F24 => KeyCode::F(24),
K::F25
| K::F26
| K::F27
| K::F28
| K::F29
| K::F30
| K::F31
| K::F32
| K::F33
| K::F34
| K::F35 => return None,
other => {
let name = other.name();
let mut chars = name.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => KeyCode::Char(c.to_ascii_lowercase()),
_ => return None,
}
}
};
Some(code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_named_keys_are_the_terminal_ones() {
assert_eq!(to_code(egui::Key::F5), Some(KeyCode::F(5)));
assert_eq!(to_code(egui::Key::PageDown), Some(KeyCode::PageDown));
assert_eq!(to_code(egui::Key::Space), Some(KeyCode::Char(' ')));
}
#[test]
fn letters_and_digits_come_through_lowercase() {
assert_eq!(to_code(egui::Key::O), Some(KeyCode::Char('o')));
assert_eq!(to_code(egui::Key::Num7), Some(KeyCode::Char('7')));
}
#[test]
fn modifiers_carry_over_and_cmd_counts_as_ctrl() {
let mods = egui::Modifiers {
alt: true,
ctrl: false,
shift: true,
mac_cmd: false,
command: false,
};
let out = to_modifiers(&mods);
assert!(out.contains(KeyModifiers::ALT));
assert!(out.contains(KeyModifiers::SHIFT));
assert!(!out.contains(KeyModifiers::CONTROL));
let cmd = egui::Modifiers {
mac_cmd: true,
..Default::default()
};
assert!(to_modifiers(&cmd).contains(KeyModifiers::CONTROL));
}
#[allow(clippy::field_reassign_with_default)]
fn collected(events: Vec<egui::Event>, modifiers: egui::Modifiers) -> Vec<KeyEvent> {
let mut input = egui::InputState::default();
input.events = events;
input.modifiers = modifiers;
collect(&input, egui::Pos2::ZERO, Metrics::estimate(10.0))
.into_iter()
.filter_map(|i| match i {
Input::Key(key) => Some(key),
Input::Mouse(_) => None,
})
.collect()
}
fn key(key: egui::Key, modifiers: egui::Modifiers) -> egui::Event {
egui::Event::Key {
key,
physical_key: None,
pressed: true,
repeat: false,
modifiers,
}
}
#[test]
fn a_printable_key_arrives_once() {
let typed = collected(
vec![
key(egui::Key::A, egui::Modifiers::default()),
egui::Event::Text("a".into()),
],
egui::Modifiers::default(),
);
assert_eq!(
typed,
vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)]
);
}
#[test]
fn a_capital_is_the_capital_with_no_shift_bit() {
let shift = egui::Modifiers {
shift: true,
..Default::default()
};
let typed = collected(
vec![key(egui::Key::H, shift), egui::Event::Text("H".into())],
shift,
);
assert_eq!(
typed,
vec![KeyEvent::new(KeyCode::Char('H'), KeyModifiers::NONE)]
);
}
#[test]
fn ctrl_and_alt_come_from_the_key_half() {
let ctrl = egui::Modifiers {
ctrl: true,
..Default::default()
};
assert_eq!(
collected(vec![key(egui::Key::O, ctrl)], ctrl),
vec![KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL)]
);
let alt_shift = egui::Modifiers {
alt: true,
shift: true,
..Default::default()
};
assert_eq!(
collected(vec![key(egui::Key::H, alt_shift)], alt_shift),
vec![KeyEvent::new(KeyCode::Char('H'), KeyModifiers::ALT)]
);
}
#[test]
fn the_keys_a_panel_is_driven_with_survive_the_trip() {
let none = egui::Modifiers::default();
let events = vec![
key(egui::Key::F5, none),
key(egui::Key::ArrowDown, none),
key(egui::Key::Enter, none),
key(egui::Key::Tab, none),
key(egui::Key::Escape, none),
egui::Event::Text("\n".into()),
egui::Event::Text("\t".into()),
];
assert_eq!(
collected(events, none),
vec![
KeyEvent::new(KeyCode::F(5), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
]
);
}
}