use super::{ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, Repeat, text};
use crcbl_core::input::KeyCode;
pub const CONTEXT: &str = "list";
pub const FIRST: &str = "list_first";
pub const LAST: &str = "list_last";
pub const PAGE_UP: &str = "list_page_up";
pub const PAGE_DOWN: &str = "list_page_down";
pub const TYPE: &str = "list_type";
pub const ACTIONS: [&str; 5] = [FIRST, LAST, PAGE_UP, PAGE_DOWN, TYPE];
const NOT_TYPED: [KeyCode; 7] = [
KeyCode::Space,
KeyCode::Backspace,
KeyCode::Delete,
KeyCode::Home,
KeyCode::End,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
];
pub fn type_keys() -> impl Iterator<Item = KeyCode> {
text::KEYS
.iter()
.copied()
.filter(|key| !NOT_TYPED.contains(key))
}
fn declarations() -> [ActionDecl; ACTIONS.len()] {
let button = |name: &str, bindings: Vec<Binding>| ActionDecl {
name: name.to_owned(),
kind: ActionKind::Button,
bindings,
};
[
button(FIRST, vec![Binding::Key(KeyCode::Home)]),
button(LAST, vec![Binding::Key(KeyCode::End)]),
button(PAGE_UP, vec![Binding::Key(KeyCode::PageUp)]),
button(PAGE_DOWN, vec![Binding::Key(KeyCode::PageDown)]),
button(TYPE, type_keys().map(Binding::Key).collect()),
]
}
pub fn declare(map: &mut ActionMap) -> Result<(), ActionMapError> {
let decls = declarations();
if let Some(taken) = decls.iter().find(|decl| map.bindings(&decl.name).is_some()) {
return Err(ActionMapError::DuplicateName(taken.name.clone()));
}
for decl in decls {
map.try_declare_in(CONTEXT, decl)?;
}
for name in [PAGE_UP, PAGE_DOWN] {
map.set_repeat(name, Some(Repeat::UI))?;
}
Ok(())
}
pub fn sync(map: &mut ActionMap, open: bool) -> Result<(), ActionMapError> {
match (open, map.is_context_active(CONTEXT)) {
(true, false) => map.push_context(CONTEXT),
(false, true) => map.pop_context(CONTEXT),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui;
const TICK: f32 = 1.0 / 60.0;
fn menu() -> ActionMap {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "walk".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}],
});
map.declare(ActionDecl {
name: "jump".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::Space)],
});
map.declare(ActionDecl {
name: "lift".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::PageUp)],
});
ui::declare(&mut map).expect("nothing reserved is taken");
declare(&mut map).expect("nothing reserved is taken");
map.push_context(ui::CONTEXT).expect("declared");
map
}
fn heard_elsewhere(map: &ActionMap) -> Vec<&'static str> {
let mut heard = Vec::new();
if map.cardinal(ui::MOVE).is_some() {
heard.push(ui::MOVE);
}
for name in [ui::ACCEPT, ui::BACK, ui::NEXT, ui::PREV, "jump", "lift"] {
if map.button_held(name) {
heard.push(name);
}
}
if map.axis2("walk") != (0.0, 0.0) {
heard.push("walk");
}
heard
}
#[test]
fn typing_and_jump_keys_are_the_lists_while_it_is_pushed() {
let mut map = menu();
let typed: Vec<KeyCode> = KeyCode::ALL
.iter()
.copied()
.filter(|key| key.as_str().starts_with("Key") || key.as_str().starts_with("Digit"))
.collect();
assert!(typed.contains(&KeyCode::KeyW), "the walk found no letters");
sync(&mut map, true).expect("declared");
assert_eq!(
map.active_contexts().collect::<Vec<_>>(),
["gameplay", "ui", "list"]
);
let jumps = [
(KeyCode::Home, FIRST),
(KeyCode::End, LAST),
(KeyCode::PageUp, PAGE_UP),
(KeyCode::PageDown, PAGE_DOWN),
];
let keys = typed
.iter()
.chain(&type_keys().collect::<Vec<_>>())
.map(|&key| (key, TYPE))
.chain(jumps)
.collect::<Vec<_>>();
for (key, action) in keys {
map.begin_tick(TICK);
map.key_event(key, true);
assert!(map.just_pressed(action), "{key} is not {action}");
assert_eq!(heard_elsewhere(&map), [] as [&str; 0], "{key} leaked");
map.key_event(key, false);
}
sync(&mut map, false).expect("on top");
let mut reached = Vec::new();
for key in [KeyCode::KeyW, KeyCode::PageUp] {
map.begin_tick(TICK);
map.key_event(key, true);
reached.extend(heard_elsewhere(&map));
map.key_event(key, false);
}
assert_eq!(reached, [ui::MOVE, "lift"], "popped, the keys stayed");
}
#[test]
fn navigation_keys_still_reach_the_ui() {
let mut map = menu();
sync(&mut map, true).expect("declared");
for key in [
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
] {
map.begin_tick(TICK);
map.key_event(key, true);
assert!(map.cardinal(ui::MOVE).is_some(), "{key} is not ui_move");
map.key_event(key, false);
}
for (key, action) in [
(KeyCode::Tab, ui::NEXT),
(KeyCode::Enter, ui::ACCEPT),
(KeyCode::Space, ui::ACCEPT),
(KeyCode::Escape, ui::BACK),
] {
map.begin_tick(TICK);
map.key_event(key, true);
assert!(map.button_held(action), "{key} did not reach {action}");
assert!(!map.button_held(TYPE));
map.key_event(key, false);
}
}
#[test]
fn a_held_page_key_repeats() {
let mut map = menu();
sync(&mut map, true).expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::PageDown, true);
assert!(map.repeated(PAGE_DOWN), "the press did not step");
map.begin_tick(crate::REPEAT_DELAY);
assert!(map.repeated(PAGE_DOWN), "the hold did not repeat");
}
#[test]
fn sync_agrees_with_the_stack_and_a_clash_declares_nothing() {
let mut map = menu();
sync(&mut map, false).expect("already off");
sync(&mut map, true).expect("declared");
sync(&mut map, true).expect("already on");
map.declare_in(
"modal",
ActionDecl {
name: "close".to_owned(),
kind: ActionKind::Button,
bindings: vec![],
},
);
map.push_context("modal").expect("declared");
assert_eq!(
sync(&mut map, false),
Err(ActionMapError::ContextNotOnTop(CONTEXT.to_owned()))
);
assert_eq!(
sync(&mut ActionMap::new(), true),
Err(ActionMapError::UnknownContext(CONTEXT.to_owned()))
);
let mut taken = ActionMap::new();
taken.declare(ActionDecl {
name: PAGE_UP.to_owned(),
kind: ActionKind::Button,
bindings: vec![],
});
assert_eq!(
declare(&mut taken),
Err(ActionMapError::DuplicateName(PAGE_UP.to_owned()))
);
assert_eq!(taken.action_names().collect::<Vec<_>>(), [PAGE_UP]);
}
}