1use super::{ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, Repeat, text};
35use crcbl_core::input::KeyCode;
36
37pub const CONTEXT: &str = "list";
39pub const FIRST: &str = "list_first";
41pub const LAST: &str = "list_last";
43pub const PAGE_UP: &str = "list_page_up";
45pub const PAGE_DOWN: &str = "list_page_down";
47pub const TYPE: &str = "list_type";
50
51pub const ACTIONS: [&str; 5] = [FIRST, LAST, PAGE_UP, PAGE_DOWN, TYPE];
53
54const 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
67pub fn type_keys() -> impl Iterator<Item = KeyCode> {
69 text::KEYS
70 .iter()
71 .copied()
72 .filter(|key| !NOT_TYPED.contains(key))
73}
74
75fn 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
91pub 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
112pub 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 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 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 #[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 #[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 #[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 #[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}