Skip to main content

crcbl_input/
list.rs

1//! The engine's reserved `list` context: the keys an open pop-up list takes —
2//! a drop-down's list or a context menu — so that they reach neither the UI's
3//! navigation nor the game.
4//!
5//! | Action        | Kind   | Keyboard                         | Repeat         |
6//! | ------------- | ------ | -------------------------------- | -------------- |
7//! | [`FIRST`]     | Button | Home                             | none           |
8//! | [`LAST`]      | Button | End                              | none           |
9//! | [`PAGE_UP`]   | Button | Page Up                          | [`Repeat::UI`] |
10//! | [`PAGE_DOWN`] | Button | Page Down                        | [`Repeat::UI`] |
11//! | [`TYPE`]      | Button | every key [`type_keys`] names    | none           |
12//!
13//! **Pushed over [`ui::CONTEXT`](crate::ui::CONTEXT) while a list is open**, as
14//! [`text`]'s context is while a field is engaged, and popped
15//! when it is not; the tree's `Ui::popup_list_open` says which. The two are
16//! never pushed together: a list takes no keys while something is engaged.
17//!
18//! **[`TYPE`] owns the keys that type a character** — the letters, the digits,
19//! the punctuation row, the ISO and JIS extra keys and the numpad's digits and
20//! operators — for the list's typeahead. Nothing reads its value: the
21//! characters arrive as the shell's `TextCommit` with the layout applied, as a
22//! field's do. Owning them is what keeps W from being `ui_move` and a game's
23//! binding on a letter from firing while a list is up.
24//!
25//! **What navigation needs stays navigation's**: the arrows, Tab, Enter,
26//! Space and Escape are not bound here, so they fall through to `ui_move`,
27//! `ui_next`, `ui_accept` and `ui_back` — and left and right still close and
28//! open a context menu's submenus. Backspace and Delete are not bound either:
29//! a list edits nothing.
30//!
31//! **No pad button is bound.** A pad has no Home, End or page keys every
32//! platform agrees on, and nothing to type with.
33
34use super::{ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, Repeat, text};
35use crcbl_core::input::KeyCode;
36
37/// The reserved context's name.
38pub const CONTEXT: &str = "list";
39/// Go to the list's first item.
40pub const FIRST: &str = "list_first";
41/// Go to the list's last item.
42pub const LAST: &str = "list_last";
43/// Go a view's height up the list.
44pub const PAGE_UP: &str = "list_page_up";
45/// Go a view's height down the list.
46pub const PAGE_DOWN: &str = "list_page_down";
47/// A key that types a character into the list's typeahead. See the module
48/// docs.
49pub const TYPE: &str = "list_type";
50
51/// Every reserved action, in the table's order.
52pub const ACTIONS: [&str; 5] = [FIRST, LAST, PAGE_UP, PAGE_DOWN, TYPE];
53
54/// The keys of [`text::KEYS`] a list leaves alone: what navigation needs, the
55/// jump keys this context binds to actions of their own, and the keys that
56/// edit text.
57const NOT_TYPED: [KeyCode; 7] = [
58    KeyCode::Space,
59    KeyCode::Backspace,
60    KeyCode::Delete,
61    KeyCode::Home,
62    KeyCode::End,
63    KeyCode::ArrowLeft,
64    KeyCode::ArrowRight,
65];
66
67/// Every key [`TYPE`] takes: each of [`text::KEYS`] that types a character.
68pub fn type_keys() -> impl Iterator<Item = KeyCode> {
69    text::KEYS
70        .iter()
71        .copied()
72        .filter(|key| !NOT_TYPED.contains(key))
73}
74
75/// The reserved actions with their default bindings.
76fn declarations() -> [ActionDecl; ACTIONS.len()] {
77    let button = |name: &str, bindings: Vec<Binding>| ActionDecl {
78        name: name.to_owned(),
79        kind: ActionKind::Button,
80        bindings,
81    };
82    [
83        button(FIRST, vec![Binding::Key(KeyCode::Home)]),
84        button(LAST, vec![Binding::Key(KeyCode::End)]),
85        button(PAGE_UP, vec![Binding::Key(KeyCode::PageUp)]),
86        button(PAGE_DOWN, vec![Binding::Key(KeyCode::PageDown)]),
87        button(TYPE, type_keys().map(Binding::Key).collect()),
88    ]
89}
90
91/// Declare the reserved `list` context into `map`, off the stack, with
92/// [`Repeat::UI`] on the two page actions.
93///
94/// # Errors
95/// [`ActionMapError::DuplicateName`] if `map` already declares any of the
96/// reserved names — checked before any is declared, so a refused call leaves
97/// the map as it was.
98pub fn declare(map: &mut ActionMap) -> Result<(), ActionMapError> {
99    let decls = declarations();
100    if let Some(taken) = decls.iter().find(|decl| map.bindings(&decl.name).is_some()) {
101        return Err(ActionMapError::DuplicateName(taken.name.clone()));
102    }
103    for decl in decls {
104        map.try_declare_in(CONTEXT, decl)?;
105    }
106    for name in [PAGE_UP, PAGE_DOWN] {
107        map.set_repeat(name, Some(Repeat::UI))?;
108    }
109    Ok(())
110}
111
112/// Puts [`CONTEXT`] on `map`'s stack while `open`, and takes it off when not:
113/// what a caller runs once a frame with `Ui::popup_list_open`'s answer. Does
114/// nothing when the stack already agrees.
115///
116/// # Errors
117/// [`ActionMapError::UnknownContext`] if [`declare`] never ran on `map`, and
118/// [`ActionMapError::ContextNotOnTop`] if `open` is false while another
119/// context was pushed over this one — an owner popping out of order, which the
120/// stack refuses rather than hides.
121pub fn sync(map: &mut ActionMap, open: bool) -> Result<(), ActionMapError> {
122    match (open, map.is_context_active(CONTEXT)) {
123        (true, false) => map.push_context(CONTEXT),
124        (false, true) => map.pop_context(CONTEXT),
125        _ => Ok(()),
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::ui;
133
134    const TICK: f32 = 1.0 / 60.0;
135
136    /// A game that walks on WASD, jumps on Space and lifts on Page Up, with
137    /// the reserved `ui` and `list` contexts declared beside it and `ui`
138    /// pushed: a menu with a drop-down.
139    fn menu() -> ActionMap {
140        let mut map = ActionMap::new();
141        map.declare(ActionDecl {
142            name: "walk".to_owned(),
143            kind: ActionKind::Axis2,
144            bindings: vec![Binding::Wasd {
145                up: KeyCode::KeyW,
146                down: KeyCode::KeyS,
147                left: KeyCode::KeyA,
148                right: KeyCode::KeyD,
149            }],
150        });
151        map.declare(ActionDecl {
152            name: "jump".to_owned(),
153            kind: ActionKind::Button,
154            bindings: vec![Binding::Key(KeyCode::Space)],
155        });
156        map.declare(ActionDecl {
157            name: "lift".to_owned(),
158            kind: ActionKind::Button,
159            bindings: vec![Binding::Key(KeyCode::PageUp)],
160        });
161        ui::declare(&mut map).expect("nothing reserved is taken");
162        declare(&mut map).expect("nothing reserved is taken");
163        map.push_context(ui::CONTEXT).expect("declared");
164        map
165    }
166
167    /// The actions outside this context that read anything.
168    fn heard_elsewhere(map: &ActionMap) -> Vec<&'static str> {
169        let mut heard = Vec::new();
170        if map.cardinal(ui::MOVE).is_some() {
171            heard.push(ui::MOVE);
172        }
173        for name in [ui::ACCEPT, ui::BACK, ui::NEXT, ui::PREV, "jump", "lift"] {
174            if map.button_held(name) {
175                heard.push(name);
176            }
177        }
178        if map.axis2("walk") != (0.0, 0.0) {
179            heard.push("walk");
180        }
181        heard
182    }
183
184    /// **Every letter and digit, and Home, End and the page keys, are the
185    /// list's while it is pushed** — each pressed alone, reaching its own
186    /// action and nothing of `ui` or the game — and they go back once it is
187    /// popped. The letters and digits are found from [`KeyCode::ALL`], not
188    /// from [`type_keys`], so one missing from it is one this finds leaking.
189    #[test]
190    fn typing_and_jump_keys_are_the_lists_while_it_is_pushed() {
191        let mut map = menu();
192        let typed: Vec<KeyCode> = KeyCode::ALL
193            .iter()
194            .copied()
195            .filter(|key| key.as_str().starts_with("Key") || key.as_str().starts_with("Digit"))
196            .collect();
197        assert!(typed.contains(&KeyCode::KeyW), "the walk found no letters");
198        sync(&mut map, true).expect("declared");
199        assert_eq!(
200            map.active_contexts().collect::<Vec<_>>(),
201            ["gameplay", "ui", "list"]
202        );
203        let jumps = [
204            (KeyCode::Home, FIRST),
205            (KeyCode::End, LAST),
206            (KeyCode::PageUp, PAGE_UP),
207            (KeyCode::PageDown, PAGE_DOWN),
208        ];
209        let keys = typed
210            .iter()
211            .chain(&type_keys().collect::<Vec<_>>())
212            .map(|&key| (key, TYPE))
213            .chain(jumps)
214            .collect::<Vec<_>>();
215        for (key, action) in keys {
216            map.begin_tick(TICK);
217            map.key_event(key, true);
218            assert!(map.just_pressed(action), "{key} is not {action}");
219            assert_eq!(heard_elsewhere(&map), [] as [&str; 0], "{key} leaked");
220            map.key_event(key, false);
221        }
222
223        sync(&mut map, false).expect("on top");
224        let mut reached = Vec::new();
225        for key in [KeyCode::KeyW, KeyCode::PageUp] {
226            map.begin_tick(TICK);
227            map.key_event(key, true);
228            reached.extend(heard_elsewhere(&map));
229            map.key_event(key, false);
230        }
231        assert_eq!(reached, [ui::MOVE, "lift"], "popped, the keys stayed");
232    }
233
234    /// **What navigation needs still reaches `ui`** while the list is
235    /// pushed: the arrows, Tab, Enter, Space and Escape.
236    #[test]
237    fn navigation_keys_still_reach_the_ui() {
238        let mut map = menu();
239        sync(&mut map, true).expect("declared");
240        for key in [
241            KeyCode::ArrowUp,
242            KeyCode::ArrowDown,
243            KeyCode::ArrowLeft,
244            KeyCode::ArrowRight,
245        ] {
246            map.begin_tick(TICK);
247            map.key_event(key, true);
248            assert!(map.cardinal(ui::MOVE).is_some(), "{key} is not ui_move");
249            map.key_event(key, false);
250        }
251        for (key, action) in [
252            (KeyCode::Tab, ui::NEXT),
253            (KeyCode::Enter, ui::ACCEPT),
254            (KeyCode::Space, ui::ACCEPT),
255            (KeyCode::Escape, ui::BACK),
256        ] {
257            map.begin_tick(TICK);
258            map.key_event(key, true);
259            assert!(map.button_held(action), "{key} did not reach {action}");
260            assert!(!map.button_held(TYPE));
261            map.key_event(key, false);
262        }
263    }
264
265    /// **A held page key repeats**, as a held arrow walks a list.
266    #[test]
267    fn a_held_page_key_repeats() {
268        let mut map = menu();
269        sync(&mut map, true).expect("declared");
270        map.begin_tick(TICK);
271        map.key_event(KeyCode::PageDown, true);
272        assert!(map.repeated(PAGE_DOWN), "the press did not step");
273        map.begin_tick(crate::REPEAT_DELAY);
274        assert!(map.repeated(PAGE_DOWN), "the hold did not repeat");
275    }
276
277    /// Sync is idempotent, refuses to pop out of order, and needs the
278    /// declaration; a clash declares nothing.
279    #[test]
280    fn sync_agrees_with_the_stack_and_a_clash_declares_nothing() {
281        let mut map = menu();
282        sync(&mut map, false).expect("already off");
283        sync(&mut map, true).expect("declared");
284        sync(&mut map, true).expect("already on");
285        map.declare_in(
286            "modal",
287            ActionDecl {
288                name: "close".to_owned(),
289                kind: ActionKind::Button,
290                bindings: vec![],
291            },
292        );
293        map.push_context("modal").expect("declared");
294        assert_eq!(
295            sync(&mut map, false),
296            Err(ActionMapError::ContextNotOnTop(CONTEXT.to_owned()))
297        );
298        assert_eq!(
299            sync(&mut ActionMap::new(), true),
300            Err(ActionMapError::UnknownContext(CONTEXT.to_owned()))
301        );
302
303        let mut taken = ActionMap::new();
304        taken.declare(ActionDecl {
305            name: PAGE_UP.to_owned(),
306            kind: ActionKind::Button,
307            bindings: vec![],
308        });
309        assert_eq!(
310            declare(&mut taken),
311            Err(ActionMapError::DuplicateName(PAGE_UP.to_owned()))
312        );
313        assert_eq!(taken.action_names().collect::<Vec<_>>(), [PAGE_UP]);
314    }
315}