leafwing-input-manager 0.20.0

A powerful, flexible and ergonomic way to manage action-input keybindings for the Bevy game engine.
Documentation
#![cfg(feature = "keyboard")]

use bevy::ecs::system::SystemState;
use bevy::input::InputPlugin;
use bevy::platform::collections::HashSet;
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
use updating::CentralInputStore;

fn test_app() -> App {
    let mut app = App::new();

    app.add_plugins(MinimalPlugins)
        .add_plugins(InputPlugin)
        .add_plugins(InputManagerPlugin::<Action>::default())
        .add_systems(Startup, spawn_input_map);
    app
}

#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
enum Action {
    One,
    Two,
    OneAndTwo,
    TwoAndThree,
    OneAndTwoAndThree,
    CtrlOne,
    AltOne,
    CtrlAltOne,
}

impl Action {
    fn variants() -> &'static [Action] {
        &[
            Self::One,
            Self::Two,
            Self::OneAndTwo,
            Self::TwoAndThree,
            Self::OneAndTwoAndThree,
            Self::CtrlOne,
            Self::AltOne,
            Self::CtrlAltOne,
        ]
    }
}

fn spawn_input_map(mut commands: Commands) {
    use Action::*;
    use KeyCode::*;

    let mut input_map = InputMap::default();

    input_map.insert(One, Digit1);
    input_map.insert(Two, Digit2);
    input_map.insert(OneAndTwo, ButtonlikeChord::new([Digit1, Digit2]));
    input_map.insert(TwoAndThree, ButtonlikeChord::new([Digit2, Digit3]));
    input_map.insert(
        OneAndTwoAndThree,
        ButtonlikeChord::new([Digit1, Digit2, Digit3]),
    );
    input_map.insert(CtrlOne, ButtonlikeChord::new([ControlLeft, Digit1]));
    input_map.insert(AltOne, ButtonlikeChord::new([AltLeft, Digit1]));
    input_map.insert(
        CtrlAltOne,
        ButtonlikeChord::new([ControlLeft, AltLeft, Digit1]),
    );

    commands.spawn(input_map);
}

trait ClashTestExt {
    /// Asserts that the set of `pressed_actions` matches the actions observed
    /// by the entity with the corresponding variant of the [`ClashStrategy`] enum
    /// in its [`InputMap`] component
    fn assert_input_map_actions_eq(
        &mut self,
        clash_strategy: ClashStrategy,
        pressed_actions: impl IntoIterator<Item = Action>,
    );
}

impl ClashTestExt for App {
    fn assert_input_map_actions_eq(
        &mut self,
        clash_strategy: ClashStrategy,
        pressed_actions: impl IntoIterator<Item = Action>,
    ) {
        let pressed_actions: HashSet<Action> = HashSet::from_iter(pressed_actions);
        let mut input_system_state: SystemState<(
            Query<&InputMap<Action>>,
            Res<CentralInputStore>,
        )> = SystemState::new(self.world_mut());

        let (input_map_query, central_input_store) = input_system_state.get(self.world());

        let input_map = input_map_query.single().expect("No InputMap found");
        let keyboard_input = self.world().resource::<ButtonInput<KeyCode>>();

        for action in Action::variants() {
            if pressed_actions.contains(action) {
                assert!(
                    input_map.pressed(action, &central_input_store, clash_strategy),
                    "{action:?} was incorrectly not pressed for {clash_strategy:?} when `Input<KeyCode>` was \n {keyboard_input:?}."
                );
            } else {
                assert!(
                    !input_map.pressed(action, &central_input_store, clash_strategy),
                    "{action:?} was incorrectly pressed for {clash_strategy:?} when `Input<KeyCode>` was \n {keyboard_input:?}"
                );
            }
        }
    }
}

#[test]
fn two_inputs_clash_handling() {
    use Action::*;
    use KeyCode::*;

    let mut app = test_app();

    // Two inputs
    Digit1.press(app.world_mut());
    Digit2.press(app.world_mut());
    app.update();

    app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, Two, OneAndTwo]);
    app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwo]);
}

#[test]
fn three_inputs_clash_handling() {
    use Action::*;
    use KeyCode::*;

    let mut app = test_app();

    // Three inputs
    Digit1.press(app.world_mut());
    Digit2.press(app.world_mut());
    Digit3.press(app.world_mut());
    app.update();

    app.assert_input_map_actions_eq(
        ClashStrategy::PressAll,
        [One, Two, OneAndTwo, TwoAndThree, OneAndTwoAndThree],
    );
    app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwoAndThree]);
}

#[test]
fn modifier_clash_handling() {
    use Action::*;
    use KeyCode::*;

    let mut app = test_app();

    // Modifier
    Digit1.press(app.world_mut());
    Digit2.press(app.world_mut());
    Digit3.press(app.world_mut());
    ControlLeft.press(app.world_mut());

    app.update();

    app.assert_input_map_actions_eq(
        ClashStrategy::PressAll,
        [One, Two, OneAndTwo, TwoAndThree, OneAndTwoAndThree, CtrlOne],
    );
    app.assert_input_map_actions_eq(
        ClashStrategy::PrioritizeLongest,
        [CtrlOne, OneAndTwoAndThree],
    );
}

#[test]
fn multiple_modifiers_clash_handling() {
    use Action::*;
    use KeyCode::*;

    let mut app = test_app();

    // Multiple modifiers
    Digit1.press(app.world_mut());
    ControlLeft.press(app.world_mut());
    AltLeft.press(app.world_mut());
    app.update();

    app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, CtrlOne, AltOne, CtrlAltOne]);
    app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [CtrlAltOne]);
}

#[test]
fn action_order_clash_handling() {
    use Action::*;
    use KeyCode::*;

    let mut app = test_app();

    // Action order
    Digit3.press(app.world_mut());
    Digit2.press(app.world_mut());
    app.update();

    app.assert_input_map_actions_eq(ClashStrategy::PressAll, [Two, TwoAndThree]);
    app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [TwoAndThree]);
}