Skip to main content

telar_ui_core/
keyboard.rs

1//! The keyboard as state rather than as events.
2//!
3//! Widgets are written against events — a press arrives, a handler runs — and that is the right shape for
4//! a button. It is the wrong shape for two other questions that come up constantly:
5//!
6//! - *"was `Shift` down when that click happened?"* A pointer event carries no modifiers, and a bare
7//!   `Shift` press produces no key event to have tracked, so the answer exists nowhere in the event stream.
8//! - *"is `ArrowUp` held **right now**?"* Asked once per frame by anything that acts for as long as a key is
9//!   down rather than at the moment it went down — a camera orbiting, a value stepping, a sprite walking.
10//!
11//! Both are answered by keeping the state as the events go past, which is what this does. The modifier half
12//! is fed by [`Event::ModifiersChanged`], which the platform layer re-sends on focus changes — so it stays
13//! right across the alt-tab-mid-chord case that reconstruction gets wrong.
14
15use platform_core::{Event, Key, ModifiersState};
16use rustc_hash::FxHashSet;
17use std::cell::RefCell;
18
19#[derive(Default)]
20struct Keyboard {
21    modifiers: ModifiersState,
22    held: FxHashSet<Key>,
23    pressed: FxHashSet<Key>,
24}
25
26thread_local! {
27    static KEYBOARD: RefCell<Keyboard> = RefCell::new(Keyboard::default());
28}
29
30/// Records what `event` says about the keyboard. The runner calls this for every event before dispatch.
31pub fn observe(event: &Event) {
32    KEYBOARD.with(|k| {
33        let mut k = k.borrow_mut();
34        match event {
35            Event::ModifiersChanged { modifiers } => k.modifiers = *modifiers,
36            Event::KeyPressed { key, modifiers } => {
37                k.modifiers = *modifiers;
38                // `insert` reports whether the key was absent, which is what separates a first press from the OS repeating one that was already down.
39                if k.held.insert(key.clone()) {
40                    k.pressed.insert(key.clone());
41                }
42            }
43            Event::KeyReleased { key, modifiers } => {
44                k.modifiers = *modifiers;
45                k.held.remove(key);
46            }
47            // A window that loses focus never sends the releases for what was held, and the keys are not held any more by the time it comes back. Letting them rot would leave whatever they drive running until the user pressed and released the same key again.
48            Event::FocusChanged { is_focused: false } => {
49                k.held.clear();
50                k.pressed.clear();
51                k.modifiers = ModifiersState::default();
52            }
53            _ => {}
54        }
55    });
56}
57
58/// Forgets the presses that belong to the frame just finished. The runner calls this once per frame,
59/// after dispatch, so [`key_pressed`] answers for exactly one frame.
60pub fn end_frame() {
61    KEYBOARD.with(|k| k.borrow_mut().pressed.clear());
62}
63
64/// The modifier keys held right now.
65///
66/// Authoritative rather than reconstructed: it comes from the platform's own reading, including the one it
67/// re-sends when the window regains focus. Read it inside a pointer handler to tell a plain click from a
68/// `Shift`-click.
69pub fn modifiers() -> ModifiersState {
70    KEYBOARD.with(|k| k.borrow().modifiers)
71}
72
73/// Whether `key` is down right now, however long it has been down.
74pub fn key_held(key: &Key) -> bool {
75    KEYBOARD.with(|k| k.borrow().held.contains(key))
76}
77
78/// Whether `key` went down during this frame. False for a key the OS is repeating, which is what makes it
79/// the one to drive a once-per-press action while [`key_held`] drives a continuous one.
80pub fn key_pressed(key: &Key) -> bool {
81    KEYBOARD.with(|k| k.borrow().pressed.contains(key))
82}
83
84/// Drops all keyboard state; parallels the other per-tree resets on teardown and hot reload.
85pub fn reset() {
86    KEYBOARD.with(|k| *k.borrow_mut() = Keyboard::default());
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use platform_core::NamedKey;
93
94    fn up() -> Key {
95        Key::Named(NamedKey::ArrowUp)
96    }
97
98    fn shift() -> ModifiersState {
99        ModifiersState {
100            is_shift: true,
101            ..ModifiersState::default()
102        }
103    }
104
105    fn fresh() {
106        reset();
107    }
108
109    #[test]
110    fn a_key_stays_held_until_it_is_released() {
111        fresh();
112        assert!(!key_held(&up()));
113        observe(&Event::KeyPressed {
114            key: up(),
115            modifiers: ModifiersState::default(),
116        });
117        assert!(key_held(&up()));
118        end_frame();
119        assert!(key_held(&up()), "holding outlives the frame it began in");
120        observe(&Event::KeyReleased {
121            key: up(),
122            modifiers: ModifiersState::default(),
123        });
124        assert!(!key_held(&up()));
125    }
126
127    #[test]
128    fn a_press_answers_for_one_frame_only() {
129        fresh();
130        observe(&Event::KeyPressed {
131            key: up(),
132            modifiers: ModifiersState::default(),
133        });
134        assert!(key_pressed(&up()));
135        end_frame();
136        assert!(!key_pressed(&up()));
137    }
138
139    /// The OS repeats a held key as fresh presses. Counting them would fire a once-per-press action forty
140    /// times a second for a user who simply never let go.
141    #[test]
142    fn a_repeated_key_is_not_a_new_press() {
143        fresh();
144        observe(&Event::KeyPressed {
145            key: up(),
146            modifiers: ModifiersState::default(),
147        });
148        end_frame();
149        observe(&Event::KeyPressed {
150            key: up(),
151            modifiers: ModifiersState::default(),
152        });
153        assert!(key_held(&up()));
154        assert!(!key_pressed(&up()), "the key never came back up");
155    }
156
157    /// The case the whole module exists for: `Shift` alone maps to no `Key`, so without its own event the
158    /// state would still read whatever the last typed character carried.
159    #[test]
160    fn a_bare_modifier_is_visible_without_any_key_event() {
161        fresh();
162        assert!(!modifiers().is_shift);
163        observe(&Event::ModifiersChanged { modifiers: shift() });
164        assert!(modifiers().is_shift);
165    }
166
167    /// Losing focus mid-chord is exactly where a reconstructed state goes wrong: the releases never come.
168    #[test]
169    fn losing_focus_forgets_what_was_held() {
170        fresh();
171        observe(&Event::KeyPressed {
172            key: up(),
173            modifiers: shift(),
174        });
175        assert!(key_held(&up()) && modifiers().is_shift);
176        observe(&Event::FocusChanged { is_focused: false });
177        assert!(!key_held(&up()));
178        assert!(!modifiers().is_shift);
179    }
180}