Skip to main content

crcbl_input/
text.rs

1//! The engine's reserved `text` context: the keys a text field takes while it
2//! is being typed into, so that they reach neither the UI's navigation nor the
3//! game.
4//!
5//! **One action, bound to every key that types or edits**: the letters, the
6//! digits, the punctuation row and the ISO and JIS extra keys, Space, the
7//! numpad's digits and operators, Backspace and Delete, Home and End, and Left
8//! and Right. Nothing reads [`TYPE`]'s value — the characters arrive as the
9//! shell's `TextCommit` with the layout applied, and the edits come from the
10//! key events themselves — it exists so the context **owns** those keys, and
11//! the context stack's rule that the topmost active context binding an input
12//! consumes it does the rest.
13//!
14//! **Pushed over [`ui::CONTEXT`](crate::ui::CONTEXT) while a text field is
15//! engaged**, and popped when it is not. While it is pushed W, A, S, D and the
16//! arrows are no `ui_move`, Space is no `ui_accept`, and a game's binding on
17//! any of them hears nothing. Enter, Escape and Tab are not bound, so they
18//! still fall through to `ui_accept`, `ui_back` and `ui_next` — which is how a
19//! field is committed, cancelled or left. The Up and Down arrows are not bound
20//! either: a single-line field has no use for them, and an engaged widget
21//! already takes navigation steps instead of focus.
22//!
23//! **A key held when the context is pushed is withheld from it until
24//! released**, as for every push: the Space that engaged a field is not then
25//! also typed into it by this context, and a W held since before the field
26//! engaged goes up for the game and stays silent here.
27
28use super::{ActionDecl, ActionKind, ActionMap, ActionMapError, Binding};
29use crcbl_core::input::KeyCode;
30
31/// The reserved context's name.
32pub const CONTEXT: &str = "text";
33/// The one action: a key a text field takes. See the module docs.
34pub const TYPE: &str = "text_type";
35
36/// Every key [`CONTEXT`] takes.
37pub const KEYS: &[KeyCode] = &[
38    KeyCode::KeyA,
39    KeyCode::KeyB,
40    KeyCode::KeyC,
41    KeyCode::KeyD,
42    KeyCode::KeyE,
43    KeyCode::KeyF,
44    KeyCode::KeyG,
45    KeyCode::KeyH,
46    KeyCode::KeyI,
47    KeyCode::KeyJ,
48    KeyCode::KeyK,
49    KeyCode::KeyL,
50    KeyCode::KeyM,
51    KeyCode::KeyN,
52    KeyCode::KeyO,
53    KeyCode::KeyP,
54    KeyCode::KeyQ,
55    KeyCode::KeyR,
56    KeyCode::KeyS,
57    KeyCode::KeyT,
58    KeyCode::KeyU,
59    KeyCode::KeyV,
60    KeyCode::KeyW,
61    KeyCode::KeyX,
62    KeyCode::KeyY,
63    KeyCode::KeyZ,
64    KeyCode::Digit0,
65    KeyCode::Digit1,
66    KeyCode::Digit2,
67    KeyCode::Digit3,
68    KeyCode::Digit4,
69    KeyCode::Digit5,
70    KeyCode::Digit6,
71    KeyCode::Digit7,
72    KeyCode::Digit8,
73    KeyCode::Digit9,
74    KeyCode::Backquote,
75    KeyCode::Minus,
76    KeyCode::Equal,
77    KeyCode::BracketLeft,
78    KeyCode::BracketRight,
79    KeyCode::Backslash,
80    KeyCode::Semicolon,
81    KeyCode::Quote,
82    KeyCode::Comma,
83    KeyCode::Period,
84    KeyCode::Slash,
85    KeyCode::IntlBackslash,
86    KeyCode::IntlRo,
87    KeyCode::IntlYen,
88    KeyCode::Space,
89    KeyCode::Backspace,
90    KeyCode::Delete,
91    KeyCode::Home,
92    KeyCode::End,
93    KeyCode::ArrowLeft,
94    KeyCode::ArrowRight,
95    KeyCode::Numpad0,
96    KeyCode::Numpad1,
97    KeyCode::Numpad2,
98    KeyCode::Numpad3,
99    KeyCode::Numpad4,
100    KeyCode::Numpad5,
101    KeyCode::Numpad6,
102    KeyCode::Numpad7,
103    KeyCode::Numpad8,
104    KeyCode::Numpad9,
105    KeyCode::NumpadDivide,
106    KeyCode::NumpadMultiply,
107    KeyCode::NumpadSubtract,
108    KeyCode::NumpadAdd,
109    KeyCode::NumpadDecimal,
110];
111
112/// Declare the reserved `text` context into `map`, off the stack.
113///
114/// # Errors
115/// [`ActionMapError::DuplicateName`] if `map` already declares [`TYPE`].
116pub fn declare(map: &mut ActionMap) -> Result<(), ActionMapError> {
117    map.try_declare_in(
118        CONTEXT,
119        ActionDecl {
120            name: TYPE.to_owned(),
121            kind: ActionKind::Button,
122            bindings: KEYS.iter().copied().map(Binding::Key).collect(),
123        },
124    )
125}
126
127/// Puts [`CONTEXT`] on `map`'s stack while `editing`, and takes it off when
128/// not: what a caller runs once a frame with
129/// `Ui::text_editing`'s answer. Does nothing when the stack already agrees.
130///
131/// # Errors
132/// [`ActionMapError::UnknownContext`] if [`declare`] never ran on `map`, and
133/// [`ActionMapError::ContextNotOnTop`] if `editing` is false while another
134/// context was pushed over this one — an owner popping out of order, which the
135/// stack refuses rather than hides.
136pub fn sync(map: &mut ActionMap, editing: bool) -> Result<(), ActionMapError> {
137    match (editing, map.is_context_active(CONTEXT)) {
138        (true, false) => map.push_context(CONTEXT),
139        (false, true) => map.pop_context(CONTEXT),
140        _ => Ok(()),
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::ui;
148
149    const TICK: f32 = 1.0 / 60.0;
150
151    /// A game that walks on WASD and jumps on Space, with both reserved
152    /// contexts declared beside it and `ui` pushed: a menu with a text field.
153    fn menu() -> ActionMap {
154        let mut map = ActionMap::new();
155        map.declare(ActionDecl {
156            name: "walk".to_owned(),
157            kind: ActionKind::Axis2,
158            bindings: vec![Binding::Wasd {
159                up: KeyCode::KeyW,
160                down: KeyCode::KeyS,
161                left: KeyCode::KeyA,
162                right: KeyCode::KeyD,
163            }],
164        });
165        map.declare(ActionDecl {
166            name: "jump".to_owned(),
167            kind: ActionKind::Button,
168            bindings: vec![Binding::Key(KeyCode::Space)],
169        });
170        ui::declare(&mut map).expect("nothing reserved is taken");
171        declare(&mut map).expect("nothing reserved is taken");
172        map.push_context(ui::CONTEXT).expect("declared");
173        map
174    }
175
176    /// Whether any reserved `ui` action or game action reads anything.
177    fn anything_but_typing(map: &ActionMap) -> Vec<&'static str> {
178        let mut heard = Vec::new();
179        if map.cardinal(ui::MOVE).is_some() {
180            heard.push(ui::MOVE);
181        }
182        for name in [ui::ACCEPT, ui::BACK, ui::NEXT, ui::PREV, "jump"] {
183            if map.button_held(name) {
184                heard.push(name);
185            }
186        }
187        if map.axis2("walk") != (0.0, 0.0) {
188            heard.push("walk");
189        }
190        heard
191    }
192
193    /// The keys a text field must take, found without reading [`KEYS`]: every
194    /// letter and digit, and every key the `ui` context's `ui_move` and
195    /// `ui_accept` bind except Enter and the up and down arrows, which a
196    /// single-line field leaves to navigation.
197    fn keys_a_field_must_take(map: &ActionMap) -> Vec<KeyCode> {
198        let mut keys: Vec<KeyCode> = KeyCode::ALL
199            .iter()
200            .copied()
201            .filter(|key| key.as_str().starts_with("Key") || key.as_str().starts_with("Digit"))
202            .collect();
203        for action in [ui::MOVE, ui::ACCEPT] {
204            for binding in map.bindings(action).expect("declared") {
205                binding.visit_keys(|key| {
206                    if ![KeyCode::Enter, KeyCode::ArrowUp, KeyCode::ArrowDown].contains(&key) {
207                        keys.push(key);
208                    }
209                });
210            }
211        }
212        keys
213    }
214
215    /// **Every key the context takes reaches neither `ui_*` nor the game**
216    /// while it is pushed — each pressed alone, so one key's route cannot hide
217    /// another's — and the same keys do reach them once it is popped. The keys
218    /// walked are [`KEYS`] and, found independently of it, every letter and
219    /// digit and every typing key the `ui` context binds, so a key missing
220    /// from the list is a key this walks and finds leaking.
221    #[test]
222    fn typing_keys_reach_neither_the_ui_nor_the_game() {
223        let mut map = menu();
224        let must = keys_a_field_must_take(&map);
225        assert!(
226            must.contains(&KeyCode::KeyW) && must.contains(&KeyCode::Space),
227            "the independent list lost the keys the claim is about: {must:?}"
228        );
229        sync(&mut map, true).expect("declared");
230        assert_eq!(
231            map.active_contexts().collect::<Vec<_>>(),
232            ["gameplay", "ui", "text"]
233        );
234        for &key in KEYS.iter().chain(&must) {
235            map.begin_tick(TICK);
236            map.key_event(key, true);
237            assert!(map.button_held(TYPE), "{key} is not the text context's");
238            assert_eq!(anything_but_typing(&map), [] as [&str; 0], "{key} leaked");
239            map.key_event(key, false);
240        }
241
242        sync(&mut map, false).expect("on top");
243        assert_eq!(
244            map.active_contexts().collect::<Vec<_>>(),
245            ["gameplay", "ui"]
246        );
247        let mut reached = Vec::new();
248        for key in [KeyCode::KeyW, KeyCode::ArrowLeft, KeyCode::Space] {
249            map.begin_tick(TICK);
250            map.key_event(key, true);
251            reached.extend(anything_but_typing(&map));
252            map.key_event(key, false);
253        }
254        assert_eq!(
255            reached,
256            [ui::MOVE, ui::MOVE, ui::ACCEPT],
257            "popped, the keys did not go back to the ui"
258        );
259    }
260
261    /// **Enter, Escape and Tab still commit, cancel and leave** while the
262    /// context is pushed: it binds none of them.
263    #[test]
264    fn enter_escape_and_tab_still_reach_the_ui() {
265        let mut map = menu();
266        sync(&mut map, true).expect("declared");
267        for (key, action) in [
268            (KeyCode::Enter, ui::ACCEPT),
269            (KeyCode::Escape, ui::BACK),
270            (KeyCode::Tab, ui::NEXT),
271        ] {
272            map.begin_tick(TICK);
273            map.key_event(key, true);
274            assert!(map.button_held(action), "{key} did not reach {action}");
275            assert!(!map.button_held(TYPE));
276            map.key_event(key, false);
277        }
278    }
279
280    /// **The Space that engaged a field does not type into it**: held across
281    /// the push, it is withheld from the new context until it comes up, and
282    /// the next press is the context's.
283    #[test]
284    fn the_key_that_engaged_the_field_is_withheld_until_released() {
285        let mut map = menu();
286        map.begin_tick(TICK);
287        map.key_event(KeyCode::Space, true);
288        assert!(map.just_pressed(ui::ACCEPT));
289        sync(&mut map, true).expect("declared");
290        map.begin_tick(TICK);
291        assert!(
292            !map.button_held(TYPE),
293            "the engaging Space reached the field"
294        );
295        assert!(!map.button_held(ui::ACCEPT) && !map.button_held("jump"));
296        map.key_event(KeyCode::Space, false);
297        map.begin_tick(TICK);
298        map.key_event(KeyCode::Space, true);
299        assert!(map.just_pressed(TYPE), "a fresh Space is not the field's");
300    }
301
302    /// Sync is idempotent, refuses to pop out of order, and needs the
303    /// declaration.
304    #[test]
305    fn sync_agrees_with_the_stack_and_refuses_out_of_order() {
306        let mut map = menu();
307        sync(&mut map, false).expect("already off");
308        sync(&mut map, true).expect("declared");
309        sync(&mut map, true).expect("already on");
310        map.declare_in(
311            "modal",
312            ActionDecl {
313                name: "close".to_owned(),
314                kind: ActionKind::Button,
315                bindings: vec![],
316            },
317        );
318        map.push_context("modal").expect("declared");
319        assert_eq!(
320            sync(&mut map, false),
321            Err(ActionMapError::ContextNotOnTop(CONTEXT.to_owned()))
322        );
323
324        let mut bare = ActionMap::new();
325        assert_eq!(
326            sync(&mut bare, true),
327            Err(ActionMapError::UnknownContext(CONTEXT.to_owned()))
328        );
329    }
330}