ui_input_state/
keyboard_state.rs

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