Skip to main content

audio_code/
input.rs

1//! Keyboard input. Available as `ctx.input`.
2//!
3//! The engine collects key events between frames, so a key that is pressed and released
4//! within a single frame is still reported by [`Input::pressed`].
5//!
6//! While a virtual UI element (menu, dialog, text field) is open it owns the keyboard and
7//! all game-facing queries return `false`. Use [`Input::raw`] if you really need to peek.
8
9use std::collections::{HashMap, HashSet};
10use std::time::{Duration, Instant};
11
12/// A key on the keyboard.
13///
14/// Letters and digits follow the label printed on the key in the active keyboard layout,
15/// so `Key::Z` is the key labelled "Z" on both QWERTY and QWERTZ keyboards.
16#[allow(missing_docs)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub enum Key {
19    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,
20    Digit0, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9,
21    F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
22    Up, Down, Left, Right,
23    Enter, Escape, Space, Tab, Backspace, Delete, Insert,
24    Home, End, PageUp, PageDown,
25    Shift, Control, Alt,
26    Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9,
27    NumpadAdd, NumpadSubtract, NumpadMultiply, NumpadDivide, NumpadDecimal, NumpadEnter,
28    Comma, Period, Minus, Plus, Slash, Backslash, Semicolon, Quote, LeftBracket, RightBracket, Backquote,
29}
30
31impl Key {
32    /// Maps a letter or digit to its key (case insensitive).
33    pub fn from_char(c: char) -> Option<Key> {
34        use Key::*;
35        const LETTERS: [Key; 26] =
36            [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];
37        const DIGITS: [Key; 10] =
38            [Digit0, Digit1, Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9];
39        let c = c.to_ascii_lowercase();
40        match c {
41            'a'..='z' => Some(LETTERS[(c as u8 - b'a') as usize]),
42            '0'..='9' => Some(DIGITS[(c as u8 - b'0') as usize]),
43            _ => None,
44        }
45    }
46
47    /// A short human readable name, e.g. "Page Up". Handy for "press X to continue" messages.
48    pub fn name(self) -> String {
49        let debug = format!("{self:?}");
50        if let Some(d) = debug.strip_prefix("Digit") {
51            return d.to_string();
52        }
53        // Split CamelCase into words: "PageUp" -> "Page Up".
54        let mut out = String::new();
55        for (i, c) in debug.chars().enumerate() {
56            if i > 0 && c.is_uppercase() {
57                out.push(' ');
58            }
59            out.push(c);
60        }
61        out
62    }
63}
64
65/// State of the modifier keys.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67pub struct Modifiers {
68    /// Either shift key is held.
69    pub shift: bool,
70    /// Either control key is held.
71    pub control: bool,
72    /// Either alt key is held.
73    pub alt: bool,
74}
75
76/// A raw key event coming from the window backend.
77#[derive(Debug, Clone, Copy)]
78pub(crate) enum KeyEvent {
79    Down { key: Key, repeat: bool },
80    Up { key: Key },
81}
82
83/// Keyboard state for the current frame.
84#[derive(Debug, Default)]
85pub struct Input {
86    queue: Vec<KeyEvent>,
87    text_queue: String,
88
89    held: HashMap<Key, Instant>,
90    pressed: HashSet<Key>,
91    repeated: HashSet<Key>,
92    released: HashSet<Key>,
93    order: Vec<Key>,
94    text: String,
95    held_now: HashMap<Key, f32>,
96    held_prev: HashMap<Key, f32>,
97
98    blocked: bool,
99}
100
101impl Input {
102    pub(crate) fn push(&mut self, event: KeyEvent) {
103        self.queue.push(event);
104    }
105
106    pub(crate) fn push_text(&mut self, text: &str) {
107        self.text_queue.extend(text.chars().filter(|c| !c.is_control()));
108    }
109
110    /// Forgets all held keys, e.g. when the window loses focus.
111    pub(crate) fn release_all(&mut self) {
112        let keys: Vec<Key> = self.held.keys().copied().collect();
113        for key in keys {
114            self.queue.push(KeyEvent::Up { key });
115        }
116    }
117
118    /// Turns the queued events into this frame's state.
119    pub(crate) fn begin_frame(&mut self, now: Instant) {
120        self.pressed.clear();
121        self.repeated.clear();
122        self.released.clear();
123        self.order.clear();
124        self.text = std::mem::take(&mut self.text_queue);
125        for event in std::mem::take(&mut self.queue) {
126            match event {
127                KeyEvent::Down { key, repeat } => {
128                    if repeat || self.held.contains_key(&key) {
129                        self.repeated.insert(key);
130                    } else {
131                        self.held.insert(key, now);
132                        self.pressed.insert(key);
133                    }
134                    self.order.push(key);
135                }
136                KeyEvent::Up { key } => {
137                    if self.held.remove(&key).is_some() {
138                        self.released.insert(key);
139                    }
140                }
141            }
142        }
143        self.held_prev = std::mem::take(&mut self.held_now);
144        for (key, since) in &self.held {
145            self.held_now.insert(*key, now.duration_since(*since).as_secs_f32());
146        }
147    }
148
149    pub(crate) fn set_blocked(&mut self, blocked: bool) {
150        self.blocked = blocked;
151    }
152
153    /// Whether game input is currently blocked by an open UI element.
154    pub fn is_blocked(&self) -> bool {
155        self.blocked
156    }
157
158    /// `true` in the frame the key went down. OS key repeat is ignored.
159    pub fn pressed(&self, key: Key) -> bool {
160        !self.blocked && self.pressed.contains(&key)
161    }
162
163    /// `true` in the frame the key went down **and** whenever the operating system sends a
164    /// key repeat while it is held. This is what text fields and menus use.
165    pub fn pressed_or_repeated(&self, key: Key) -> bool {
166        !self.blocked && (self.pressed.contains(&key) || self.repeated.contains(&key))
167    }
168
169    /// `true` as long as the key is held down.
170    pub fn held(&self, key: Key) -> bool {
171        !self.blocked && self.held.contains_key(&key)
172    }
173
174    /// `true` in the frame the key was let go.
175    pub fn released(&self, key: Key) -> bool {
176        !self.blocked && self.released.contains(&key)
177    }
178
179    /// How long the key has been held in seconds, or `None` if it is up.
180    pub fn held_for(&self, key: Key) -> Option<f32> {
181        if self.blocked { None } else { self.held_now.get(&key).copied() }
182    }
183
184    /// Game-controlled key repeat with your own timing, independent of OS settings.
185    ///
186    /// Returns `true` when the key is pressed, then again after `delay` seconds and from then
187    /// on every `every` seconds while it stays held. Perfect for walking on a grid:
188    ///
189    /// ```no_run
190    /// # use audio_code::prelude::*;
191    /// # fn f(ctx: &mut Context) {
192    /// if ctx.input.repeating(Key::Up, 0.3, 0.3) {
193    ///     // take one step forward
194    /// }
195    /// # }
196    /// ```
197    pub fn repeating(&self, key: Key, delay: f32, every: f32) -> bool {
198        if self.blocked {
199            return false;
200        }
201        if self.pressed.contains(&key) {
202            return true;
203        }
204        let (Some(&now), Some(&prev)) = (self.held_now.get(&key), self.held_prev.get(&key)) else {
205            return false;
206        };
207        if now < delay {
208            return false;
209        }
210        let every = every.max(0.001);
211        // Count how many repeat points have been crossed; report if a new one was crossed.
212        let ticks = |t: f32| if t < delay { -1.0 } else { ((t - delay) / every).floor() };
213        ticks(now) > ticks(prev)
214    }
215
216    /// -1.0, 0.0 or 1.0 depending on which of two held keys is down, e.g.
217    /// `ctx.input.axis(Key::Left, Key::Right)` for running in a side scroller.
218    pub fn axis(&self, negative: Key, positive: Key) -> f32 {
219        self.held(positive) as i32 as f32 - self.held(negative) as i32 as f32
220    }
221
222    /// Movement direction from the arrow keys or WASD: `x` is right, `y` is forward.
223    /// Diagonals are normalized, so moving diagonally is not faster.
224    pub fn movement(&self) -> glam::Vec2 {
225        let x = (self.held(Key::Right) || self.held(Key::D)) as i32 - (self.held(Key::Left) || self.held(Key::A)) as i32;
226        let y = (self.held(Key::Up) || self.held(Key::W)) as i32 - (self.held(Key::Down) || self.held(Key::S)) as i32;
227        glam::Vec2::new(x as f32, y as f32).normalize_or_zero()
228    }
229
230    /// Like [`Input::movement`], plus `z` for up (Page Up or E) and down (Page Down or Q),
231    /// for flying, swimming or climbing. `x` is right, `y` is forward, `z` is up. Diagonals
232    /// are normalized.
233    pub fn movement_3d(&self) -> glam::Vec3 {
234        let flat = self.movement();
235        let z = (self.held(Key::PageUp) || self.held(Key::E)) as i32 - (self.held(Key::PageDown) || self.held(Key::Q)) as i32;
236        glam::Vec3::new(flat.x, flat.y, z as f32).normalize_or_zero()
237    }
238
239    /// `true` if any key went down this frame.
240    pub fn any_pressed(&self) -> bool {
241        !self.blocked && !self.pressed.is_empty()
242    }
243
244    /// Keys that went down this frame in the order they were pressed (including OS repeats).
245    pub fn keys_pressed(&self) -> &[Key] {
246        if self.blocked { &[] } else { &self.order }
247    }
248
249    /// Text typed this frame, respecting keyboard layout and shift state.
250    pub fn text(&self) -> &str {
251        if self.blocked { "" } else { &self.text }
252    }
253
254    /// Current state of shift, control and alt.
255    pub fn modifiers(&self) -> Modifiers {
256        let held = |k| !self.blocked && self.held.contains_key(&k);
257        Modifiers { shift: held(Key::Shift), control: held(Key::Control), alt: held(Key::Alt) }
258    }
259
260    /// Unblocked view of the input, even while a UI element is open.
261    pub fn raw(&self) -> RawInput<'_> {
262        RawInput(self)
263    }
264
265    /// Duration the key has been held, using a real `Duration`.
266    pub fn held_duration(&self, key: Key) -> Option<Duration> {
267        self.held_for(key).map(Duration::from_secs_f32)
268    }
269}
270
271/// Input queries that ignore UI blocking. Obtain it with [`Input::raw`].
272#[derive(Debug, Clone, Copy)]
273pub struct RawInput<'a>(&'a Input);
274
275impl RawInput<'_> {
276    /// See [`Input::pressed`].
277    pub fn pressed(&self, key: Key) -> bool {
278        self.0.pressed.contains(&key)
279    }
280
281    /// See [`Input::pressed_or_repeated`].
282    pub fn pressed_or_repeated(&self, key: Key) -> bool {
283        self.0.pressed.contains(&key) || self.0.repeated.contains(&key)
284    }
285
286    /// See [`Input::held`].
287    pub fn held(&self, key: Key) -> bool {
288        self.0.held.contains_key(&key)
289    }
290
291    /// See [`Input::released`].
292    pub fn released(&self, key: Key) -> bool {
293        self.0.released.contains(&key)
294    }
295
296    /// See [`Input::text`].
297    pub fn text(&self) -> &str {
298        &self.0.text
299    }
300
301    /// See [`Input::keys_pressed`].
302    pub fn keys_pressed(&self) -> &[Key] {
303        &self.0.order
304    }
305
306    /// See [`Input::modifiers`].
307    pub fn modifiers(&self) -> Modifiers {
308        let held = |k| self.0.held.contains_key(&k);
309        Modifiers { shift: held(Key::Shift), control: held(Key::Control), alt: held(Key::Alt) }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn press_and_release_in_one_frame_is_seen() {
319        let mut input = Input::default();
320        input.push(KeyEvent::Down { key: Key::Space, repeat: false });
321        input.push(KeyEvent::Up { key: Key::Space });
322        input.begin_frame(Instant::now());
323        assert!(input.pressed(Key::Space));
324        assert!(input.released(Key::Space));
325        assert!(!input.held(Key::Space));
326    }
327
328    #[test]
329    fn blocking_hides_keys() {
330        let mut input = Input::default();
331        input.push(KeyEvent::Down { key: Key::A, repeat: false });
332        input.begin_frame(Instant::now());
333        input.set_blocked(true);
334        assert!(!input.pressed(Key::A));
335        assert!(input.raw().pressed(Key::A));
336    }
337
338    #[test]
339    fn game_repeat() {
340        let mut input = Input::default();
341        let t = Instant::now();
342        input.push(KeyEvent::Down { key: Key::Up, repeat: false });
343        input.begin_frame(t);
344        assert!(input.repeating(Key::Up, 0.3, 0.1));
345        input.begin_frame(t + Duration::from_millis(200));
346        assert!(!input.repeating(Key::Up, 0.3, 0.1));
347        input.begin_frame(t + Duration::from_millis(310));
348        assert!(input.repeating(Key::Up, 0.3, 0.1));
349        input.begin_frame(t + Duration::from_millis(350));
350        assert!(!input.repeating(Key::Up, 0.3, 0.1));
351        input.begin_frame(t + Duration::from_millis(420));
352        assert!(input.repeating(Key::Up, 0.3, 0.1));
353    }
354
355    #[test]
356    fn movement_3d_adds_up_and_down() {
357        let mut input = Input::default();
358        input.push(KeyEvent::Down { key: Key::PageUp, repeat: false });
359        input.begin_frame(Instant::now());
360        assert_eq!(input.movement_3d(), glam::Vec3::Z);
361        input.push(KeyEvent::Down { key: Key::W, repeat: false });
362        input.begin_frame(Instant::now());
363        assert!((input.movement_3d() - glam::Vec3::new(0.0, 1.0, 1.0).normalize()).length() < 1e-5);
364    }
365
366    #[test]
367    fn key_names() {
368        assert_eq!(Key::PageUp.name(), "Page Up");
369        assert_eq!(Key::Digit5.name(), "5");
370        assert_eq!(Key::from_char('Q'), Some(Key::Q));
371    }
372}