ui_input_state/
keyboard_state.rs

1// Copyright 2025 the UI Events Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! # Keyboard state across frames.
5//!
6//! `KeyboardState` tracks per-frame transitions (just pressed/released), keys
7//! currently held down, and the active [`Modifiers`]. Feed it
8//! [`KeyboardEvent`] values as they arrive; query it during your update pass;
9//! call [`clear_frame`](KeyboardState::clear_frame) at the end of the frame.
10//!
11//! ## Example:
12//!
13//! ```no_run
14//! use ui_input_state::KeyboardState;
15//! use ui_events::keyboard::{KeyboardEvent, KeyState, Key, Location, Code, Modifiers};
16//!
17//! let mut ks = KeyboardState::default();
18//! let ev = KeyboardEvent {
19//!     state: KeyState::Down,
20//!     key: Key::Character("z".into()),
21//!     location: Location::Standard,
22//!     code: Code::KeyZ,
23//!     modifiers: Modifiers::empty(),
24//!     is_composing: false,
25//!     repeat: false,
26//! };
27//! ks.process_keyboard_event(ev);
28//! assert!(ks.key_str_just_pressed("z"));
29//! ```
30use ui_events::keyboard::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
31
32extern crate alloc;
33use alloc::vec::Vec;
34
35#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
36struct KeyInfo(Key, Location, Code);
37
38/// A stateful view of the primary pointer.
39#[derive(Clone, Debug, Default)]
40pub struct KeyboardState {
41    /// Keys that were pressed during the current frame.
42    just_pressed: Vec<KeyInfo>,
43    /// Keys that were released during the current frame.
44    just_released: Vec<KeyInfo>,
45    /// Keys that are currently being held down.
46    down: Vec<KeyInfo>,
47    /// Modifiers state.
48    pub modifiers: Modifiers,
49}
50
51impl KeyboardState {
52    /// Return `true` if the `key` was pressed within the last frame with
53    /// any [`Location`].
54    pub fn key_just_pressed(&self, key: Key) -> bool {
55        self.just_pressed.iter().any(|KeyInfo(k, ..)| k == &key)
56    }
57
58    /// Return `true` if a `Key::Character` matching `s` was pressed within the last frame
59    /// with any [`Location`].
60    ///
61    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
62    /// If you are matching a [`Key::Named`] then use [`key_just_pressed`].
63    ///
64    /// [`key_just_pressed`]: KeyboardState::key_just_pressed
65    /// [`String`]: alloc::string::String
66    pub fn key_str_just_pressed(&self, s: &str) -> bool {
67        self.just_pressed
68            .iter()
69            .any(|KeyInfo(k, ..)| matches!(k, Key::Character(c) if c == s))
70    }
71
72    /// Return `true` if the `key` was pressed within the last frame with `location`.
73    pub fn key_just_pressed_location(&self, key: Key, location: Location) -> bool {
74        self.just_pressed
75            .iter()
76            .any(|KeyInfo(k, l, _)| k == &key && l == &location)
77    }
78
79    /// Return `true` if a `Key::Character` matching `s` was pressed within the last frame
80    /// with `location`.
81    ///
82    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
83    /// If you are matching a [`Key::Named`] then use [`key_just_pressed_location`].
84    ///
85    /// [`key_just_pressed_location`]: KeyboardState::key_just_pressed_location
86    /// [`String`]: alloc::string::String
87    pub fn key_str_just_pressed_location(&self, s: &str, location: Location) -> bool {
88        self.just_pressed
89            .iter()
90            .any(|KeyInfo(k, l, ..)| l == &location && matches!(k, Key::Character(c) if c == s))
91    }
92
93    /// Return `true` if the `Code` was pressed within the last frame.
94    pub fn code_just_pressed(&self, code: Code) -> bool {
95        self.just_pressed.iter().any(|KeyInfo(_, _, c)| c == &code)
96    }
97
98    /// Return `true` if the `key` was released within the last frame with
99    /// any [`Location`].
100    pub fn key_just_released(&self, key: Key) -> bool {
101        self.just_released.iter().any(|KeyInfo(k, ..)| k == &key)
102    }
103
104    /// Return `true` if a `Key::Character` matching `s` was released within the last frame
105    /// with any [`Location`].
106    ///
107    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
108    /// If you are matching a [`Key::Named`] then use [`key_just_released`].
109    ///
110    /// [`key_just_released`]: KeyboardState::key_just_released
111    /// [`String`]: alloc::string::String
112    pub fn key_str_just_released(&self, s: &str) -> bool {
113        self.just_released
114            .iter()
115            .any(|KeyInfo(k, ..)| matches!(k, Key::Character(c) if c == s))
116    }
117
118    /// Return `true` if the `key` was released within the last frame with `location`.
119    pub fn key_just_released_location(&self, key: Key, location: Location) -> bool {
120        self.just_released
121            .iter()
122            .any(|KeyInfo(k, l, _)| k == &key && l == &location)
123    }
124
125    /// Return `true` if a `Key::Character` matching `s` was released within the last frame
126    /// with `location`.
127    ///
128    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
129    /// If you are matching a [`Key::Named`] then use [`key_just_released_location`].
130    ///
131    /// [`key_just_released_location`]: KeyboardState::key_just_released_location
132    /// [`String`]: alloc::string::String
133    pub fn key_str_just_released_location(&self, s: &str, location: Location) -> bool {
134        self.just_released
135            .iter()
136            .any(|KeyInfo(k, l, ..)| l == &location && matches!(k, Key::Character(c) if c == s))
137    }
138
139    /// Return `true` if the `Code` was released within the last frame.
140    pub fn code_just_released(&self, code: Code) -> bool {
141        self.just_released.iter().any(|KeyInfo(_, _, c)| c == &code)
142    }
143
144    /// Return `true` if any key is currently held down.
145    pub fn is_any_down(&self) -> bool {
146        !self.down.is_empty()
147    }
148
149    /// Return `true` if the `key` is currently pressed with any [`Location`].
150    ///
151    /// For a [`Key::Character`], you can use [`key_str_down`] to avoid allocating
152    /// a [`String`] each time you check.
153    ///
154    /// [`key_str_down`]: KeyboardState::key_str_down
155    /// [`String`]: alloc::string::String
156    pub fn key_down(&self, key: Key) -> bool {
157        self.down.iter().any(|KeyInfo(k, ..)| k == &key)
158    }
159
160    /// Return `true` if a `Key::Character` matching `s` is currently pressed with any [`Location`].
161    ///
162    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
163    /// If you are matching a [`Key::Named`] then use [`key_down`].
164    ///
165    /// [`key_down`]: KeyboardState::key_down
166    /// [`String`]: alloc::string::String
167    pub fn key_str_down(&self, s: &str) -> bool {
168        self.down
169            .iter()
170            .any(|KeyInfo(k, ..)| matches!(k, Key::Character(c) if c == s))
171    }
172
173    /// Return `true` if the `key` is currently pressed with `location`.
174    ///
175    /// For a [`Key::Character`], you can use [`key_str_down_location`] to avoid allocating
176    /// a [`String`] each time you check.
177    ///
178    /// [`key_str_down_location`]: KeyboardState::key_str_down_location
179    /// [`String`]: alloc::string::String
180    pub fn key_down_location(&self, key: Key, location: Location) -> bool {
181        self.down
182            .iter()
183            .any(|KeyInfo(k, l, _)| k == &key && l == &location)
184    }
185
186    /// Return `true` if a `Key::Character` matching `s` is currently pressed with `location`.
187    ///
188    /// This is an optimization for matching [`Key::Character`] without allocating a [`String`].
189    /// If you are matching a [`Key::Named`] then use [`key_down`].
190    ///
191    /// [`key_down`]: KeyboardState::key_down_location.
192    /// [`String`]: alloc::string::String
193    pub fn key_str_down_location(&self, s: &str, location: Location) -> bool {
194        self.down
195            .iter()
196            .any(|KeyInfo(k, l, ..)| l == &location && matches!(k, Key::Character(c) if c == s))
197    }
198
199    /// Return `true` if the `code` is currently pressed with any [`Location`].
200    pub fn code_down(&self, code: Code) -> bool {
201        self.down.iter().any(|KeyInfo(_, _, c)| c == &code)
202    }
203
204    /// Clear the per-frame state to prepare for a new frame.
205    pub fn clear_frame(&mut self) {
206        self.just_pressed.clear();
207        self.just_released.clear();
208    }
209
210    /// Update the state based on the given pointer event.
211    ///
212    /// Only events from the primary pointer are processed. Press and release
213    /// events update the `just_pressed`, `just_released`, and `down` states.
214    pub fn process_keyboard_event(&mut self, event: KeyboardEvent) {
215        self.modifiers = event.modifiers;
216        let info = KeyInfo(event.key, event.location, event.code);
217        match event.state {
218            KeyState::Down => {
219                self.just_pressed.push(info.clone());
220                self.down.push(info.clone());
221            }
222            KeyState::Up => {
223                self.just_released.push(info.clone());
224                self.down.retain(|other| other != &info);
225            }
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use ui_events::keyboard::{Code, NamedKey};
234
235    fn make_key_down_event(key: Key) -> KeyboardEvent {
236        KeyboardEvent {
237            state: KeyState::Down,
238            key,
239            location: Location::Standard,
240            code: Code::Unidentified,
241            modifiers: Default::default(),
242            is_composing: false,
243            repeat: false,
244        }
245    }
246
247    fn make_key_up_event(key: Key) -> KeyboardEvent {
248        KeyboardEvent {
249            state: KeyState::Up,
250            key,
251            location: Location::Standard,
252            code: Code::Unidentified,
253            modifiers: Default::default(),
254            is_composing: false,
255            repeat: false,
256        }
257    }
258
259    #[test]
260    fn press_and_hold_a() {
261        let mut state = KeyboardState::default();
262        state.process_keyboard_event(make_key_down_event(Key::Character("A".into())));
263
264        assert!(state.key_just_pressed(Key::Character("A".into())));
265        assert!(state.key_str_just_pressed("A"));
266        assert!(state.key_str_just_pressed_location("A", Location::Standard));
267        assert!(!state.key_str_just_pressed_location("A", Location::Left));
268        assert!(state.key_down(Key::Character("A".into())));
269        assert!(state.key_str_down("A"));
270        assert!(state.key_str_down_location("A", Location::Standard));
271        assert!(!state.key_str_down_location("A", Location::Left));
272        assert!(!state.key_just_released(Key::Character("A".into())));
273        assert!(!state.key_str_just_released("A"));
274        assert!(!state.key_str_just_released_location("A", Location::Standard));
275        assert!(!state.key_str_just_released_location("A", Location::Left));
276
277        state.clear_frame();
278
279        assert!(!state.key_just_pressed(Key::Character("A".into())));
280        assert!(!state.key_str_just_pressed("A"));
281        assert!(!state.key_str_just_pressed_location("A", Location::Standard));
282        assert!(!state.key_str_just_pressed_location("A", Location::Left));
283        assert!(state.key_down(Key::Character("A".into())));
284        assert!(state.key_str_down("A"));
285        assert!(state.key_str_down_location("A", Location::Standard));
286        assert!(!state.key_str_down_location("A", Location::Left));
287    }
288
289    #[test]
290    fn press_and_release_a() {
291        let mut state = KeyboardState::default();
292        state.process_keyboard_event(make_key_down_event(Key::Character("A".into())));
293        state.process_keyboard_event(make_key_up_event(Key::Character("A".into())));
294
295        assert!(state.key_just_pressed(Key::Character("A".into())));
296        assert!(state.key_str_just_pressed("A"));
297        assert!(state.key_str_just_pressed_location("A", Location::Standard));
298        assert!(!state.key_str_just_pressed_location("A", Location::Left));
299        assert!(state.key_just_released(Key::Character("A".into())));
300        assert!(state.key_str_just_released("A"));
301        assert!(state.key_str_just_released_location("A", Location::Standard));
302        assert!(!state.key_str_just_released_location("A", Location::Left));
303        assert!(!state.key_down(Key::Character("A".into())));
304        assert!(!state.key_str_down("A"));
305        assert!(!state.key_str_down_location("A", Location::Standard));
306        assert!(!state.key_str_down_location("A", Location::Left));
307    }
308
309    #[test]
310    fn release_after_hold() {
311        let mut state = KeyboardState::default();
312        state.process_keyboard_event(make_key_down_event(Key::Character("A".into())));
313        state.clear_frame();
314        state.process_keyboard_event(make_key_up_event(Key::Character("A".into())));
315
316        assert!(!state.key_just_pressed(Key::Character("A".into())));
317        assert!(!state.key_str_just_pressed("A"));
318        assert!(!state.key_str_just_pressed_location("A", Location::Standard));
319        assert!(!state.key_str_just_pressed_location("A", Location::Left));
320        assert!(state.key_just_released(Key::Character("A".into())));
321        assert!(state.key_str_just_released("A"));
322        assert!(state.key_str_just_released_location("A", Location::Standard));
323        assert!(!state.key_str_just_released_location("A", Location::Left));
324        assert!(!state.key_down(Key::Character("A".into())));
325        assert!(!state.key_str_down("A"));
326        assert!(!state.key_str_down_location("A", Location::Standard));
327        assert!(!state.key_str_down_location("A", Location::Left));
328    }
329
330    fn make_code_down_event(code: Code) -> KeyboardEvent {
331        KeyboardEvent {
332            state: KeyState::Down,
333            key: Key::Named(NamedKey::Unidentified),
334            location: Location::Standard,
335            code,
336            modifiers: Default::default(),
337            is_composing: false,
338            repeat: false,
339        }
340    }
341
342    fn make_code_up_event(code: Code) -> KeyboardEvent {
343        KeyboardEvent {
344            state: KeyState::Up,
345            key: Key::Named(NamedKey::Unidentified),
346            location: Location::Standard,
347            code,
348            modifiers: Default::default(),
349            is_composing: false,
350            repeat: false,
351        }
352    }
353
354    #[test]
355    fn press_and_hold_a_code() {
356        let mut state = KeyboardState::default();
357        state.process_keyboard_event(make_code_down_event(Code::KeyA));
358
359        assert!(state.code_just_pressed(Code::KeyA));
360        assert!(state.code_down(Code::KeyA));
361        assert!(!state.code_just_released(Code::KeyA));
362
363        state.clear_frame();
364
365        assert!(!state.code_just_pressed(Code::KeyA));
366        assert!(state.code_down(Code::KeyA));
367    }
368
369    #[test]
370    fn press_and_release_a_code() {
371        let mut state = KeyboardState::default();
372        state.process_keyboard_event(make_code_down_event(Code::KeyA));
373        state.process_keyboard_event(make_code_up_event(Code::KeyA));
374
375        assert!(state.code_just_pressed(Code::KeyA));
376        assert!(state.code_just_released(Code::KeyA));
377        assert!(!state.code_down(Code::KeyA));
378    }
379
380    #[test]
381    fn release_after_hold_code() {
382        let mut state = KeyboardState::default();
383        state.process_keyboard_event(make_code_down_event(Code::KeyA));
384        state.clear_frame();
385        state.process_keyboard_event(make_code_up_event(Code::KeyA));
386
387        assert!(!state.code_just_pressed(Code::KeyA));
388        assert!(state.code_just_released(Code::KeyA));
389        assert!(!state.code_down(Code::KeyA));
390    }
391}