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