Skip to main content

arcane_engine/platform/
input.rs

1use std::collections::HashSet;
2
3/// Tracks keyboard and mouse state each frame.
4#[derive(Debug, Default)]
5pub struct InputState {
6    /// Keys currently held down (using winit logical key names).
7    pub keys_down: HashSet<String>,
8    /// Keys pressed this frame (went from up to down).
9    pub keys_pressed: HashSet<String>,
10    /// Keys released this frame (went from down to up).
11    pub keys_released: HashSet<String>,
12    /// Mouse position in window coordinates.
13    pub mouse_x: f32,
14    pub mouse_y: f32,
15    /// Mouse buttons currently held.
16    pub mouse_buttons: HashSet<u8>,
17}
18
19impl InputState {
20    /// Call at the start of each frame to clear per-frame events.
21    pub fn begin_frame(&mut self) {
22        self.keys_pressed.clear();
23        self.keys_released.clear();
24    }
25
26    /// Record a key press event.
27    pub fn key_down(&mut self, key: &str) {
28        if self.keys_down.insert(key.to_string()) {
29            self.keys_pressed.insert(key.to_string());
30        }
31    }
32
33    /// Record a key release event.
34    pub fn key_up(&mut self, key: &str) {
35        if self.keys_down.remove(key) {
36            self.keys_released.insert(key.to_string());
37        }
38    }
39
40    /// Record mouse movement.
41    pub fn mouse_move(&mut self, x: f32, y: f32) {
42        self.mouse_x = x;
43        self.mouse_y = y;
44    }
45
46    /// Check if a key is currently held.
47    pub fn is_key_down(&self, key: &str) -> bool {
48        self.keys_down.contains(key)
49    }
50
51    /// Check if a key was pressed this frame.
52    pub fn is_key_pressed(&self, key: &str) -> bool {
53        self.keys_pressed.contains(key)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn key_pressed_survives_until_read() {
63        // Simulates the winit event loop sequence:
64        // 1. Key event arrives between frames
65        // 2. Frame callback reads input
66        // 3. begin_frame() clears per-frame state for next frame
67        let mut input = InputState::default();
68
69        // Between frames: key event arrives
70        input.key_down("ArrowUp");
71        assert!(input.is_key_pressed("ArrowUp"));
72        assert!(input.is_key_down("ArrowUp"));
73
74        // Frame callback reads it — must still be visible
75        assert!(input.is_key_pressed("ArrowUp"));
76
77        // AFTER callback: clear for next frame
78        input.begin_frame();
79        assert!(!input.is_key_pressed("ArrowUp"));
80        assert!(input.is_key_down("ArrowUp")); // still held
81    }
82
83    #[test]
84    fn begin_frame_before_read_loses_input() {
85        // Documents the bug we hit: if begin_frame() runs BEFORE
86        // the callback reads, keys_pressed is empty.
87        let mut input = InputState::default();
88
89        input.key_down("w");
90        assert!(input.is_key_pressed("w"));
91
92        // Wrong order: clear before read
93        input.begin_frame();
94        assert!(!input.is_key_pressed("w")); // lost!
95    }
96
97    #[test]
98    fn held_key_does_not_re_trigger_pressed() {
99        let mut input = InputState::default();
100
101        input.key_down("a");
102        assert!(input.is_key_pressed("a"));
103
104        input.begin_frame();
105
106        // Same key still held — should NOT appear as pressed again
107        input.key_down("a");
108        assert!(!input.is_key_pressed("a"));
109        assert!(input.is_key_down("a"));
110    }
111
112    #[test]
113    fn key_release_tracked() {
114        let mut input = InputState::default();
115
116        input.key_down("Space");
117        input.begin_frame();
118        input.key_up("Space");
119
120        assert!(!input.is_key_down("Space"));
121        assert!(input.keys_released.contains("Space"));
122
123        input.begin_frame();
124        assert!(!input.keys_released.contains("Space"));
125    }
126
127    #[test]
128    fn mouse_position_is_tracked() {
129        let mut input = InputState::default();
130        assert_eq!(input.mouse_x, 0.0);
131        assert_eq!(input.mouse_y, 0.0);
132
133        input.mouse_x = 100.5;
134        input.mouse_y = 200.75;
135
136        assert_eq!(input.mouse_x, 100.5);
137        assert_eq!(input.mouse_y, 200.75);
138    }
139
140    #[test]
141    fn multiple_keys_can_be_down_simultaneously() {
142        let mut input = InputState::default();
143
144        input.key_down("w");
145        input.key_down("a");
146        input.key_down("d");
147
148        assert!(input.is_key_down("w"));
149        assert!(input.is_key_down("a"));
150        assert!(input.is_key_down("d"));
151        assert!(input.is_key_pressed("w"));
152        assert!(input.is_key_pressed("a"));
153        assert!(input.is_key_pressed("d"));
154    }
155
156    #[test]
157    fn releasing_one_key_does_not_affect_others() {
158        let mut input = InputState::default();
159
160        input.key_down("w");
161        input.key_down("a");
162        input.begin_frame();
163
164        input.key_up("w");
165
166        assert!(!input.is_key_down("w"));
167        assert!(input.is_key_down("a"));
168    }
169
170    #[test]
171    fn key_pressed_works_with_special_keys() {
172        let mut input = InputState::default();
173
174        input.key_down("Escape");
175        input.key_down("Return");
176        input.key_down("ArrowLeft");
177
178        assert!(input.is_key_pressed("Escape"));
179        assert!(input.is_key_pressed("Return"));
180        assert!(input.is_key_pressed("ArrowLeft"));
181    }
182
183    #[test]
184    fn default_input_state_has_no_keys_down() {
185        let input = InputState::default();
186
187        assert!(!input.is_key_down("a"));
188        assert!(!input.is_key_down("Space"));
189        assert!(!input.is_key_pressed("w"));
190        assert_eq!(input.keys_down.len(), 0);
191        assert_eq!(input.keys_pressed.len(), 0);
192    }
193}