use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum KeyCode {
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,
Key0,
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
Up,
Down,
Left,
Right,
LShift,
RShift,
LCtrl,
RCtrl,
LAlt,
RAlt,
LSuper,
RSuper,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Space,
Enter,
Escape,
Tab,
Backspace,
Delete,
Insert,
Home,
End,
PageUp,
PageDown,
CapsLock,
Minus,
Equals,
LeftBracket,
RightBracket,
Backslash,
Semicolon,
Apostrophe,
Comma,
Period,
Slash,
Grave,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MouseButton {
Left,
Right,
Middle,
Back,
Forward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GamepadButton {
South,
East,
West,
North,
DPadUp,
DPadDown,
DPadLeft,
DPadRight,
LeftBumper,
RightBumper,
LeftStick,
RightStick,
Start,
Select,
Home,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GamepadAxis {
LeftStickX,
LeftStickY,
RightStickX,
RightStickY,
LeftTrigger,
RightTrigger,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum InputEvent {
KeyPressed(KeyCode),
KeyReleased(KeyCode),
MouseMoved {
x: f64,
y: f64,
},
MouseButtonPressed(MouseButton),
MouseButtonReleased(MouseButton),
MouseScroll {
dx: f64,
dy: f64,
},
GamepadButtonPressed(GamepadButton),
GamepadButtonReleased(GamepadButton),
GamepadAxisMoved {
axis: GamepadAxis,
value: f64,
},
Touch {
id: u64,
x: f64,
y: f64,
phase: TouchPhase,
},
TextInput(char),
CursorLock(bool),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TouchPhase {
Started,
Moved,
Ended,
Cancelled,
}
pub type ActionName = String;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum InputBinding {
Key(KeyCode),
Mouse(MouseButton),
Gamepad(GamepadButton),
GamepadAxisPositive(GamepadAxis),
GamepadAxisNegative(GamepadAxis),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionMap {
bindings: HashMap<ActionName, Vec<InputBinding>>,
}
impl ActionMap {
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, action: impl Into<String>, binding: InputBinding) {
self.bindings
.entry(action.into())
.or_default()
.push(binding);
}
pub fn is_action_pressed(&self, action: &str, state: &InputState) -> bool {
let Some(bindings) = self.bindings.get(action) else {
return false;
};
bindings.iter().any(|b| match b {
InputBinding::Key(key) => state.is_key_pressed(*key),
InputBinding::Mouse(btn) => state.is_mouse_button_pressed(*btn),
InputBinding::Gamepad(btn) => state.is_gamepad_button_pressed(*btn),
InputBinding::GamepadAxisPositive(axis) => state.gamepad_axis(*axis) > 0.5,
InputBinding::GamepadAxisNegative(axis) => state.gamepad_axis(*axis) < -0.5,
})
}
pub fn is_action_just_pressed(&self, action: &str, state: &InputState) -> bool {
let Some(bindings) = self.bindings.get(action) else {
return false;
};
bindings.iter().any(|b| match b {
InputBinding::Key(key) => state.is_key_just_pressed(*key),
InputBinding::Mouse(btn) => state.is_mouse_button_just_pressed(*btn),
InputBinding::Gamepad(btn) => state.is_gamepad_button_just_pressed(*btn),
_ => false,
})
}
pub fn action_axis(&self, action: &str, state: &InputState) -> f64 {
let Some(bindings) = self.bindings.get(action) else {
return 0.0;
};
for b in bindings {
match b {
InputBinding::GamepadAxisPositive(axis)
| InputBinding::GamepadAxisNegative(axis) => {
let v = state.gamepad_axis(*axis);
if v.abs() > 0.1 {
return v;
}
}
InputBinding::Key(key) => {
if state.is_key_pressed(*key) {
return 1.0;
}
}
_ => {}
}
}
0.0
}
pub fn action_count(&self) -> usize {
self.bindings.len()
}
}
#[derive(Debug, Default)]
pub struct InputState {
pressed_keys: HashSet<KeyCode>,
pressed_buttons: HashSet<MouseButton>,
mouse_x: f64,
mouse_y: f64,
mouse_dx: f64,
mouse_dy: f64,
mouse_initialized: bool,
scroll_dx: f64,
scroll_dy: f64,
just_pressed: HashSet<KeyCode>,
just_released: HashSet<KeyCode>,
just_pressed_buttons: HashSet<MouseButton>,
just_released_buttons: HashSet<MouseButton>,
pressed_gamepad: HashSet<GamepadButton>,
just_pressed_gamepad: HashSet<GamepadButton>,
just_released_gamepad: HashSet<GamepadButton>,
gamepad_axes: HashMap<GamepadAxis, f64>,
touches: HashMap<u64, (f64, f64, TouchPhase)>,
text_input: Vec<char>,
cursor_locked: bool,
pub context: String,
}
impl InputState {
pub fn new() -> Self {
Self::default()
}
pub fn process_event(&mut self, event: &InputEvent) {
match event {
InputEvent::KeyPressed(key) => {
if self.pressed_keys.insert(*key) {
self.just_pressed.insert(*key);
}
}
InputEvent::KeyReleased(key) => {
self.pressed_keys.remove(key);
self.just_released.insert(*key);
}
InputEvent::MouseMoved { x, y } => {
if self.mouse_initialized {
self.mouse_dx += x - self.mouse_x;
self.mouse_dy += y - self.mouse_y;
}
self.mouse_x = *x;
self.mouse_y = *y;
self.mouse_initialized = true;
}
InputEvent::MouseButtonPressed(btn) => {
if self.pressed_buttons.insert(*btn) {
self.just_pressed_buttons.insert(*btn);
}
}
InputEvent::MouseButtonReleased(btn) => {
self.pressed_buttons.remove(btn);
self.just_released_buttons.insert(*btn);
}
InputEvent::MouseScroll { dx, dy } => {
self.scroll_dx += dx;
self.scroll_dy += dy;
}
InputEvent::GamepadButtonPressed(btn) => {
if self.pressed_gamepad.insert(*btn) {
self.just_pressed_gamepad.insert(*btn);
}
}
InputEvent::GamepadButtonReleased(btn) => {
self.pressed_gamepad.remove(btn);
self.just_released_gamepad.insert(*btn);
}
InputEvent::GamepadAxisMoved { axis, value } => {
self.gamepad_axes.insert(*axis, *value);
}
InputEvent::Touch { id, x, y, phase } => match phase {
TouchPhase::Ended | TouchPhase::Cancelled => {
self.touches.remove(id);
}
_ => {
self.touches.insert(*id, (*x, *y, *phase));
}
},
InputEvent::TextInput(ch) => {
self.text_input.push(*ch);
}
InputEvent::CursorLock(locked) => {
self.cursor_locked = *locked;
}
}
}
pub fn is_key_pressed(&self, key: KeyCode) -> bool {
self.pressed_keys.contains(&key)
}
pub fn is_key_just_pressed(&self, key: KeyCode) -> bool {
self.just_pressed.contains(&key)
}
pub fn is_key_just_released(&self, key: KeyCode) -> bool {
self.just_released.contains(&key)
}
pub fn is_mouse_button_pressed(&self, btn: MouseButton) -> bool {
self.pressed_buttons.contains(&btn)
}
pub fn is_mouse_button_just_pressed(&self, btn: MouseButton) -> bool {
self.just_pressed_buttons.contains(&btn)
}
pub fn is_mouse_button_just_released(&self, btn: MouseButton) -> bool {
self.just_released_buttons.contains(&btn)
}
pub fn mouse_position(&self) -> (f64, f64) {
(self.mouse_x, self.mouse_y)
}
pub fn is_gamepad_button_pressed(&self, btn: GamepadButton) -> bool {
self.pressed_gamepad.contains(&btn)
}
pub fn is_gamepad_button_just_pressed(&self, btn: GamepadButton) -> bool {
self.just_pressed_gamepad.contains(&btn)
}
pub fn is_gamepad_button_just_released(&self, btn: GamepadButton) -> bool {
self.just_released_gamepad.contains(&btn)
}
pub fn gamepad_axis(&self, axis: GamepadAxis) -> f64 {
self.gamepad_axes.get(&axis).copied().unwrap_or(0.0)
}
pub fn mouse_delta(&self) -> (f64, f64) {
(self.mouse_dx, self.mouse_dy)
}
pub fn scroll_delta(&self) -> (f64, f64) {
(self.scroll_dx, self.scroll_dy)
}
pub fn touches(&self) -> &HashMap<u64, (f64, f64, TouchPhase)> {
&self.touches
}
pub fn text_input(&self) -> &[char] {
&self.text_input
}
pub fn is_cursor_locked(&self) -> bool {
self.cursor_locked
}
pub fn set_context(&mut self, context: impl Into<String>) {
self.context = context.into();
}
pub fn clear_frame(&mut self) {
self.just_pressed.clear();
self.just_released.clear();
self.just_pressed_buttons.clear();
self.just_released_buttons.clear();
self.just_pressed_gamepad.clear();
self.just_released_gamepad.clear();
self.mouse_dx = 0.0;
self.mouse_dy = 0.0;
self.scroll_dx = 0.0;
self.scroll_dy = 0.0;
self.text_input.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_press_release() {
let mut state = InputState::new();
state.process_event(&InputEvent::KeyPressed(KeyCode::W));
assert!(state.is_key_pressed(KeyCode::W));
assert!(state.is_key_just_pressed(KeyCode::W));
state.process_event(&InputEvent::KeyReleased(KeyCode::W));
assert!(!state.is_key_pressed(KeyCode::W));
assert!(state.is_key_just_released(KeyCode::W));
}
#[test]
fn key_not_pressed() {
let state = InputState::new();
assert!(!state.is_key_pressed(KeyCode::A));
}
#[test]
fn mouse_moved() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseMoved { x: 100.0, y: 200.0 });
assert_eq!(state.mouse_position(), (100.0, 200.0));
}
#[test]
fn mouse_button() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseButtonPressed(MouseButton::Left));
assert!(state.is_mouse_button_pressed(MouseButton::Left));
assert!(!state.is_mouse_button_pressed(MouseButton::Right));
state.process_event(&InputEvent::MouseButtonReleased(MouseButton::Left));
assert!(!state.is_mouse_button_pressed(MouseButton::Left));
}
#[test]
fn mouse_scroll() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseScroll { dx: 0.0, dy: 3.0 });
state.process_event(&InputEvent::MouseScroll { dx: 0.0, dy: -1.0 });
assert_eq!(state.scroll_delta(), (0.0, 2.0));
}
#[test]
fn clear_frame_resets_transient() {
let mut state = InputState::new();
state.process_event(&InputEvent::KeyPressed(KeyCode::Space));
state.process_event(&InputEvent::MouseScroll { dx: 1.0, dy: 1.0 });
assert!(state.is_key_just_pressed(KeyCode::Space));
state.clear_frame();
assert!(!state.is_key_just_pressed(KeyCode::Space));
assert_eq!(state.scroll_delta(), (0.0, 0.0));
assert!(state.is_key_pressed(KeyCode::Space));
}
#[test]
fn just_pressed_only_on_first_event() {
let mut state = InputState::new();
state.process_event(&InputEvent::KeyPressed(KeyCode::A));
state.process_event(&InputEvent::KeyPressed(KeyCode::A)); assert!(state.is_key_just_pressed(KeyCode::A));
state.clear_frame();
assert!(!state.is_key_just_pressed(KeyCode::A));
assert!(state.is_key_pressed(KeyCode::A));
}
#[test]
fn multiple_keys() {
let mut state = InputState::new();
state.process_event(&InputEvent::KeyPressed(KeyCode::W));
state.process_event(&InputEvent::KeyPressed(KeyCode::LShift));
assert!(state.is_key_pressed(KeyCode::W));
assert!(state.is_key_pressed(KeyCode::LShift));
assert!(!state.is_key_pressed(KeyCode::S));
}
#[test]
fn default_mouse_position() {
let state = InputState::new();
assert_eq!(state.mouse_position(), (0.0, 0.0));
}
#[test]
fn serde_input_event() {
let event = InputEvent::KeyPressed(KeyCode::Escape);
let json = serde_json::to_string(&event).unwrap();
let decoded: InputEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, decoded);
}
#[test]
fn serde_mouse_event() {
let event = InputEvent::MouseMoved { x: 42.0, y: 99.0 };
let json = serde_json::to_string(&event).unwrap();
let decoded: InputEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, decoded);
}
#[test]
fn mouse_button_edge_triggers() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseButtonPressed(MouseButton::Left));
assert!(state.is_mouse_button_just_pressed(MouseButton::Left));
assert!(!state.is_mouse_button_just_released(MouseButton::Left));
state.clear_frame();
assert!(!state.is_mouse_button_just_pressed(MouseButton::Left));
assert!(state.is_mouse_button_pressed(MouseButton::Left));
state.process_event(&InputEvent::MouseButtonReleased(MouseButton::Left));
assert!(state.is_mouse_button_just_released(MouseButton::Left));
assert!(!state.is_mouse_button_pressed(MouseButton::Left));
}
#[test]
fn all_mouse_button_variants() {
let mut state = InputState::new();
let buttons = [
MouseButton::Left,
MouseButton::Right,
MouseButton::Middle,
MouseButton::Back,
MouseButton::Forward,
];
for btn in &buttons {
state.process_event(&InputEvent::MouseButtonPressed(*btn));
}
for btn in &buttons {
assert!(state.is_mouse_button_pressed(*btn));
}
}
#[test]
fn rapid_press_release_same_frame() {
let mut state = InputState::new();
state.process_event(&InputEvent::KeyPressed(KeyCode::A));
state.process_event(&InputEvent::KeyReleased(KeyCode::A));
assert!(!state.is_key_pressed(KeyCode::A));
assert!(state.is_key_just_pressed(KeyCode::A));
assert!(state.is_key_just_released(KeyCode::A));
}
#[test]
fn serde_all_event_variants() {
let events = vec![
InputEvent::KeyPressed(KeyCode::A),
InputEvent::KeyReleased(KeyCode::Z),
InputEvent::MouseMoved { x: 0.0, y: 0.0 },
InputEvent::MouseButtonPressed(MouseButton::Middle),
InputEvent::MouseButtonReleased(MouseButton::Back),
InputEvent::MouseScroll { dx: -1.5, dy: 2.5 },
];
for event in &events {
let json = serde_json::to_string(event).unwrap();
let decoded: InputEvent = serde_json::from_str(&json).unwrap();
assert_eq!(*event, decoded);
}
}
#[test]
fn mouse_delta() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseMoved { x: 100.0, y: 200.0 });
assert_eq!(state.mouse_delta(), (0.0, 0.0));
state.clear_frame();
state.process_event(&InputEvent::MouseMoved { x: 110.0, y: 205.0 });
assert_eq!(state.mouse_delta(), (10.0, 5.0));
state.clear_frame();
assert_eq!(state.mouse_delta(), (0.0, 0.0));
}
#[test]
fn mouse_delta_accumulates() {
let mut state = InputState::new();
state.process_event(&InputEvent::MouseMoved { x: 10.0, y: 20.0 });
state.process_event(&InputEvent::MouseMoved { x: 30.0, y: 25.0 });
assert_eq!(state.mouse_delta(), (20.0, 5.0));
}
#[test]
fn gamepad_button_press_release() {
let mut state = InputState::new();
state.process_event(&InputEvent::GamepadButtonPressed(GamepadButton::South));
assert!(state.is_gamepad_button_pressed(GamepadButton::South));
assert!(state.is_gamepad_button_just_pressed(GamepadButton::South));
state.process_event(&InputEvent::GamepadButtonReleased(GamepadButton::South));
assert!(!state.is_gamepad_button_pressed(GamepadButton::South));
assert!(state.is_gamepad_button_just_released(GamepadButton::South));
}
#[test]
fn gamepad_axis() {
let mut state = InputState::new();
assert_eq!(state.gamepad_axis(GamepadAxis::LeftStickX), 0.0);
state.process_event(&InputEvent::GamepadAxisMoved {
axis: GamepadAxis::LeftStickX,
value: 0.75,
});
assert_eq!(state.gamepad_axis(GamepadAxis::LeftStickX), 0.75);
}
#[test]
fn gamepad_clear_frame() {
let mut state = InputState::new();
state.process_event(&InputEvent::GamepadButtonPressed(GamepadButton::North));
state.clear_frame();
assert!(!state.is_gamepad_button_just_pressed(GamepadButton::North));
assert!(state.is_gamepad_button_pressed(GamepadButton::North));
}
#[test]
fn action_map_bind_and_check() {
let mut map = ActionMap::new();
map.bind("jump", InputBinding::Key(KeyCode::Space));
map.bind("jump", InputBinding::Gamepad(GamepadButton::South));
assert_eq!(map.action_count(), 1);
let mut state = InputState::new();
assert!(!map.is_action_pressed("jump", &state));
state.process_event(&InputEvent::KeyPressed(KeyCode::Space));
assert!(map.is_action_pressed("jump", &state));
}
#[test]
fn action_map_just_pressed() {
let mut map = ActionMap::new();
map.bind("fire", InputBinding::Mouse(MouseButton::Left));
let mut state = InputState::new();
state.process_event(&InputEvent::MouseButtonPressed(MouseButton::Left));
assert!(map.is_action_just_pressed("fire", &state));
state.clear_frame();
assert!(!map.is_action_just_pressed("fire", &state));
}
#[test]
fn action_map_gamepad_axis() {
let mut map = ActionMap::new();
map.bind(
"move_right",
InputBinding::GamepadAxisPositive(GamepadAxis::LeftStickX),
);
let mut state = InputState::new();
assert_eq!(map.action_axis("move_right", &state), 0.0);
state.process_event(&InputEvent::GamepadAxisMoved {
axis: GamepadAxis::LeftStickX,
value: 0.8,
});
assert!((map.action_axis("move_right", &state) - 0.8).abs() < f64::EPSILON);
}
#[test]
fn action_map_key_as_axis() {
let mut map = ActionMap::new();
map.bind("move_right", InputBinding::Key(KeyCode::D));
let mut state = InputState::new();
assert_eq!(map.action_axis("move_right", &state), 0.0);
state.process_event(&InputEvent::KeyPressed(KeyCode::D));
assert_eq!(map.action_axis("move_right", &state), 1.0);
}
#[test]
fn action_map_unknown_action() {
let map = ActionMap::new();
let state = InputState::new();
assert!(!map.is_action_pressed("nonexistent", &state));
assert_eq!(map.action_axis("nonexistent", &state), 0.0);
}
#[test]
fn action_map_serde() {
let mut map = ActionMap::new();
map.bind("jump", InputBinding::Key(KeyCode::Space));
let json = serde_json::to_string(&map).unwrap();
let decoded: ActionMap = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.action_count(), 1);
}
#[test]
fn gamepad_button_serde() {
let btn = GamepadButton::South;
let json = serde_json::to_string(&btn).unwrap();
let decoded: GamepadButton = serde_json::from_str(&json).unwrap();
assert_eq!(btn, decoded);
}
#[test]
fn gamepad_event_serde() {
let event = InputEvent::GamepadAxisMoved {
axis: GamepadAxis::RightTrigger,
value: 0.5,
};
let json = serde_json::to_string(&event).unwrap();
let decoded: InputEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, decoded);
}
}