1use super::{ActionDecl, ActionKind, ActionMap, ActionMapError, Binding};
29use crcbl_core::input::KeyCode;
30
31pub const CONTEXT: &str = "text";
33pub const TYPE: &str = "text_type";
35
36pub 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
112pub 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
127pub 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 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 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 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 #[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 #[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 #[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 #[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}