use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Key {
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
Digit0, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9,
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
Up, Down, Left, Right,
Enter, Escape, Space, Tab, Backspace, Delete, Insert,
Home, End, PageUp, PageDown,
Shift, Control, Alt,
Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9,
NumpadAdd, NumpadSubtract, NumpadMultiply, NumpadDivide, NumpadDecimal, NumpadEnter,
Comma, Period, Minus, Plus, Slash, Backslash, Semicolon, Quote, LeftBracket, RightBracket, Backquote,
}
impl Key {
pub fn from_char(c: char) -> Option<Key> {
use Key::*;
const LETTERS: [Key; 26] =
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z];
const DIGITS: [Key; 10] =
[Digit0, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9];
let c = c.to_ascii_lowercase();
match c {
'a'..='z' => Some(LETTERS[(c as u8 - b'a') as usize]),
'0'..='9' => Some(DIGITS[(c as u8 - b'0') as usize]),
_ => None,
}
}
pub fn name(self) -> String {
let debug = format!("{self:?}");
if let Some(d) = debug.strip_prefix("Digit") {
return d.to_string();
}
let mut out = String::new();
for (i, c) in debug.chars().enumerate() {
if i > 0 && c.is_uppercase() {
out.push(' ');
}
out.push(c);
}
out
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Modifiers {
pub shift: bool,
pub control: bool,
pub alt: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum KeyEvent {
Down { key: Key, repeat: bool },
Up { key: Key },
}
#[derive(Debug, Default)]
pub struct Input {
queue: Vec<KeyEvent>,
text_queue: String,
held: HashMap<Key, Instant>,
pressed: HashSet<Key>,
repeated: HashSet<Key>,
released: HashSet<Key>,
order: Vec<Key>,
text: String,
held_now: HashMap<Key, f32>,
held_prev: HashMap<Key, f32>,
blocked: bool,
}
impl Input {
pub(crate) fn push(&mut self, event: KeyEvent) {
self.queue.push(event);
}
pub(crate) fn push_text(&mut self, text: &str) {
self.text_queue.extend(text.chars().filter(|c| !c.is_control()));
}
pub(crate) fn release_all(&mut self) {
let keys: Vec<Key> = self.held.keys().copied().collect();
for key in keys {
self.queue.push(KeyEvent::Up { key });
}
}
pub(crate) fn begin_frame(&mut self, now: Instant) {
self.pressed.clear();
self.repeated.clear();
self.released.clear();
self.order.clear();
self.text = std::mem::take(&mut self.text_queue);
for event in std::mem::take(&mut self.queue) {
match event {
KeyEvent::Down { key, repeat } => {
if repeat || self.held.contains_key(&key) {
self.repeated.insert(key);
} else {
self.held.insert(key, now);
self.pressed.insert(key);
}
self.order.push(key);
}
KeyEvent::Up { key } => {
if self.held.remove(&key).is_some() {
self.released.insert(key);
}
}
}
}
self.held_prev = std::mem::take(&mut self.held_now);
for (key, since) in &self.held {
self.held_now.insert(*key, now.duration_since(*since).as_secs_f32());
}
}
pub(crate) fn set_blocked(&mut self, blocked: bool) {
self.blocked = blocked;
}
pub fn is_blocked(&self) -> bool {
self.blocked
}
pub fn pressed(&self, key: Key) -> bool {
!self.blocked && self.pressed.contains(&key)
}
pub fn pressed_or_repeated(&self, key: Key) -> bool {
!self.blocked && (self.pressed.contains(&key) || self.repeated.contains(&key))
}
pub fn held(&self, key: Key) -> bool {
!self.blocked && self.held.contains_key(&key)
}
pub fn released(&self, key: Key) -> bool {
!self.blocked && self.released.contains(&key)
}
pub fn held_for(&self, key: Key) -> Option<f32> {
if self.blocked { None } else { self.held_now.get(&key).copied() }
}
pub fn repeating(&self, key: Key, delay: f32, every: f32) -> bool {
if self.blocked {
return false;
}
if self.pressed.contains(&key) {
return true;
}
let (Some(&now), Some(&prev)) = (self.held_now.get(&key), self.held_prev.get(&key)) else {
return false;
};
if now < delay {
return false;
}
let every = every.max(0.001);
let ticks = |t: f32| if t < delay { -1.0 } else { ((t - delay) / every).floor() };
ticks(now) > ticks(prev)
}
pub fn axis(&self, negative: Key, positive: Key) -> f32 {
self.held(positive) as i32 as f32 - self.held(negative) as i32 as f32
}
pub fn movement(&self) -> glam::Vec2 {
let x = (self.held(Key::Right) || self.held(Key::D)) as i32 - (self.held(Key::Left) || self.held(Key::A)) as i32;
let y = (self.held(Key::Up) || self.held(Key::W)) as i32 - (self.held(Key::Down) || self.held(Key::S)) as i32;
glam::Vec2::new(x as f32, y as f32).normalize_or_zero()
}
pub fn movement_3d(&self) -> glam::Vec3 {
let flat = self.movement();
let z = (self.held(Key::PageUp) || self.held(Key::E)) as i32 - (self.held(Key::PageDown) || self.held(Key::Q)) as i32;
glam::Vec3::new(flat.x, flat.y, z as f32).normalize_or_zero()
}
pub fn any_pressed(&self) -> bool {
!self.blocked && !self.pressed.is_empty()
}
pub fn keys_pressed(&self) -> &[Key] {
if self.blocked { &[] } else { &self.order }
}
pub fn text(&self) -> &str {
if self.blocked { "" } else { &self.text }
}
pub fn modifiers(&self) -> Modifiers {
let held = |k| !self.blocked && self.held.contains_key(&k);
Modifiers { shift: held(Key::Shift), control: held(Key::Control), alt: held(Key::Alt) }
}
pub fn raw(&self) -> RawInput<'_> {
RawInput(self)
}
pub fn held_duration(&self, key: Key) -> Option<Duration> {
self.held_for(key).map(Duration::from_secs_f32)
}
}
#[derive(Debug, Clone, Copy)]
pub struct RawInput<'a>(&'a Input);
impl RawInput<'_> {
pub fn pressed(&self, key: Key) -> bool {
self.0.pressed.contains(&key)
}
pub fn pressed_or_repeated(&self, key: Key) -> bool {
self.0.pressed.contains(&key) || self.0.repeated.contains(&key)
}
pub fn held(&self, key: Key) -> bool {
self.0.held.contains_key(&key)
}
pub fn released(&self, key: Key) -> bool {
self.0.released.contains(&key)
}
pub fn text(&self) -> &str {
&self.0.text
}
pub fn keys_pressed(&self) -> &[Key] {
&self.0.order
}
pub fn modifiers(&self) -> Modifiers {
let held = |k| self.0.held.contains_key(&k);
Modifiers { shift: held(Key::Shift), control: held(Key::Control), alt: held(Key::Alt) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn press_and_release_in_one_frame_is_seen() {
let mut input = Input::default();
input.push(KeyEvent::Down { key: Key::Space, repeat: false });
input.push(KeyEvent::Up { key: Key::Space });
input.begin_frame(Instant::now());
assert!(input.pressed(Key::Space));
assert!(input.released(Key::Space));
assert!(!input.held(Key::Space));
}
#[test]
fn blocking_hides_keys() {
let mut input = Input::default();
input.push(KeyEvent::Down { key: Key::A, repeat: false });
input.begin_frame(Instant::now());
input.set_blocked(true);
assert!(!input.pressed(Key::A));
assert!(input.raw().pressed(Key::A));
}
#[test]
fn game_repeat() {
let mut input = Input::default();
let t = Instant::now();
input.push(KeyEvent::Down { key: Key::Up, repeat: false });
input.begin_frame(t);
assert!(input.repeating(Key::Up, 0.3, 0.1));
input.begin_frame(t + Duration::from_millis(200));
assert!(!input.repeating(Key::Up, 0.3, 0.1));
input.begin_frame(t + Duration::from_millis(310));
assert!(input.repeating(Key::Up, 0.3, 0.1));
input.begin_frame(t + Duration::from_millis(350));
assert!(!input.repeating(Key::Up, 0.3, 0.1));
input.begin_frame(t + Duration::from_millis(420));
assert!(input.repeating(Key::Up, 0.3, 0.1));
}
#[test]
fn movement_3d_adds_up_and_down() {
let mut input = Input::default();
input.push(KeyEvent::Down { key: Key::PageUp, repeat: false });
input.begin_frame(Instant::now());
assert_eq!(input.movement_3d(), glam::Vec3::Z);
input.push(KeyEvent::Down { key: Key::W, repeat: false });
input.begin_frame(Instant::now());
assert!((input.movement_3d() - glam::Vec3::new(0.0, 1.0, 1.0).normalize()).length() < 1e-5);
}
#[test]
fn key_names() {
assert_eq!(Key::PageUp.name(), "Page Up");
assert_eq!(Key::Digit5.name(), "5");
assert_eq!(Key::from_char('Q'), Some(Key::Q));
}
}