1use super::{
37 ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, Modifier, PAD_ACTIVITY_THRESHOLD,
38 PadButton, Repeat, Stick,
39};
40use crcbl_core::input::KeyCode;
41
42pub const CONTEXT: &str = "ui";
44pub const MOVE: &str = "ui_move";
46pub const NEXT: &str = "ui_next";
48pub const PREV: &str = "ui_prev";
50pub const ACCEPT: &str = "ui_accept";
52pub const BACK: &str = "ui_back";
54pub const MENU: &str = "ui_menu";
57
58pub const ACTIONS: [&str; 6] = [MOVE, NEXT, PREV, ACCEPT, BACK, MENU];
60
61pub const STICK_DEADZONE: f32 = PAD_ACTIVITY_THRESHOLD;
65
66fn declarations() -> [ActionDecl; ACTIONS.len()] {
68 let button = |name: &str, bindings: Vec<Binding>| ActionDecl {
69 name: name.to_owned(),
70 kind: ActionKind::Button,
71 bindings,
72 };
73 [
74 ActionDecl {
75 name: MOVE.to_owned(),
76 kind: ActionKind::Axis2,
77 bindings: vec![
78 Binding::Wasd {
79 up: KeyCode::ArrowUp,
80 down: KeyCode::ArrowDown,
81 left: KeyCode::ArrowLeft,
82 right: KeyCode::ArrowRight,
83 },
84 Binding::Wasd {
85 up: KeyCode::KeyW,
86 down: KeyCode::KeyS,
87 left: KeyCode::KeyA,
88 right: KeyCode::KeyD,
89 },
90 Binding::PadStick {
91 stick: Stick::Left,
92 deadzone: STICK_DEADZONE,
93 },
94 Binding::PadDpad,
95 ],
96 },
97 button(
98 NEXT,
99 vec![
100 Binding::Key(KeyCode::Tab),
101 Binding::PadButton(PadButton::RightShoulder),
102 ],
103 ),
104 button(
105 PREV,
106 vec![
107 Binding::Chord {
108 modifier: Modifier::Shift,
109 key: KeyCode::Tab,
110 },
111 Binding::PadButton(PadButton::LeftShoulder),
112 ],
113 ),
114 button(
115 ACCEPT,
116 vec![
117 Binding::Key(KeyCode::Enter),
118 Binding::Key(KeyCode::Space),
119 Binding::PadButton(PadButton::South),
120 ],
121 ),
122 button(
123 BACK,
124 vec![
125 Binding::Key(KeyCode::Escape),
126 Binding::PadButton(PadButton::East),
127 ],
128 ),
129 button(
130 MENU,
131 vec![
132 Binding::Key(KeyCode::ContextMenu),
133 Binding::Chord {
134 modifier: Modifier::Shift,
135 key: KeyCode::F10,
136 },
137 ],
138 ),
139 ]
140}
141
142pub fn declare(map: &mut ActionMap) -> Result<(), ActionMapError> {
150 let decls = declarations();
151 if let Some(taken) = decls.iter().find(|decl| map.bindings(&decl.name).is_some()) {
152 return Err(ActionMapError::DuplicateName(taken.name.clone()));
153 }
154 for decl in decls {
155 map.try_declare_in(CONTEXT, decl)?;
156 }
157 for name in [MOVE, NEXT, PREV] {
158 map.set_repeat(name, Some(Repeat::UI))?;
159 }
160 Ok(())
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::{Cardinal, GAMEPLAY_CONTEXT};
167
168 const TICK: f32 = 1.0 / 60.0;
169
170 fn declared() -> ActionMap {
171 let mut map = ActionMap::new();
172 map.declare(ActionDecl {
173 name: "walk".to_owned(),
174 kind: ActionKind::Axis2,
175 bindings: vec![Binding::Wasd {
176 up: KeyCode::KeyW,
177 down: KeyCode::KeyS,
178 left: KeyCode::KeyA,
179 right: KeyCode::KeyD,
180 }],
181 });
182 map.declare(ActionDecl {
183 name: "jump".to_owned(),
184 kind: ActionKind::Button,
185 bindings: vec![Binding::Key(KeyCode::Space)],
186 });
187 declare(&mut map).expect("nothing reserved is taken");
188 map
189 }
190
191 #[test]
194 fn the_ui_context_is_declared_off_the_stack() {
195 let mut map = declared();
196 for name in ACTIONS {
197 assert_eq!(map.context_of(name), Some(CONTEXT), "{name}");
198 }
199 assert_eq!(
200 map.active_contexts().collect::<Vec<_>>(),
201 [GAMEPLAY_CONTEXT]
202 );
203 map.key_event(KeyCode::KeyW, true);
204 map.key_event(KeyCode::Space, true);
205 assert_eq!(map.axis2("walk"), (0.0, 1.0));
206 assert!(map.button_held("jump"));
207 assert_eq!(map.cardinal(MOVE), None);
208 assert!(!map.button_held(ACCEPT));
209 }
210
211 #[test]
214 fn pushed_it_drives_the_reserved_actions_from_the_table() {
215 let mut map = declared();
216 map.push_context(CONTEXT).expect("declared");
217
218 map.begin_tick(TICK);
219 map.key_event(KeyCode::ArrowLeft, true);
220 assert_eq!(map.cardinal(MOVE), Some(Cardinal::Left));
221 map.key_event(KeyCode::ArrowLeft, false);
222 map.key_event(KeyCode::KeyS, true);
223 assert_eq!(map.cardinal(MOVE), Some(Cardinal::Down));
224 assert_eq!(map.axis2("walk"), (0.0, 0.0), "S is the UI's now");
225 map.key_event(KeyCode::KeyS, false);
226
227 for key in [KeyCode::Enter, KeyCode::Space] {
228 map.begin_tick(TICK);
229 map.key_event(key, true);
230 assert!(map.just_pressed(ACCEPT), "{key}");
231 assert!(!map.button_held("jump"));
232 map.key_event(key, false);
233 }
234
235 map.begin_tick(TICK);
236 map.key_event(KeyCode::Escape, true);
237 assert!(map.just_pressed(BACK));
238
239 map.begin_tick(TICK);
240 map.key_event(KeyCode::Tab, true);
241 assert!(map.repeated(NEXT) && !map.button_held(PREV));
242 map.key_event(KeyCode::Tab, false);
243 map.begin_tick(TICK);
244 map.key_event(KeyCode::ShiftRight, true);
245 map.key_event(KeyCode::Tab, true);
246 assert!(map.repeated(PREV), "Shift+Tab is ui_prev");
247 assert!(!map.button_held(NEXT), "and not ui_next as well");
248 map.key_event(KeyCode::Tab, false);
249
250 map.begin_tick(TICK);
251 map.key_event(KeyCode::F10, true);
252 assert!(map.just_pressed(MENU), "Shift+F10 is ui_menu");
253 map.key_event(KeyCode::F10, false);
254 map.key_event(KeyCode::ShiftRight, false);
255 map.begin_tick(TICK);
256 map.key_event(KeyCode::F10, true);
257 assert!(!map.button_held(MENU), "F10 alone is not");
258 map.key_event(KeyCode::F10, false);
259 map.begin_tick(TICK);
260 map.key_event(KeyCode::ContextMenu, true);
261 assert!(map.just_pressed(MENU), "the menu key is ui_menu");
262 }
263
264 #[test]
269 fn pushed_it_drives_the_reserved_actions_from_a_pad() {
270 use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PadAxis, PadKind};
271 let pad = GamepadId(3);
272 let at_rest = GamepadSnapshot::neutral(PadKind::Xbox);
273 let state = |snapshot| GamepadEvent::State { id: pad, snapshot };
274 let holding = |button: PadButton| GamepadSnapshot {
275 buttons: [button].into_iter().collect(),
276 ..at_rest
277 };
278 let mut map = declared();
279 map.push_context(CONTEXT).expect("declared");
280
281 map.begin_tick(TICK);
282 let mut drift = at_rest;
283 drift.axes[PadAxis::LeftY as usize] = -STICK_DEADZONE / 2.0;
284 map.gamepad_event(&state(drift));
285 assert_eq!(map.cardinal(MOVE), None, "drift inside the dead zone");
286 let mut down = at_rest;
287 down.axes[PadAxis::LeftY as usize] = -1.0;
288 map.gamepad_event(&state(down));
289 assert_eq!(map.cardinal(MOVE), Some(Cardinal::Down));
290 assert_eq!(map.axis2("walk"), (0.0, 0.0), "the game binds no stick");
291 map.gamepad_event(&state(at_rest));
292
293 for (button, direction) in [
294 (PadButton::DpadUp, Cardinal::Up),
295 (PadButton::DpadDown, Cardinal::Down),
296 (PadButton::DpadLeft, Cardinal::Left),
297 (PadButton::DpadRight, Cardinal::Right),
298 ] {
299 map.begin_tick(TICK);
300 map.gamepad_event(&state(holding(button)));
301 assert_eq!(map.cardinal(MOVE), Some(direction), "{button:?}");
302 assert!(map.repeated(MOVE), "{button:?} steps once on the press");
303 map.gamepad_event(&state(at_rest));
304 }
305
306 for (button, action) in [
307 (PadButton::RightShoulder, NEXT),
308 (PadButton::LeftShoulder, PREV),
309 (PadButton::South, ACCEPT),
310 (PadButton::East, BACK),
311 ] {
312 map.begin_tick(TICK);
313 map.gamepad_event(&state(holding(button)));
314 assert!(map.just_pressed(action), "{button:?} is {action}");
315 map.gamepad_event(&state(at_rest));
316 }
317 }
318
319 #[test]
321 fn the_reserved_actions_rebind() {
322 let mut map = declared();
323 map.push_context(CONTEXT).expect("declared");
324 map.rebind(ACCEPT, vec![Binding::Key(KeyCode::KeyE)])
325 .expect("declared");
326 map.key_event(KeyCode::KeyE, true);
327 map.key_event(KeyCode::Space, true);
328 assert!(map.button_held(ACCEPT));
329 assert!(
330 map.button_held("jump"),
331 "Space is no longer the UI's, so it falls through"
332 );
333 }
334
335 #[test]
337 fn a_clash_declares_nothing() {
338 let mut map = ActionMap::new();
339 map.declare(ActionDecl {
340 name: BACK.to_owned(),
341 kind: ActionKind::Button,
342 bindings: vec![],
343 });
344 assert_eq!(
345 declare(&mut map),
346 Err(ActionMapError::DuplicateName(BACK.to_owned())),
347 );
348 assert_eq!(map.action_names().collect::<Vec<_>>(), [BACK]);
349 }
350}