use std::collections::{HashMap, HashSet};
use crcbl_core::input::{KeyCode, PointerButton};
use super::{
ActionMap, ActionMapError, Binding, HeldKeys, Modifier, PadButton, PadButtons, Stick, Trigger,
};
use crate::trace::TracedInput;
pub const GAMEPLAY_CONTEXT: &str = "gameplay";
pub const GLOBAL_CONTEXT: &str = "global";
const GLOBAL_INDEX: usize = 1;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Routes {
keys: HashMap<KeyCode, usize>,
chords: HashMap<KeyCode, Vec<Modifier>>,
pad_chords: HashMap<PadButton, Vec<PadButton>>,
outranking_pad_chords: HashMap<PadButton, Vec<(PadButton, usize)>>,
button_chords: HashMap<PointerButton, Vec<Modifier>>,
buttons: HashMap<PointerButton, usize>,
controls: HashMap<String, usize>,
motion: Option<usize>,
scroll: Option<usize>,
scroll_chords: Vec<KeyCode>,
pub(crate) pointer: Option<usize>,
pad_buttons: HashMap<PadButton, usize>,
pad_sticks: HashMap<Stick, usize>,
pad_triggers: HashMap<Trigger, usize>,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Suppressed {
pub(crate) keys: HashSet<KeyCode>,
pub(crate) buttons: HashSet<PointerButton>,
pub(crate) controls: HashSet<String>,
pub(crate) pad_buttons: PadButtons,
pub(crate) control_sticks: HashSet<String>,
pub(crate) pad_sticks: HashSet<Stick>,
pub(crate) pad_triggers: HashSet<Trigger>,
}
impl Suppressed {
fn extend(&mut self, other: Self) {
self.keys.extend(other.keys);
self.buttons.extend(other.buttons);
self.controls.extend(other.controls);
self.pad_buttons = self.pad_buttons.union(other.pad_buttons);
self.control_sticks.extend(other.control_sticks);
self.pad_sticks.extend(other.pad_sticks);
self.pad_triggers.extend(other.pad_triggers);
}
fn withholds(&self, binding: &Binding) -> bool {
let mut key = false;
binding.visit_keys(|owned| key |= self.keys.contains(&owned));
key || match binding {
Binding::ScrollChord { held } => self.keys.contains(held),
Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
self.buttons.contains(button)
}
Binding::Virtual(id) => {
self.controls.contains(id.as_str()) || self.control_sticks.contains(id.as_str())
}
Binding::PadButton(button) | Binding::PadChord { button, .. } => {
self.pad_buttons.contains(*button)
}
Binding::PadDpad => PadButton::DPAD
.iter()
.any(|&button| self.pad_buttons.contains(button)),
Binding::PadStick { stick, .. } => self.pad_sticks.contains(stick),
Binding::PadTrigger { trigger, .. } => self.pad_triggers.contains(trigger),
Binding::Key(_)
| Binding::KeyAxis { .. }
| Binding::Chord { .. }
| Binding::Wasd { .. }
| Binding::MouseMotion
| Binding::MouseScroll
| Binding::PointerPosition { .. } => false,
}
}
pub(crate) fn holds(&self, input: &TracedInput) -> bool {
match input {
TracedInput::Key(key) => self.keys.contains(key),
TracedInput::MouseButton(button) => self.buttons.contains(button),
TracedInput::Wheel => false,
TracedInput::Control(id) => self.controls.contains(id.as_str()),
TracedInput::ControlStick(id) => self.control_sticks.contains(id.as_str()),
TracedInput::PadButton(button) => self.pad_buttons.contains(*button),
TracedInput::PadStick(stick) => self.pad_sticks.contains(stick),
TracedInput::PadTrigger(trigger) => self.pad_triggers.contains(trigger),
}
}
}
pub(crate) struct View<'a> {
pub(crate) context: usize,
pub(crate) routes: &'a Routes,
pub(crate) suppressed: &'a Suppressed,
pub(crate) held_keys: &'a HeldKeys,
pub(crate) held_buttons: &'a HashSet<PointerButton>,
pub(crate) held_controls: &'a HashSet<String>,
pub(crate) held_pad_buttons: PadButtons,
}
impl View<'_> {
fn owns_held(&self, key: KeyCode) -> bool {
self.held_keys.contains_key(&key)
&& !self.suppressed.keys.contains(&key)
&& self.routes.keys.get(&key) == Some(&self.context)
}
pub(crate) fn key(&self, key: KeyCode) -> bool {
self.owns_held(key) && !self.any_held(self.routes.chords.get(&key))
}
fn any_held(&self, modifiers: Option<&Vec<Modifier>>) -> bool {
modifiers.is_some_and(|modifiers| modifiers.iter().any(|m| m.held(self.held_keys)))
}
pub(crate) fn chord(&self, modifier: Modifier, key: KeyCode) -> bool {
self.owns_held(key) && modifier.held(self.held_keys)
}
fn owns_held_button(&self, button: PointerButton) -> bool {
self.held_buttons.contains(&button)
&& !self.suppressed.buttons.contains(&button)
&& self.routes.buttons.get(&button) == Some(&self.context)
}
pub(crate) fn button(&self, button: PointerButton) -> bool {
self.owns_held_button(button) && !self.any_held(self.routes.button_chords.get(&button))
}
pub(crate) fn button_chord(&self, modifier: Modifier, button: PointerButton) -> bool {
self.owns_held_button(button) && modifier.held(self.held_keys)
}
pub(crate) fn control(&self, id: &str) -> bool {
self.held_controls.contains(id)
&& !self.suppressed.controls.contains(id)
&& self.owns_control(id)
}
pub(crate) fn stick(&self, id: &str) -> bool {
self.owns_control(id) && !self.suppressed.control_sticks.contains(id)
}
fn owns_control(&self, id: &str) -> bool {
self.routes.controls.get(id) == Some(&self.context)
}
pub(crate) fn motion(&self) -> bool {
self.routes.motion == Some(self.context)
}
pub(crate) fn scroll(&self) -> bool {
self.routes.scroll == Some(self.context) && self.wheel_key().is_none()
}
pub(crate) fn scroll_chord(&self, held: KeyCode) -> bool {
self.routes.scroll == Some(self.context) && self.wheel_key() == Some(held)
}
fn wheel_key(&self) -> Option<KeyCode> {
self.routes
.scroll_chords
.iter()
.filter(|key| !self.suppressed.keys.contains(key))
.filter_map(|&key| self.held_keys.get(&key).map(|&serial| (serial, key)))
.max()
.map(|(_, key)| key)
}
pub(crate) fn pointer(&self) -> bool {
self.routes.pointer == Some(self.context)
}
fn owns_held_pad_button(&self, button: PadButton) -> bool {
self.held_pad_buttons.contains(button)
&& !self.suppressed.pad_buttons.contains(button)
&& self.routes.pad_reader(button, self.held_pad_buttons) == Some(self.context)
}
pub(crate) fn pad_button(&self, button: PadButton) -> bool {
self.owns_held_pad_button(button)
&& !self
.routes
.pad_chords
.get(&button)
.is_some_and(|modifiers| {
modifiers
.iter()
.any(|&modifier| self.held_pad_buttons.contains(modifier))
})
}
pub(crate) fn pad_chord(&self, modifier: PadButton, button: PadButton) -> bool {
self.owns_held_pad_button(button)
&& self.held_pad_buttons.contains(modifier)
&& (self.routes.pad_buttons.get(&button) == Some(&self.context)
|| self.routes.outranks(button, modifier, self.context))
}
pub(crate) fn pad_stick(&self, stick: Stick) -> bool {
self.routes.pad_sticks.get(&stick) == Some(&self.context)
&& !self.suppressed.pad_sticks.contains(&stick)
}
pub(crate) fn trigger(&self, trigger: Trigger) -> bool {
self.routes.pad_triggers.get(&trigger) == Some(&self.context)
&& !self.suppressed.pad_triggers.contains(&trigger)
}
}
impl Routes {
pub(crate) fn is_scroll_chord_key(&self, key: KeyCode) -> bool {
self.scroll_chords.contains(&key)
}
pub(crate) fn owner(&self, input: &TracedInput, held: PadButtons) -> Option<usize> {
match input {
TracedInput::Key(key) => self.keys.get(key).copied(),
TracedInput::MouseButton(button) => self.buttons.get(button).copied(),
TracedInput::Wheel => self.scroll,
TracedInput::Control(id) | TracedInput::ControlStick(id) => {
self.controls.get(id.as_str()).copied()
}
TracedInput::PadButton(button) => self.pad_reader(*button, held),
TracedInput::PadStick(stick) => self.pad_sticks.get(stick).copied(),
TracedInput::PadTrigger(trigger) => self.pad_triggers.get(trigger).copied(),
}
}
pub(crate) fn pad_reader(&self, button: PadButton, held: PadButtons) -> Option<usize> {
self.outranking_pad_chords
.get(&button)
.and_then(|chords| {
chords
.iter()
.find(|&&(modifier, _)| held.contains(modifier))
})
.map(|&(_, context)| context)
.or_else(|| self.pad_buttons.get(&button).copied())
}
fn outranks(&self, button: PadButton, modifier: PadButton, context: usize) -> bool {
self.outranking_pad_chords
.get(&button)
.is_some_and(|chords| chords.contains(&(modifier, context)))
}
fn build(map: &ActionMap) -> Self {
let mut routes = Self::default();
let mut below_modal = false;
let mut pad_chords_above = HashSet::new();
for context in map.route_order() {
let bindings = || {
map.slots
.iter()
.filter(move |slot| slot.context == context)
.flat_map(|slot| slot.decl.bindings.iter())
};
if below_modal {
for binding in bindings() {
match binding {
Binding::MouseMotion => {
routes.motion.get_or_insert(context);
}
Binding::PointerPosition { .. } => {
routes.pointer.get_or_insert(context);
}
_ => {}
}
}
continue;
}
for binding in bindings() {
binding.visit_keys(|key| {
routes.keys.entry(key).or_insert(context);
});
match binding {
Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
routes.buttons.entry(*button).or_insert(context);
}
Binding::Virtual(id) => {
if !routes.controls.contains_key(id.as_str()) {
routes.controls.insert(id.clone(), context);
}
}
Binding::MouseMotion => {
routes.motion.get_or_insert(context);
}
Binding::MouseScroll | Binding::ScrollChord { .. } => {
routes.scroll.get_or_insert(context);
}
Binding::PointerPosition { .. } => {
routes.pointer.get_or_insert(context);
}
Binding::PadButton(button) | Binding::PadChord { button, .. } => {
routes.pad_buttons.entry(*button).or_insert(context);
}
Binding::PadDpad => {
for button in PadButton::DPAD {
routes.pad_buttons.entry(button).or_insert(context);
}
}
Binding::PadStick { stick, .. } => {
routes.pad_sticks.entry(*stick).or_insert(context);
}
Binding::PadTrigger { trigger, .. } => {
routes.pad_triggers.entry(*trigger).or_insert(context);
}
Binding::Key(_)
| Binding::KeyAxis { .. }
| Binding::Chord { .. }
| Binding::Wasd { .. } => {}
}
}
let slots = map.slots.iter().filter(|slot| slot.context == context);
for slot in slots {
for binding in &slot.decl.bindings {
match binding {
Binding::Chord { modifier, key }
if routes.keys.get(key) == Some(&context) =>
{
add_chord(&mut routes.chords, *key, *modifier);
}
Binding::ButtonChord { modifier, button }
if routes.buttons.get(button) == Some(&context) =>
{
add_chord(&mut routes.button_chords, *button, *modifier);
}
Binding::PadChord { modifier, button }
if routes.pad_buttons.get(button) == Some(&context) =>
{
add_chord(&mut routes.pad_chords, *button, *modifier);
}
Binding::PadChord { modifier, button }
if slot.pad_chords_outrank
&& !pad_chords_above.contains(&(*modifier, *button)) =>
{
add_chord(
&mut routes.outranking_pad_chords,
*button,
(*modifier, context),
);
}
Binding::ScrollChord { held }
if slot.enabled
&& routes.scroll == Some(context)
&& !routes.scroll_chords.contains(held) =>
{
routes.scroll_chords.push(*held);
}
_ => {}
}
}
}
for binding in bindings() {
if let Binding::PadChord { modifier, button } = binding {
pad_chords_above.insert((*modifier, *button));
}
}
below_modal = map.modal.contains(&context);
}
routes
}
}
fn add_chord<I: Eq + std::hash::Hash, M: PartialEq>(
chords: &mut HashMap<I, Vec<M>>,
input: I,
modifier: M,
) {
let modifiers = chords.entry(input).or_default();
if !modifiers.contains(&modifier) {
modifiers.push(modifier);
}
}
impl ActionMap {
pub fn push_context(&mut self, context: &str) -> Result<(), ActionMapError> {
self.push(context, false)
}
pub fn push_context_modal(&mut self, context: &str) -> Result<(), ActionMapError> {
self.push(context, true)
}
fn push(&mut self, context: &str, modal: bool) -> Result<(), ActionMapError> {
let index = self.context_index(context)?;
if self.is_active(index) {
return Err(ActionMapError::ContextAlreadyActive(context.to_owned()));
}
self.stack.push(index);
if modal {
self.modal.push(index);
}
self.restack();
Ok(())
}
pub fn pop_context(&mut self, context: &str) -> Result<(), ActionMapError> {
let index = self.context_index(context)?;
if self.stack.len() == 1 || self.stack.last() != Some(&index) {
return Err(ActionMapError::ContextNotOnTop(context.to_owned()));
}
self.stack.pop();
self.modal.retain(|&modal| modal != index);
self.restack();
Ok(())
}
pub fn active_contexts(&self) -> impl Iterator<Item = &str> {
self.stack
.iter()
.map(|&index| self.contexts[index].as_str())
}
#[must_use]
pub fn is_context_active(&self, context: &str) -> bool {
self.contexts
.iter()
.position(|name| name == context)
.is_some_and(|index| self.is_active(index))
}
fn route_order(&self) -> impl Iterator<Item = usize> + '_ {
std::iter::once(GLOBAL_INDEX).chain(self.stack.iter().rev().copied())
}
pub(crate) fn blocking_modal(&self, claims: impl Fn(&Binding) -> bool) -> Option<usize> {
let mut modal = None;
for context in self.route_order() {
let binds = || {
self.slots
.iter()
.filter(|slot| slot.context == context)
.any(|slot| slot.decl.bindings.iter().any(&claims))
};
if modal.is_some() && binds() {
return modal;
}
if modal.is_none() && self.modal.contains(&context) {
modal = Some(context);
}
}
None
}
#[must_use]
pub fn is_context_modal(&self, context: &str) -> bool {
self.contexts
.iter()
.position(|name| name == context)
.is_some_and(|index| self.modal.contains(&index))
}
fn is_active(&self, index: usize) -> bool {
index == GLOBAL_INDEX || self.stack.contains(&index)
}
#[must_use]
pub fn context_of(&self, name: &str) -> Option<&str> {
let &idx = self.name_to_idx.get(name)?;
Some(self.contexts[self.slots[idx].context].as_str())
}
pub fn set_pad_chords_outrank(
&mut self,
name: &str,
outrank: bool,
) -> Result<(), ActionMapError> {
let Some(&idx) = self.name_to_idx.get(name) else {
return Err(ActionMapError::UnknownAction(name.to_owned()));
};
if self.slots[idx].pad_chords_outrank != outrank {
self.slots[idx].pad_chords_outrank = outrank;
self.reroute();
}
Ok(())
}
#[must_use]
pub fn pad_chords_outrank(&self, name: &str) -> Option<bool> {
let &idx = self.name_to_idx.get(name)?;
Some(self.slots[idx].pad_chords_outrank)
}
fn context_index(&self, context: &str) -> Result<usize, ActionMapError> {
self.contexts
.iter()
.position(|name| name == context)
.ok_or_else(|| ActionMapError::UnknownContext(context.to_owned()))
}
pub(crate) fn is_live(&self, idx: usize) -> bool {
let slot = &self.slots[idx];
slot.enabled && self.is_active(slot.context)
}
pub(crate) fn reroute(&mut self) {
self.routes = Routes::build(self);
self.resolve_live();
}
fn resolve_live(&mut self) {
for idx in 0..self.slots.len() {
if self.is_live(idx) {
self.resolve_one(idx);
}
}
}
pub fn suppress_held(&mut self) {
let withheld = Suppressed {
keys: self.held_keys.keys().copied().collect(),
buttons: self.held_buttons.clone(),
controls: self.held_controls.clone(),
pad_buttons: self.held_pad_buttons,
control_sticks: self
.control_sticks
.iter()
.filter(|(_, deflection)| **deflection != (0.0, 0.0))
.map(|(id, _)| id.clone())
.collect(),
pad_sticks: [Stick::Left, Stick::Right]
.into_iter()
.filter(|&stick| !self.stick_rests(stick))
.collect(),
pad_triggers: [Trigger::Left, Trigger::Right]
.into_iter()
.filter(|&trigger| !self.trigger_rests(trigger))
.collect(),
};
for slot in &mut self.slots {
slot.patterns.cancel();
}
self.withhold(withheld);
}
pub fn suppress_held_action(&mut self, name: &str) -> Result<(), ActionMapError> {
let Some(&idx) = self.name_to_idx.get(name) else {
return Err(ActionMapError::UnknownAction(name.to_owned()));
};
let mut withheld = Suppressed::default();
for binding in &self.slots[idx].decl.bindings {
self.held_by(binding, &mut withheld);
}
self.slots[idx].patterns.cancel();
for slot in &mut self.slots {
if slot.decl.bindings.iter().any(|b| withheld.withholds(b)) {
slot.patterns.cancel();
}
}
self.withhold(withheld);
Ok(())
}
fn held_by(&self, binding: &Binding, withheld: &mut Suppressed) {
binding.visit_keys(|key| {
if self.held_keys.contains_key(&key) {
withheld.keys.insert(key);
}
});
match binding {
Binding::ScrollChord { held } if self.held_keys.contains_key(held) => {
withheld.keys.insert(*held);
}
Binding::MouseButton(button) | Binding::ButtonChord { button, .. }
if self.held_buttons.contains(button) =>
{
withheld.buttons.insert(*button);
}
Binding::Virtual(id) => {
if self.held_controls.contains(id.as_str()) {
withheld.controls.insert(id.clone());
}
if self
.control_sticks
.get(id.as_str())
.is_some_and(|&deflection| deflection != (0.0, 0.0))
{
withheld.control_sticks.insert(id.clone());
}
}
Binding::PadButton(button) | Binding::PadChord { button, .. }
if self.held_pad_buttons.contains(*button) =>
{
withheld.pad_buttons.insert(*button);
}
Binding::PadDpad => {
for button in PadButton::DPAD {
if self.held_pad_buttons.contains(button) {
withheld.pad_buttons.insert(button);
}
}
}
Binding::PadStick { stick, .. } if !self.stick_rests(*stick) => {
withheld.pad_sticks.insert(*stick);
}
Binding::PadTrigger { trigger, .. } if !self.trigger_rests(*trigger) => {
withheld.pad_triggers.insert(*trigger);
}
_ => {}
}
}
fn withhold(&mut self, withheld: Suppressed) {
self.suppressed.extend(withheld);
self.resolve_live();
}
fn restack(&mut self) {
let routes = Routes::build(self);
let old = std::mem::replace(&mut self.routes, routes);
let new = &self.routes;
for key in self.held_keys.keys() {
if old.keys.get(key) != new.keys.get(key) {
self.suppressed.keys.insert(*key);
}
}
for button in &self.held_buttons {
if old.buttons.get(button) != new.buttons.get(button) {
self.suppressed.buttons.insert(*button);
}
}
for control in &self.held_controls {
if old.controls.get(control) != new.controls.get(control) {
self.suppressed.controls.insert(control.clone());
}
}
let held = self.held_pad_buttons;
for button in PadButton::ALL {
if held.contains(button) && old.pad_reader(button, held) != new.pad_reader(button, held)
{
self.suppressed.pad_buttons.insert(button);
}
}
for idx in 0..self.slots.len() {
self.slots[idx].patterns.cancel();
if self.is_active(self.slots[idx].context) {
if self.slots[idx].enabled {
self.resolve_one(idx);
}
} else {
self.slots[idx].reset();
}
}
}
}
#[cfg(test)]
mod outrank_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, ActionValue, InputTickState, Repeat};
const TICK: f32 = 1.0 / 60.0;
fn button(name: &str, bindings: Vec<Binding>) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind: ActionKind::Button,
bindings,
}
}
fn menu_over_gameplay() -> ActionMap {
let mut map = ActionMap::new();
map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
map.declare(button("crouch", vec![Binding::Key(KeyCode::KeyC)]));
map.declare_in(
"menu",
button(
"accept",
vec![Binding::Key(KeyCode::Space), Binding::Key(KeyCode::Enter)],
),
);
map
}
#[test]
fn a_key_bound_in_both_reaches_only_the_pushed_context_then_gameplay_after_pop() {
let mut map = menu_over_gameplay();
assert_eq!(map.context_of("accept"), Some("menu"));
assert_eq!(map.context_of("jump"), Some(GAMEPLAY_CONTEXT));
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(
map.just_pressed("jump"),
"nothing is pushed: gameplay has it"
);
assert!(
!map.button_held("accept"),
"and a context off the stack is idle"
);
map.key_event(KeyCode::Space, false);
map.push_context("menu").expect("declared, not active");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(map.just_pressed("accept"), "the pushed context owns Space");
assert!(
!map.button_held("jump"),
"and gameplay beneath it does not see it"
);
map.key_event(KeyCode::Space, false);
map.pop_context("menu").expect("menu is on top");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(
map.just_pressed("jump"),
"popped: Space is gameplay's again"
);
assert!(!map.button_held("accept"));
}
fn inventory_over_gameplay() -> ActionMap {
let mut map = ActionMap::new();
map.declare(button("walk", vec![Binding::Key(KeyCode::KeyW)]));
map.declare(button(
"shoot",
vec![Binding::MouseButton(PointerButton::Left)],
));
map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
map.declare(axis(
"aim_x",
ActionKind::Axis1,
vec![Binding::PointerPosition {
axis: crate::PointerAxis::X,
}],
));
map.declare_in(
"inventory",
button("close", vec![Binding::Key(KeyCode::Escape)]),
);
map.declare_in(
GLOBAL_CONTEXT,
button("map", vec![Binding::Key(KeyCode::KeyM)]),
);
map
}
#[test]
fn a_modal_context_stops_what_it_does_not_bind() {
let mut map = inventory_over_gameplay();
map.push_context("inventory").expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::KeyW, true);
assert!(map.button_held("walk"), "a plain context lets W through");
map.key_event(KeyCode::KeyW, false);
map.pop_context("inventory").expect("on top");
map.push_context_modal("inventory").expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::KeyW, true);
map.mouse_button(PointerButton::Left, true);
map.mouse_scroll(0.0, 1.0);
map.pointer_position(0.5, 0.25);
map.key_event(KeyCode::Escape, true);
assert!(!map.button_held("walk"), "W reached gameplay");
assert!(
!map.button_held("shoot"),
"the left button reached gameplay"
);
assert_eq!(map.axis1("zoom"), 0.0, "the wheel reached gameplay");
assert!(map.just_pressed("close"), "the modal context's own key");
assert_eq!(map.axis1("aim_x"), 0.5, "the cursor is still read beneath");
}
#[test]
fn a_key_held_into_a_modal_context_waits_for_a_new_press() {
let mut map = inventory_over_gameplay();
map.begin_tick(TICK);
map.key_event(KeyCode::KeyW, true);
assert!(map.button_held("walk"));
map.push_context_modal("inventory").expect("declared");
assert!(!map.button_held("walk"), "the walk stops as it opens");
map.pop_context("inventory").expect("on top");
map.begin_tick(TICK);
assert!(!map.button_held("walk"), "and does not resume as it closes");
map.key_event(KeyCode::KeyW, false);
map.key_event(KeyCode::KeyW, true);
assert!(map.button_held("walk"), "a new press walks");
}
#[test]
fn the_global_context_stays_above_a_modal_one() {
let mut map = inventory_over_gameplay();
map.push_context_modal("inventory").expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::KeyM, true);
assert!(
map.just_pressed("map"),
"M is global's over the modal screen"
);
assert_eq!(
map.push_context(GLOBAL_CONTEXT),
Err(ActionMapError::ContextAlreadyActive(
GLOBAL_CONTEXT.to_owned()
))
);
assert_eq!(
map.pop_context(GLOBAL_CONTEXT),
Err(ActionMapError::ContextNotOnTop(GLOBAL_CONTEXT.to_owned()))
);
assert!(map.is_context_active(GLOBAL_CONTEXT));
assert_eq!(
map.active_contexts().collect::<Vec<_>>(),
[GAMEPLAY_CONTEXT, "inventory"]
);
}
#[test]
fn an_input_the_top_context_does_not_bind_falls_through() {
let mut map = menu_over_gameplay();
map.push_context("menu").expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::KeyC, true);
assert!(
map.just_pressed("crouch"),
"menu binds no C, so it falls through"
);
map.key_event(KeyCode::KeyQ, true);
assert!(!map.button_held("accept") && !map.button_held("jump"));
}
#[test]
fn nested_pushes_route_each_input_to_the_topmost_binder() {
let mut map = menu_over_gameplay();
map.declare_in(
"editor",
button("select", vec![Binding::Key(KeyCode::Enter)]),
);
map.push_context("menu").expect("declared");
map.push_context("editor").expect("declared");
assert_eq!(
map.active_contexts().collect::<Vec<_>>(),
[GAMEPLAY_CONTEXT, "menu", "editor"],
);
assert_eq!(
map.push_context("menu"),
Err(ActionMapError::ContextAlreadyActive("menu".to_owned())),
);
assert_eq!(
map.pop_context("menu"),
Err(ActionMapError::ContextNotOnTop("menu".to_owned())),
"menu is under editor, and popping it would pop editor's",
);
map.begin_tick(TICK);
map.key_event(KeyCode::Enter, true);
map.key_event(KeyCode::Space, true);
map.key_event(KeyCode::KeyC, true);
assert!(
map.button_held("select"),
"Enter: editor is topmost and binds it"
);
assert!(
map.button_held("accept"),
"Space falls through editor to menu"
);
assert!(
!map.button_held("jump"),
"and menu consumes it before gameplay"
);
assert!(map.button_held("crouch"), "C falls through both");
for key in [KeyCode::Enter, KeyCode::Space, KeyCode::KeyC] {
map.key_event(key, false);
}
map.pop_context("editor").expect("editor is on top");
map.begin_tick(TICK);
map.key_event(KeyCode::Enter, true);
assert!(map.just_pressed("accept"), "editor popped: menu owns Enter");
assert!(!map.button_held("select"), "and a popped context is idle");
}
#[test]
fn a_key_held_across_a_push_or_pop_waits_for_its_release() {
let mut map = menu_over_gameplay();
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
map.begin_tick(TICK);
map.push_context("menu").expect("declared");
assert!(
map.just_released("jump"),
"the walk stops when a menu opens over it"
);
assert!(
!map.button_held("accept"),
"and the menu is not pressed by it"
);
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true); assert!(!map.button_held("accept"));
map.key_event(KeyCode::Space, false);
map.key_event(KeyCode::Space, true);
assert!(
map.just_pressed("accept"),
"a fresh press after the lift is menu's"
);
map.pop_context("menu").expect("menu is on top");
assert!(
!map.button_held("jump"),
"the key that closed the menu does not jump"
);
map.begin_tick(TICK);
assert!(!map.button_held("jump"));
map.key_event(KeyCode::Space, false);
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(map.just_pressed("jump"));
}
#[test]
fn a_held_key_whose_owner_does_not_change_is_not_withheld() {
let mut map = menu_over_gameplay();
map.key_event(KeyCode::KeyC, true);
map.push_context("menu").expect("declared");
map.begin_tick(TICK);
assert!(map.button_held("crouch"));
assert!(!map.just_released("crouch"));
}
#[test]
fn the_stack_refuses_unknown_contexts_and_the_base() {
let mut map = menu_over_gameplay();
assert_eq!(
map.push_context("nope"),
Err(ActionMapError::UnknownContext("nope".to_owned())),
);
assert_eq!(
map.push_context(GAMEPLAY_CONTEXT),
Err(ActionMapError::ContextAlreadyActive(
GAMEPLAY_CONTEXT.to_owned()
)),
);
assert_eq!(
map.pop_context(GAMEPLAY_CONTEXT),
Err(ActionMapError::ContextNotOnTop(GAMEPLAY_CONTEXT.to_owned())),
);
assert!(map.is_context_active(GAMEPLAY_CONTEXT));
assert!(!map.is_context_active("menu"));
assert!(!map.is_context_active("nope"));
}
#[test]
fn pointer_inputs_and_controls_are_consumed_too() {
let mut map = ActionMap::new();
map.declare(button(
"fire",
vec![
Binding::MouseButton(PointerButton::Left),
Binding::Virtual("btn".to_owned()),
],
));
map.declare(ActionDecl {
name: "aim".to_owned(),
kind: ActionKind::Axis1,
bindings: vec![Binding::PointerPosition {
axis: crate::PointerAxis::X,
}],
});
map.declare_in(
"menu",
ActionDecl {
name: "cursor".to_owned(),
kind: ActionKind::Axis1,
bindings: vec![Binding::PointerPosition {
axis: crate::PointerAxis::X,
}],
},
);
map.declare_in(
"menu",
button(
"click",
vec![
Binding::MouseButton(PointerButton::Left),
Binding::Virtual("btn".to_owned()),
],
),
);
map.push_context("menu").expect("declared");
map.begin_tick(TICK);
map.pointer_position(0.5, 0.0);
map.mouse_button(PointerButton::Left, true);
map.virtual_button("btn", true);
assert!(map.just_pressed("click"));
assert!(!map.button_held("fire"));
assert_eq!(map.axis1("cursor"), 0.5);
assert_eq!(
map.axis1("aim"),
0.0,
"the menu's pointer is not the game's"
);
let Some(ActionValue::Axis1(aim)) = map.action("aim") else {
panic!("aim is an Axis1");
};
assert!(!aim.pointer_moved, "nor did it move for the game");
map.begin_tick(TICK);
assert_eq!(map.axis1("aim"), 0.0, "a consumed pointer stays consumed");
assert!(!map.button_held("fire"));
}
#[test]
fn a_rebind_moves_ownership_and_re_resolves_the_loser() {
let mut map = menu_over_gameplay();
map.push_context("menu").expect("declared");
map.key_event(KeyCode::KeyC, true);
assert!(map.button_held("crouch"));
map.rebind("accept", vec![Binding::Key(KeyCode::KeyC)])
.expect("declared");
assert!(
!map.button_held("crouch"),
"menu took C, so gameplay lost it"
);
assert!(
map.button_held("accept"),
"and a rebind resolves against held keys"
);
}
#[test]
fn a_chord_shadows_the_plain_key_and_leaves_its_modifier_alone() {
let mut map = ActionMap::new();
map.declare(button("sprint", vec![Binding::Key(KeyCode::ShiftLeft)]));
map.declare_in("menu", button("next", vec![Binding::Key(KeyCode::Tab)]));
map.declare_in(
"menu",
button(
"prev",
vec![Binding::Chord {
modifier: Modifier::Shift,
key: KeyCode::Tab,
}],
),
);
map.push_context("menu").expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::Tab, true);
assert!(map.button_held("next") && !map.button_held("prev"));
map.key_event(KeyCode::ShiftLeft, true);
assert!(
map.just_released("next"),
"Shift turns the held Tab into Shift+Tab"
);
assert!(map.just_pressed("prev"));
assert!(
map.button_held("sprint"),
"the modifier still reaches gameplay"
);
map.key_event(KeyCode::ShiftLeft, false);
assert!(!map.button_held("prev") && map.button_held("next"));
}
fn alt_right() -> Binding {
Binding::ButtonChord {
modifier: Modifier::Alt,
button: PointerButton::Right,
}
}
fn sights() -> ActionMap {
let mut map = ActionMap::new();
map.declare(button(
"aim",
vec![Binding::MouseButton(PointerButton::Right)],
));
map.declare(button("zoom", vec![alt_right()]));
map.declare(button("lean", vec![Binding::Key(KeyCode::AltLeft)]));
map
}
#[test]
fn a_button_chord_shadows_the_plain_button_and_leaves_its_modifier_alone() {
let mut map = sights();
map.begin_tick(TICK);
map.mouse_button(PointerButton::Right, true);
assert!(map.button_held("aim") && !map.button_held("zoom"));
map.mouse_button(PointerButton::Right, false);
map.begin_tick(TICK);
map.key_event(KeyCode::AltLeft, true);
map.mouse_button(PointerButton::Right, true);
assert!(map.just_pressed("zoom"));
assert!(!map.button_held("aim"), "the plain binding fired too");
assert!(map.button_held("lean"), "the modifier was consumed");
}
#[test]
fn a_modifier_hands_a_held_mouse_button_to_the_chord_and_back() {
let mut map = sights();
map.begin_tick(TICK);
map.mouse_button(PointerButton::Right, true);
assert!(map.just_pressed("aim"));
map.begin_tick(TICK);
map.key_event(KeyCode::AltRight, true);
assert!(
map.just_released("aim"),
"Alt turns the held right button into Alt+right"
);
assert!(map.just_pressed("zoom"));
map.begin_tick(TICK);
map.key_event(KeyCode::AltRight, false);
assert!(map.just_released("zoom"));
assert!(map.just_pressed("aim"), "the button went back to the aim");
}
#[test]
fn a_button_chord_shadows_only_in_the_context_that_owns_its_button() {
let mut map = ActionMap::new();
map.declare(button(
"aim",
vec![Binding::MouseButton(PointerButton::Right)],
));
map.declare_in("optic", button("magnify", vec![alt_right()]));
map.key_event(KeyCode::AltLeft, true);
map.mouse_button(PointerButton::Right, true);
assert!(
map.button_held("aim"),
"a chord off the stack shadowed the button"
);
assert!(!map.button_held("magnify"));
map.mouse_button(PointerButton::Right, false);
map.push_context("optic").expect("declared");
map.mouse_button(PointerButton::Right, true);
assert!(map.button_held("magnify"));
assert!(!map.button_held("aim"));
}
fn axis(name: &str, kind: ActionKind, bindings: Vec<Binding>) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind,
bindings,
}
}
fn wasd() -> Binding {
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}
}
#[test]
fn suppress_held_withholds_a_key_until_it_is_pressed_again() {
let mut map = menu_over_gameplay();
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
map.begin_tick(TICK);
map.suppress_held();
assert!(map.just_released("jump"), "the walk stops on the spot");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true); assert!(!map.button_held("jump"));
map.key_event(KeyCode::Space, false);
map.key_event(KeyCode::Space, true);
assert!(map.just_pressed("jump"), "pressed again");
}
#[test]
fn suppress_held_mid_press_fires_no_pattern_and_the_next_press_does() {
let mut map = menu_over_gameplay();
map.set_tap("jump", Some(crate::Tap::default()))
.expect("declared");
map.set_hold("jump", Some(crate::Hold::default()))
.expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
map.begin_tick(TICK);
map.suppress_held();
let mut fired = map.tapped("jump");
for _ in 0..60 {
map.begin_tick(TICK);
fired |= map.hold_fired("jump");
}
map.key_event(KeyCode::Space, false);
fired |= map.tapped("jump");
assert!(!fired, "neither a hold nor a tap");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
map.begin_tick(TICK);
map.key_event(KeyCode::Space, false);
assert!(map.tapped("jump"), "suppress, release, press: it taps");
}
#[test]
fn suppress_held_drops_a_waiting_first_tap() {
let mut map = menu_over_gameplay();
map.set_double_tap("jump", Some(crate::DoubleTap::default()))
.expect("declared");
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
map.key_event(KeyCode::Space, false);
map.suppress_held();
map.begin_tick(TICK);
map.key_event(KeyCode::Space, true);
assert!(!map.double_tapped("jump"));
}
#[test]
fn suppress_held_withholds_an_axis_per_key() {
let mut map = ActionMap::new();
map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
map.declare(axis(
"zoom",
ActionKind::Axis1,
vec![Binding::KeyAxis {
negative: KeyCode::KeyQ,
positive: KeyCode::KeyE,
}],
));
map.key_event(KeyCode::KeyW, true);
map.key_event(KeyCode::KeyE, true);
map.suppress_held();
assert_eq!(map.axis2("move"), (0.0, 0.0));
assert_eq!(map.axis1("zoom"), 0.0);
map.begin_tick(TICK);
assert_eq!(map.axis2("move"), (0.0, 0.0), "still held, still withheld");
map.key_event(KeyCode::KeyD, true);
assert_eq!(map.axis2("move"), (1.0, 0.0), "a fresh key moves");
map.key_event(KeyCode::KeyQ, true);
assert_eq!(map.axis1("zoom"), -1.0);
map.key_event(KeyCode::KeyW, false);
map.key_event(KeyCode::KeyW, true);
let (x, y) = map.axis2("move");
assert!(
x > 0.0 && y > 0.0,
"W pressed again: diagonal, got {x}, {y}"
);
}
#[test]
fn suppress_held_withholds_a_chord_until_its_key_is_pressed_again() {
let mut map = ActionMap::new();
map.declare(button(
"save",
vec![Binding::Chord {
modifier: Modifier::Control,
key: KeyCode::KeyS,
}],
));
map.key_event(KeyCode::ControlLeft, true);
map.key_event(KeyCode::KeyS, true);
assert!(map.button_held("save"));
map.suppress_held();
assert!(!map.button_held("save"));
map.key_event(KeyCode::KeyS, false);
map.key_event(KeyCode::KeyS, true);
assert!(map.just_pressed("save"), "Ctrl never let go");
}
#[test]
fn suppress_held_withholds_a_button_chord_until_its_button_is_pressed_again() {
let mut map = sights();
map.key_event(KeyCode::AltLeft, true);
map.mouse_button(PointerButton::Right, true);
assert!(map.button_held("zoom"));
map.suppress_held();
assert!(!map.button_held("zoom"));
map.mouse_button(PointerButton::Right, false);
map.mouse_button(PointerButton::Right, true);
assert!(map.just_pressed("zoom"), "Alt never let go");
}
#[test]
fn suppress_held_withholds_pointer_buttons_and_on_screen_controls() {
let mut map = ActionMap::new();
map.declare(button(
"fire",
vec![
Binding::MouseButton(PointerButton::Left),
Binding::Virtual("btn".to_owned()),
],
));
map.declare(axis(
"move",
ActionKind::Axis2,
vec![Binding::Virtual("stick".to_owned())],
));
map.mouse_button(PointerButton::Left, true);
map.virtual_button("btn", true);
map.virtual_stick("stick", 0.0, 1.0);
map.suppress_held();
assert!(!map.button_held("fire"));
assert_eq!(map.axis2("move"), (0.0, 0.0));
map.virtual_stick("stick", 0.5, 0.5);
assert_eq!(map.axis2("move"), (0.0, 0.0), "moved, never centred");
map.virtual_stick("stick", 0.0, 0.0);
map.virtual_stick("stick", 0.0, 1.0);
assert_eq!(map.axis2("move"), (0.0, 1.0), "centred, then pushed");
map.mouse_button(PointerButton::Left, false);
assert!(
!map.button_held("fire"),
"the on-screen button is still held"
);
map.virtual_button("btn", false);
map.mouse_button(PointerButton::Left, true);
assert!(map.just_pressed("fire"));
}
fn pad(snapshot: impl FnOnce(&mut crate::GamepadSnapshot)) -> crate::GamepadEvent {
let mut state = crate::GamepadSnapshot::neutral(crate::PadKind::Xbox);
snapshot(&mut state);
crate::GamepadEvent::State {
id: crate::GamepadId(1),
snapshot: state,
}
}
fn stick_y(y: f32) -> crate::GamepadEvent {
pad(|state| state.axes[crate::PadAxis::LeftY as usize] = y)
}
#[test]
fn suppress_held_withholds_pad_buttons_sticks_and_triggers() {
let mut map = ActionMap::new();
map.declare(axis(
"move",
ActionKind::Axis2,
vec![Binding::PadStick {
stick: Stick::Left,
deadzone: 0.2,
}],
));
map.declare(button(
"fire",
vec![Binding::PadTrigger {
trigger: Trigger::Right,
threshold: 0.5,
}],
));
map.declare(button("jump", vec![Binding::PadButton(PadButton::South)]));
map.gamepad_event(&pad(|state| {
state.axes[crate::PadAxis::LeftY as usize] = 1.0;
state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
state.buttons.insert(PadButton::South);
}));
assert_eq!(map.axis2("move"), (0.0, 1.0));
assert!(map.button_held("fire") && map.button_held("jump"));
map.suppress_held();
assert_eq!(map.axis2("move"), (0.0, 0.0));
assert!(!map.button_held("fire") && !map.button_held("jump"));
map.gamepad_event(&stick_y(0.8));
assert_eq!(map.axis2("move"), (0.0, 0.0), "eased off, never rested");
assert!(!map.button_held("jump"), "and South was let go of");
map.gamepad_event(&stick_y(0.1));
map.gamepad_event(&stick_y(1.0));
assert_eq!(map.axis2("move"), (0.0, 1.0), "rested, then pushed");
map.gamepad_event(&pad(|state| {
state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
state.buttons.insert(PadButton::South);
}));
assert!(map.button_held("fire") && map.button_held("jump"));
}
#[test]
fn suppress_held_action_withholds_only_that_actions_inputs() {
let mut map = ActionMap::new();
map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
map.declare(button("forward", vec![Binding::Key(KeyCode::KeyW)]));
map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
map.key_event(KeyCode::KeyW, true);
map.key_event(KeyCode::Space, true);
map.suppress_held_action("move").expect("declared");
assert_eq!(map.axis2("move"), (0.0, 0.0));
assert!(!map.button_held("forward"), "W itself is withheld");
assert!(map.button_held("jump"), "Space is not move's");
map.key_event(KeyCode::KeyW, false);
map.key_event(KeyCode::KeyW, true);
assert_eq!(map.axis2("move"), (0.0, 1.0));
assert_eq!(
map.suppress_held_action("nope"),
Err(ActionMapError::UnknownAction("nope".to_owned())),
);
}
fn scroll_chords() -> ActionMap {
let mut map = ActionMap::new();
map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
for (name, held) in [
("zoom_z", KeyCode::KeyZ),
("zoom_ctrl", KeyCode::ControlLeft),
] {
map.declare(axis(
name,
ActionKind::Axis1,
vec![Binding::ScrollChord { held }],
));
}
map.declare(button("z", vec![Binding::Key(KeyCode::KeyZ)]));
map
}
fn wheel(map: &mut ActionMap) -> [f32; 3] {
map.begin_tick(TICK);
map.mouse_scroll(0.0, 1.0);
["zoom", "zoom_z", "zoom_ctrl"].map(|name| map.axis1(name))
}
#[test]
fn a_disabled_scroll_chord_takes_the_wheel_from_nothing() {
let mut map = scroll_chords();
map.key_event(KeyCode::ControlLeft, true);
map.set_enabled("zoom_z", false);
map.key_event(KeyCode::KeyZ, true);
assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Ctrl keeps the wheel");
map.key_event(KeyCode::ControlLeft, false);
assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "Z silences nothing");
map.key_event(KeyCode::ControlLeft, true);
map.set_enabled("zoom_z", true);
assert_eq!(
wheel(&mut map),
[0.0, 0.0, 1.0],
"Ctrl went down after Z, so it is the newer press"
);
map.key_event(KeyCode::ControlLeft, false);
map.key_event(KeyCode::ControlLeft, true);
map.key_event(KeyCode::KeyZ, false);
map.key_event(KeyCode::KeyZ, true);
map.set_enabled("zoom_z", false);
map.set_enabled("zoom_z", true);
assert_eq!(
wheel(&mut map),
[0.0, 1.0, 0.0],
"Z pressed after Ctrl takes it back once enabled"
);
}
#[test]
fn a_scroll_chord_takes_the_wheel_while_its_key_is_down() {
let mut map = scroll_chords();
assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
map.key_event(KeyCode::KeyZ, true);
assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
assert!(map.button_held("z"), "Z is read, not consumed");
map.key_event(KeyCode::KeyZ, false);
assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "the wheel is back");
}
#[test]
fn the_most_recently_pressed_scroll_chord_key_takes_the_wheel() {
let mut map = scroll_chords();
map.key_event(KeyCode::ControlLeft, true);
map.key_event(KeyCode::KeyZ, true);
assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "Ctrl then Z: Z's");
map.key_event(KeyCode::ControlLeft, true); assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "a repeat is no press");
map.key_event(KeyCode::KeyZ, false);
assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "back to Ctrl");
map.key_event(KeyCode::ControlLeft, false);
map.key_event(KeyCode::KeyZ, true);
map.key_event(KeyCode::ControlLeft, true);
assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Z then Ctrl: Ctrl's");
map.key_event(KeyCode::ControlLeft, false);
assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "back to Z");
map.key_event(KeyCode::KeyZ, false);
assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
}
#[test]
fn a_scroll_chord_key_pressed_mid_tick_takes_that_ticks_scroll() {
let mut map = scroll_chords();
map.begin_tick(TICK);
map.mouse_scroll(0.0, 1.0);
map.key_event(KeyCode::KeyZ, true);
assert_eq!(["zoom", "zoom_z"].map(|name| map.axis1(name)), [0.0, 1.0]);
}
#[test]
fn a_scroll_chord_claims_the_wheel_for_its_context() {
let mut map = ActionMap::new();
map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
map.declare_in(
"map",
axis(
"pan",
ActionKind::Axis1,
vec![Binding::ScrollChord {
held: KeyCode::AltLeft,
}],
),
);
map.push_context("map").expect("declared");
map.begin_tick(TICK);
map.mouse_scroll(0.0, 1.0);
assert_eq!(map.axis1("zoom"), 0.0, "the pushed context owns the wheel");
assert_eq!(map.axis1("pan"), 0.0, "and Alt is not down");
map.key_event(KeyCode::AltLeft, true);
assert_eq!(map.axis1("pan"), 1.0);
}
#[test]
fn a_suppressed_scroll_chord_key_leaves_the_wheel_plain() {
let mut map = scroll_chords();
map.key_event(KeyCode::KeyZ, true);
map.suppress_held();
assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
map.key_event(KeyCode::KeyZ, false);
map.key_event(KeyCode::KeyZ, true);
assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
}
#[test]
fn a_scripted_sequence_yields_the_same_action_stream_twice() {
fn run() -> Vec<String> {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "move".to_owned(),
kind: ActionKind::Axis2,
bindings: vec![Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}],
});
map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
crate::ui::declare(&mut map).expect("no clash with move or jump");
map.set_repeat("jump", Some(Repeat::UI)).expect("declared");
let script: &[(u32, KeyCode, bool)] = &[
(0, KeyCode::KeyD, true),
(1, KeyCode::Space, true),
(3, KeyCode::KeyW, true),
(5, KeyCode::Tab, true),
(6, KeyCode::ShiftRight, true),
(9, KeyCode::Space, false),
(12, KeyCode::KeyD, false),
(14, KeyCode::Space, true),
(20, KeyCode::KeyW, false),
(40, KeyCode::Tab, false),
(41, KeyCode::ShiftRight, false),
(44, KeyCode::Space, false),
];
let mut stream = Vec::new();
for tick in 0..60 {
map.begin_tick(TICK);
match tick {
4 | 30 => map.push_context(crate::ui::CONTEXT).expect("off the stack"),
18 | 50 => map.pop_context(crate::ui::CONTEXT).expect("on top"),
_ => {}
}
for &(at, key, pressed) in script {
if at == tick {
map.key_event(key, pressed);
}
}
let repeats: Vec<bool> =
map.action_names().map(|name| map.repeated(name)).collect();
stream.push(format!(
"{tick} {:?} {repeats:?} {:?}",
InputTickState::capture(&map),
map.last_device(),
));
}
stream
}
let first = run();
assert_eq!(first, run());
assert!(
first.iter().any(|line| line.contains("just_pressed: true")),
"the script pressed something, or equality says nothing",
);
}
}