From 6d1d202f1c25df99d3b5195d366dcd6e8c4d2e90 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 24 Feb 2024 02:00:12 +0800 Subject: [PATCH 01/20] Reimplement keyboard inputs --- RELEASES.md | 2 +- src/lib.rs | 1 + src/user_inputs/axislike_settings.rs | 285 +++++++++++++++++++++++++++ src/user_inputs/chord_inputs.rs | 30 +++ src/user_inputs/gamepad_inputs.rs | 1 + src/user_inputs/keyboard_inputs.rs | 126 ++++++++++++ src/user_inputs/mod.rs | 73 +++++++ src/user_inputs/mouse_inputs.rs | 41 ++++ 8 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 src/user_inputs/axislike_settings.rs create mode 100644 src/user_inputs/chord_inputs.rs create mode 100644 src/user_inputs/gamepad_inputs.rs create mode 100644 src/user_inputs/keyboard_inputs.rs create mode 100644 src/user_inputs/mod.rs create mode 100644 src/user_inputs/mouse_inputs.rs diff --git a/RELEASES.md b/RELEASES.md index d7f2cccc..b3e714b8 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,7 +4,7 @@ ### Breaking Changes -- Removed `Direction` type. Use `bevy::math::primitives::Direction2d`. +- removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. ## Version 0.13.3 diff --git a/src/lib.rs b/src/lib.rs index 00b2954a..1abf4a6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ pub mod plugin; pub mod systems; pub mod timing; pub mod user_input; +pub mod user_inputs; // Importing the derive macro pub use leafwing_input_manager_macros::Actionlike; diff --git a/src/user_inputs/axislike_settings.rs b/src/user_inputs/axislike_settings.rs new file mode 100644 index 00000000..a5a0f5a2 --- /dev/null +++ b/src/user_inputs/axislike_settings.rs @@ -0,0 +1,285 @@ +//! Utilities for handling axis-like input settings. +//! +//! This module provides a set of tools for managing settings related to axis-like inputs, +//! commonly used in applications such as mouse motion, game controllers, and joysticks. +//! +//! # Deadzones +//! +//! Deadzones are regions around the center of the input range where no action is taken. +//! This helps to eliminate small fluctuations or inaccuracies in input devices, +//! providing smoother and more precise control. +//! +//! # Sensitivity +//! +//! Sensitivity adjustments allow users to fine-tune the responsiveness of input devices. +//! By scaling input values, sensitivity settings can affect the rate at which changes in input are reflected in the output. +//! +//! # Inversion +//! +//! Input inversion allows for changing the directionality of input devices. +//! For example, inverting the input axis of a joystick or mouse can reverse the direction of movement. + +use bevy::prelude::Reflect; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +/// Settings for single-axis inputs. +#[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] +pub struct SingleAxisSettings { + /// Sensitivity of the input. + sensitivity: f32, + + /// The [`SingleAxisDeadzone`] settings for the input. + deadzone: SingleAxisDeadzone, + + /// The inversion factor of the input. + /// + /// Using an `f32` here instead of a `bool` reduces performance cost, + /// directly indicating inversion status without conditionals. + /// + /// # Values + /// + /// - `1.0` means the input isn't inverted + /// - `-1.0` means the input is inverted + inversion_factor: f32, +} + +impl SingleAxisSettings { + /// The default [`SingleAxisSettings`]. + /// + /// - Sensitivity: `1.0` + /// - Deadzone: Default deadzone (excludes input values within the range [-0.1, 0.1]) + /// - Inversion: Not inverted + pub const DEFAULT: Self = Self { + sensitivity: 1.0, + deadzone: SingleAxisDeadzone::DEFAULT, + inversion_factor: 1.0, + }; + + /// The inverted [`SingleAxisSettings`] with default other settings. + /// + /// - Sensitivity: `1.0` + /// - Deadzone: Default deadzone (excludes input values within the range [-0.1, 0.1]) + /// - Inversion: Inverted + pub const DEFAULT_INVERTED: Self = Self { + sensitivity: 1.0, + deadzone: SingleAxisDeadzone::DEFAULT, + inversion_factor: -1.0, + }; + + /// The default [`SingleAxisSettings`] with a zero deadzone. + /// + /// - Sensitivity: `1.0` + /// - Deadzone: Zero deadzone (excludes only the zeroes) + /// - Inversion: Not inverted + pub const ZERO_DEADZONE: Self = Self { + sensitivity: 1.0, + deadzone: SingleAxisDeadzone::ZERO, + inversion_factor: 1.0, + }; + + /// The inverted [`SingleAxisSettings`] with a zero deadzone. + /// + /// - Sensitivity: `1.0` + /// - Deadzone: Zero deadzone (excludes only the zeroes) + /// - Inversion: Inverted + pub const ZERO_DEADZONE_INVERTED: Self = Self { + sensitivity: 1.0, + deadzone: SingleAxisDeadzone::ZERO, + inversion_factor: -1.0, + }; + + /// Creates a new [`SingleAxisSettings`] with the given settings. + pub const fn new(sensitivity: f32, deadzone: SingleAxisDeadzone) -> Self { + Self { + deadzone, + sensitivity, + inversion_factor: 1.0, + } + } + + /// Creates a new [`SingleAxisSettings`] with the given sensitivity. + pub const fn with_sensitivity(sensitivity: f32) -> Self { + Self::new(sensitivity, SingleAxisDeadzone::DEFAULT) + } + + /// Creates a new [`SingleAxisSettings`] with the given settings. + pub const fn with_deadzone(deadzone: SingleAxisDeadzone) -> Self { + Self::new(1.0, deadzone) + } + + /// Creates a new [`SingleAxisSettings`] with only negative values being filtered. + /// + /// # Arguments + /// + /// - `negative_low` - The lower limit for negative values. + pub fn with_negative_only(negative_low: f32) -> Self { + let deadzone = SingleAxisDeadzone::negative_only(negative_low); + Self::new(1.0, deadzone) + } + + /// Creates a new [`SingleAxisSettings`] with only positive values being filtered. + /// + /// # Arguments + /// + /// - `positive_low` - The lower limit for positive values. + pub fn with_positive_only(positive_low: f32) -> Self { + let deadzone = SingleAxisDeadzone::positive_only(positive_low); + Self::new(1.0, deadzone) + } + + /// Returns a new [`SingleAxisSettings`] with inversion applied. + pub fn with_inverted(self) -> SingleAxisSettings { + Self { + sensitivity: self.sensitivity, + deadzone: self.deadzone, + inversion_factor: -self.inversion_factor, + } + } + + /// Returns the input value after applying these settings. + pub fn apply_settings(&self, input_value: f32) -> f32 { + let deadzone_value = self.deadzone.apply_deadzone(input_value); + + deadzone_value * self.sensitivity * self.inversion_factor + } + + /// Returns the input value after applying these settings without the deadzone. + pub fn apply_settings_without_deadzone(&self, input_value: f32) -> f32 { + input_value * self.sensitivity * self.inversion_factor + } +} + +/// Deadzone settings for single-axis inputs. +#[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] +pub struct SingleAxisDeadzone { + /// The lower limit for negative values. + negative_low: f32, + + /// The lower limit for positive values. + positive_low: f32, + + /// The width of the deadzone around the negative axis. + /// + /// This value represents the absolute value of `negative_low`, + /// reducing the performance cost of using `abs` during value computation. + negative_low_width: f32, +} + +impl SingleAxisDeadzone { + /// The default deadzone with a small offset to filter out near-zero input values. + /// + /// This deadzone excludes input values within the range [-0.1, 0.1]. + pub const DEFAULT: Self = Self { + negative_low: -0.1, + positive_low: 0.1, + negative_low_width: 0.1, + }; + + /// The deadzone that only filters out the zeroes. + /// + /// This deadzone does not filter out near-zero negative or positive values. + pub const ZERO: Self = Self { + negative_low: 0.0, + positive_low: 0.0, + negative_low_width: 0.0, + }; + + /// Creates a new [`SingleAxisDeadzone`] with the given settings. + /// + /// # Arguments + /// + /// - `negative_low` - The lower limit for negative values, clamped to `0.0` if greater than `0.0`. + /// - `positive_low` - The lower limit for positive values, clamped to `0.0` if less than `0.0`. + pub fn new(negative_low: f32, positive_low: f32) -> Self { + Self { + negative_low: negative_low.min(0.0), + positive_low: positive_low.max(0.0), + negative_low_width: negative_low.abs(), + } + } + + /// Creates a new [`SingleAxisDeadzone`] with only negative values being filtered. + /// + /// # Arguments + /// + /// - `negative_low` - The lower limit for negative values, clamped to `0.0` if greater than `0.0`. + pub fn negative_only(negative_low: f32) -> Self { + Self { + negative_low: negative_low.min(0.0), + positive_low: f32::MAX, + negative_low_width: negative_low.abs(), + } + } + + /// Creates a new [`SingleAxisDeadzone`] with only positive values being filtered. + /// + /// # Arguments + /// + /// - `positive_low` - The lower limit for negative values, clamped to `0.0` if less than `0.0`. + pub fn positive_only(positive_low: f32) -> Self { + Self { + negative_low: f32::MIN, + positive_low: positive_low.max(0.0), + negative_low_width: f32::MAX, + } + } + + /// Returns the input value after applying the deadzone. + /// + /// This function calculates the deadzone width based on the input value and the deadzone settings. + /// If the input value falls within the deadzone range, it returns `0.0`. + /// Otherwise, it normalizes the input value into the range [-1.0, 1.0] by subtracting the deadzone width. + /// + /// # Panics + /// + /// Panic if both the negative and positive deadzone ranges are active, must never be reached. + /// + /// If this happens, you might be exploring the quantum realm! + /// Consider offering your computer a cup of coffee and politely asking for a less mysterious explanation. + pub fn apply_deadzone(&self, input_value: f32) -> f32 { + let is_negative_active = self.negative_low > input_value; + let is_positive_active = self.positive_low < input_value; + + let deadzone_width = match (is_negative_active, is_positive_active) { + // The input value is within the deadzone and falls back to `0.0` + (false, false) => return 0.0, + // The input value is outside the negative deadzone range + (true, false) => self.negative_low_width, + // The input value is outside the positive deadzone range + (false, true) => self.positive_low, + // This case must never be reached. + // Unless you've discovered the elusive quantum deadzone! + // Please check your quantum computer and contact the Rust Team. + (true, true) => unreachable!("Quantum deadzone detected!"), + }; + + // Normalize the input value into the range [-1.0, 1.0] + input_value.signum() * (input_value.abs() - deadzone_width) / (1.0 - deadzone_width) + } +} + +// Unfortunately, Rust doesn't let us automatically derive `Eq` and `Hash` for `f32`. +// It's like teaching a fish to ride a bike – a bit nonsensical! +// But if that fish really wants to pedal, we'll make it work. +// So here we are, showing Rust who's boss! + +impl Eq for SingleAxisSettings {} + +impl std::hash::Hash for SingleAxisSettings { + fn hash(&self, state: &mut H) { + FloatOrd(self.sensitivity).hash(state); + self.deadzone.hash(state); + FloatOrd(self.inversion_factor).hash(state); + } +} + +impl Eq for SingleAxisDeadzone {} + +impl std::hash::Hash for SingleAxisDeadzone { + fn hash(&self, state: &mut H) { + FloatOrd(self.negative_low).hash(state); + FloatOrd(self.positive_low).hash(state); + FloatOrd(self.negative_low_width).hash(state); + } +} diff --git a/src/user_inputs/chord_inputs.rs b/src/user_inputs/chord_inputs.rs new file mode 100644 index 00000000..96e3cd04 --- /dev/null +++ b/src/user_inputs/chord_inputs.rs @@ -0,0 +1,30 @@ +//! A combination of buttons, pressed simultaneously. + +use bevy::prelude::Reflect; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; + +use crate::user_inputs::UserInput; + +/// A combined input with two inner [`UserInput`]s. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize)] +pub struct Combined2Inputs<'a, U1, U2> +where + U1: UserInput<'a> + Deserialize<'a>, + U2: UserInput<'a> + Deserialize<'a>, +{ + inner: (U1, U2), + _phantom_data: PhantomData<(&'a U1, &'a U2)>, +} + +/// A combined input with three inner [`UserInput`]s. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize)] +pub struct Combined3Inputs<'a, U1, U2, U3> +where + U1: UserInput<'a> + Deserialize<'a>, + U2: UserInput<'a> + Deserialize<'a>, + U3: UserInput<'a> + Deserialize<'a>, +{ + inner: (U1, U2, U3), + _phantom_data: PhantomData<(&'a U1, &'a U2, &'a U3)>, +} diff --git a/src/user_inputs/gamepad_inputs.rs b/src/user_inputs/gamepad_inputs.rs new file mode 100644 index 00000000..13f6c8d6 --- /dev/null +++ b/src/user_inputs/gamepad_inputs.rs @@ -0,0 +1 @@ +//! Utilities for handling gamepad inputs. diff --git a/src/user_inputs/keyboard_inputs.rs b/src/user_inputs/keyboard_inputs.rs new file mode 100644 index 00000000..b9db04c0 --- /dev/null +++ b/src/user_inputs/keyboard_inputs.rs @@ -0,0 +1,126 @@ +//! Utilities for handling keyboard inputs. +//! +//! This module provides utilities for working with keyboard inputs in the applications. +//! It includes support for querying the state of individual keys, +//! as well as modifiers like Alt, Control, Shift, and Super (OS symbol key). +//! +//! # Usage +//! +//! The [`UserInput`] trait is implemented for [`KeyCode`], +//! allowing you to easily query the state of specific keys. +//! +//! Additionally, the [`ModifierKey`] enum represents keyboard modifiers +//! and provides methods for querying their state. + +use bevy::prelude::{KeyCode, Reflect}; +use serde::{Deserialize, Serialize}; + +use crate::input_streams::InputStreams; +use crate::user_inputs::UserInput; + +/// Built-in support for Bevy's [`KeyCode`]. +impl UserInput<'_> for KeyCode { + /// Retrieves the strength of the key press. + /// + /// # Returns + /// + /// - `None` if the keyboard input tracking is unavailable. + /// - `Some(0.0)` if this tracked key isn't pressed. + /// - `Some(1.0)` if this tracked key is currently pressed. + fn value(&self, input_query: InputStreams<'_>) -> Option { + let keycode_stream = input_query; + keycode_stream + .keycodes + .map(|keyboard| keyboard.pressed(*self)) + .map(f32::from) + } + + /// Checks if this tracked key is being pressed down during the current tick. + fn started(&self, input_query: InputStreams<'_>) -> bool { + let keycode_stream = input_query; + keycode_stream + .keycodes + .is_some_and(|keyboard| keyboard.just_pressed(*self)) + } + + /// Checks if this tracked key is being released during the current tick. + fn finished(&self, input_query: InputStreams<'_>) -> bool { + let keycode_stream = input_query; + keycode_stream + .keycodes + .is_some_and(|keyboard| keyboard.just_released(*self)) + } +} + +/// The keyboard modifier combining two [`KeyCode`] values into one representation. +/// +/// Each variant corresponds to a pair of [`KeyCode`] modifiers, +/// such as Alt, Control, Shift, or Super (OS symbol key), +/// one for the left and one for the right key, +/// indicating the modifier's activation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +pub enum ModifierKey { + /// The Alt key, corresponds to [`KeyCode::AltLeft`] and [`KeyCode::AltRight`]. + Alt, + /// The Control key, corresponds to [`KeyCode::ControlLeft`] and [`KeyCode::ControlRight`]. + Control, + /// The Shift key, corresponds to [`KeyCode::ShiftLeft`] and [`KeyCode::ShiftRight`]. + Shift, + /// The Super (OS symbol) key, corresponds to [`KeyCode::SuperLeft`] and [`KeyCode::SuperRight`]. + Super, +} + +impl ModifierKey { + /// Returns the [`KeyCode`] corresponding to the left key of the modifier. + pub const fn left(&self) -> KeyCode { + match self { + ModifierKey::Alt => KeyCode::AltLeft, + ModifierKey::Control => KeyCode::ControlLeft, + ModifierKey::Shift => KeyCode::ShiftLeft, + ModifierKey::Super => KeyCode::SuperLeft, + } + } + + /// Returns the [`KeyCode`] corresponding to the right key of the modifier. + pub const fn right(&self) -> KeyCode { + match self { + ModifierKey::Alt => KeyCode::AltRight, + ModifierKey::Control => KeyCode::ControlRight, + ModifierKey::Shift => KeyCode::ShiftRight, + ModifierKey::Super => KeyCode::SuperRight, + } + } +} + +impl UserInput<'_> for ModifierKey { + /// Retrieves the strength of the key press. + /// + /// # Returns + /// + /// - `None` if the keyboard input tracking is unavailable. + /// - `Some(0.0)` if these tracked keys aren't pressed. + /// - `Some(1.0)` if these tracked keys are currently pressed. + fn value(&self, input_query: InputStreams<'_>) -> Option { + let keycode_stream = input_query; + keycode_stream + .keycodes + .map(|keyboard| keyboard.pressed(self.left()) | keyboard.pressed(self.right())) + .map(f32::from) + } + + /// Checks if these tracked keys are being pressed down during the current tick. + fn started(&self, input_query: InputStreams<'_>) -> bool { + let keycode_stream = input_query; + keycode_stream.keycodes.is_some_and(|keyboard| { + keyboard.just_pressed(self.left()) | keyboard.just_pressed(self.right()) + }) + } + + /// Checks if these tracked keys are being released during the current tick. + fn finished(&self, input_query: InputStreams<'_>) -> bool { + let keycode_stream = input_query; + keycode_stream.keycodes.is_some_and(|keyboard| { + keyboard.just_released(self.left()) | keyboard.just_released(self.right()) + }) + } +} diff --git a/src/user_inputs/mod.rs b/src/user_inputs/mod.rs new file mode 100644 index 00000000..b547a4f5 --- /dev/null +++ b/src/user_inputs/mod.rs @@ -0,0 +1,73 @@ +//! Helpful abstractions over user inputs of all sorts. +//! +//! This module provides abstractions and utilities for defining and handling user inputs +//! across various input devices such as gamepads, keyboards, and mice. +//! It offers a unified interface for querying input values and states, +//! making it easier to manage and process user interactions within a Bevy application. +//! +//! # Traits +//! +//! - [`UserInput`]: A trait for defining a specific kind of user input. +//! It provides methods for checking if the input is active, +//! retrieving its current value, and detecting when it started or finished. +//! +//! # Modules +//! +//! ## General Input Settings +//! +//! - [`axislike_settings`]: Utilities for configuring axis-like input. +//! +//! ## General Inputs +//! +//! - [`gamepad_inputs`]: Utilities for handling gamepad inputs. +//! - [`keyboard_inputs`]: Utilities for handling keyboard inputs. +//! - [`mouse_inputs`]: Utilities for handling mouse inputs. +//! +//! ## Specific Inputs: +//! +//! - [`chord_inputs`]: A combination of buttons, pressed simultaneously. + +use bevy::prelude::{Reflect, Vec2}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::hash::Hash; + +use crate::input_streams::InputStreams; + +pub mod axislike_settings; + +pub mod gamepad_inputs; +pub mod keyboard_inputs; +pub mod mouse_inputs; + +pub mod chord_inputs; + +/// Allows defining a specific kind of user input. +pub trait UserInput<'a>: + Send + Sync + Debug + Clone + PartialEq + Eq + Hash + Reflect + Serialize + Deserialize<'a> +{ + /// Checks if this input is currently active. + fn is_active(&self, input_query: InputStreams<'a>) -> bool { + self.value(input_query).is_some_and(|value| value != 0.0) + } + + /// Retrieves the current value from input if available. + fn value(&self, _input_query: InputStreams<'a>) -> Option { + None + } + + /// Retrieves the current two-dimensional values from input if available. + fn pair_values(&self, _input_query: InputStreams<'a>) -> Option { + None + } + + /// Checks if this input is being active during the current tick. + fn started(&self, _input_query: InputStreams<'a>) -> bool { + false + } + + /// Checks if this input is being inactive during the current tick. + fn finished(&self, _input_query: InputStreams<'a>) -> bool { + false + } +} diff --git a/src/user_inputs/mouse_inputs.rs b/src/user_inputs/mouse_inputs.rs new file mode 100644 index 00000000..2fd8ace3 --- /dev/null +++ b/src/user_inputs/mouse_inputs.rs @@ -0,0 +1,41 @@ +//! Utilities for handling keyboard inputs. + +use bevy::prelude::Reflect; +use serde::{Deserialize, Serialize}; + +use crate::input_streams::InputStreams; +use crate::user_inputs::axislike_settings::SingleAxisSettings; +use crate::user_inputs::UserInput; + +/// Vertical mouse wheel input with settings. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +pub struct MouseWheelVertical(pub SingleAxisSettings); + +impl UserInput<'_> for MouseWheelVertical { + /// Retrieves the magnitude of vertical mouse wheel movements. + /// + /// Returns `None` if mouse input tracking is unavailable. + /// Returns `Some(0.0)` if the tracked mouse wheel isn't scrolling. + /// Returns `Some(magnitude)` of the tracked mouse wheel along the y-axis. + fn value(&self, input_query: InputStreams) -> Option { + let mouse_wheel_input = input_query.mouse_wheel; + mouse_wheel_input.map(|mouse_wheel| { + let movements = mouse_wheel.iter().map(|wheel| wheel.y).sum(); + self.0.apply_settings(movements) + }) + } + + /// Checks if the mouse wheel started scrolling vertically during the current tick. + fn started(&self, _input_query: InputStreams) -> bool { + // Unable to accurately determine this here; + // it should be checked during the update of the `ActionState`. + false + } + + /// Checks if the mouse wheel stopped scrolling vertically during the current tick. + fn finished(&self, _input_query: InputStreams) -> bool { + // Unable to accurately determine this here; + // it should be checked during the update of the `ActionState`. + false + } +} From 9c3697a964ab4099da3950cada07fbd0d9e35973 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 24 Feb 2024 02:17:03 +0800 Subject: [PATCH 02/20] Update document --- src/user_inputs/axislike_settings.rs | 92 ++++++++++++++-------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/user_inputs/axislike_settings.rs b/src/user_inputs/axislike_settings.rs index a5a0f5a2..ec786727 100644 --- a/src/user_inputs/axislike_settings.rs +++ b/src/user_inputs/axislike_settings.rs @@ -103,7 +103,7 @@ impl SingleAxisSettings { Self::new(sensitivity, SingleAxisDeadzone::DEFAULT) } - /// Creates a new [`SingleAxisSettings`] with the given settings. + /// Creates a new [`SingleAxisSettings`] with the given deadzone. pub const fn with_deadzone(deadzone: SingleAxisDeadzone) -> Self { Self::new(1.0, deadzone) } @@ -112,9 +112,9 @@ impl SingleAxisSettings { /// /// # Arguments /// - /// - `negative_low` - The lower limit for negative values. - pub fn with_negative_only(negative_low: f32) -> Self { - let deadzone = SingleAxisDeadzone::negative_only(negative_low); + /// - `negative_max` - The maximum limit for negative values. + pub fn with_negative_only(negative_max: f32) -> Self { + let deadzone = SingleAxisDeadzone::negative_only(negative_max); Self::new(1.0, deadzone) } @@ -122,9 +122,9 @@ impl SingleAxisSettings { /// /// # Arguments /// - /// - `positive_low` - The lower limit for positive values. - pub fn with_positive_only(positive_low: f32) -> Self { - let deadzone = SingleAxisDeadzone::positive_only(positive_low); + /// - `positive_min` - The minimum limit for positive values. + pub fn with_positive_only(positive_min: f32) -> Self { + let deadzone = SingleAxisDeadzone::positive_only(positive_min); Self::new(1.0, deadzone) } @@ -153,49 +153,49 @@ impl SingleAxisSettings { /// Deadzone settings for single-axis inputs. #[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] pub struct SingleAxisDeadzone { - /// The lower limit for negative values. - negative_low: f32, + /// The maximum limit for negative values. + negative_max: f32, - /// The lower limit for positive values. - positive_low: f32, + /// The minimum limit for positive values. + positive_min: f32, /// The width of the deadzone around the negative axis. /// - /// This value represents the absolute value of `negative_low`, + /// This value represents the absolute value of `negative_max`, /// reducing the performance cost of using `abs` during value computation. - negative_low_width: f32, + negative_deadzone_width: f32, } impl SingleAxisDeadzone { /// The default deadzone with a small offset to filter out near-zero input values. /// - /// This deadzone excludes input values within the range [-0.1, 0.1]. + /// This deadzone excludes input values within the range `[-0.1, 0.1]`. pub const DEFAULT: Self = Self { - negative_low: -0.1, - positive_low: 0.1, - negative_low_width: 0.1, + negative_max: -0.1, + positive_min: 0.1, + negative_deadzone_width: 0.1, }; /// The deadzone that only filters out the zeroes. /// - /// This deadzone does not filter out near-zero negative or positive values. + /// This deadzone doesn't filter out near-zero negative or positive values. pub const ZERO: Self = Self { - negative_low: 0.0, - positive_low: 0.0, - negative_low_width: 0.0, + negative_max: 0.0, + positive_min: 0.0, + negative_deadzone_width: 0.0, }; - /// Creates a new [`SingleAxisDeadzone`] with the given settings. + /// Creates a new [`SingleAxisDeadzone`] to filter out input values within the range `[negative_max, positive_min]`. /// /// # Arguments /// - /// - `negative_low` - The lower limit for negative values, clamped to `0.0` if greater than `0.0`. - /// - `positive_low` - The lower limit for positive values, clamped to `0.0` if less than `0.0`. - pub fn new(negative_low: f32, positive_low: f32) -> Self { + /// - `negative_max` - The maximum limit for negative values, clamped to `0.0` if greater than `0.0`. + /// - `positive_min` - The minimum limit for positive values, clamped to `0.0` if less than `0.0`. + pub fn new(negative_max: f32, positive_min: f32) -> Self { Self { - negative_low: negative_low.min(0.0), - positive_low: positive_low.max(0.0), - negative_low_width: negative_low.abs(), + negative_max: negative_max.min(0.0), + positive_min: positive_min.max(0.0), + negative_deadzone_width: negative_max.abs(), } } @@ -203,12 +203,12 @@ impl SingleAxisDeadzone { /// /// # Arguments /// - /// - `negative_low` - The lower limit for negative values, clamped to `0.0` if greater than `0.0`. - pub fn negative_only(negative_low: f32) -> Self { + /// - `negative_max` - The maximum limit for negative values, clamped to `0.0` if greater than `0.0`. + pub fn negative_only(negative_max: f32) -> Self { Self { - negative_low: negative_low.min(0.0), - positive_low: f32::MAX, - negative_low_width: negative_low.abs(), + negative_max: negative_max.min(0.0), + positive_min: f32::MAX, + negative_deadzone_width: negative_max.abs(), } } @@ -216,12 +216,12 @@ impl SingleAxisDeadzone { /// /// # Arguments /// - /// - `positive_low` - The lower limit for negative values, clamped to `0.0` if less than `0.0`. - pub fn positive_only(positive_low: f32) -> Self { + /// - `positive_min` - The minimum limit for negative values, clamped to `0.0` if less than `0.0`. + pub fn positive_only(positive_min: f32) -> Self { Self { - negative_low: f32::MIN, - positive_low: positive_low.max(0.0), - negative_low_width: f32::MAX, + negative_max: f32::MIN, + positive_min: positive_min.max(0.0), + negative_deadzone_width: f32::MAX, } } @@ -229,7 +229,7 @@ impl SingleAxisDeadzone { /// /// This function calculates the deadzone width based on the input value and the deadzone settings. /// If the input value falls within the deadzone range, it returns `0.0`. - /// Otherwise, it normalizes the input value into the range [-1.0, 1.0] by subtracting the deadzone width. + /// Otherwise, it normalizes the input value into the range `[-1.0, 1.0]` by subtracting the deadzone width. /// /// # Panics /// @@ -238,16 +238,16 @@ impl SingleAxisDeadzone { /// If this happens, you might be exploring the quantum realm! /// Consider offering your computer a cup of coffee and politely asking for a less mysterious explanation. pub fn apply_deadzone(&self, input_value: f32) -> f32 { - let is_negative_active = self.negative_low > input_value; - let is_positive_active = self.positive_low < input_value; + let is_negative_active = self.negative_max > input_value; + let is_positive_active = self.positive_min < input_value; let deadzone_width = match (is_negative_active, is_positive_active) { // The input value is within the deadzone and falls back to `0.0` (false, false) => return 0.0, // The input value is outside the negative deadzone range - (true, false) => self.negative_low_width, + (true, false) => self.negative_deadzone_width, // The input value is outside the positive deadzone range - (false, true) => self.positive_low, + (false, true) => self.positive_min, // This case must never be reached. // Unless you've discovered the elusive quantum deadzone! // Please check your quantum computer and contact the Rust Team. @@ -278,8 +278,8 @@ impl Eq for SingleAxisDeadzone {} impl std::hash::Hash for SingleAxisDeadzone { fn hash(&self, state: &mut H) { - FloatOrd(self.negative_low).hash(state); - FloatOrd(self.positive_low).hash(state); - FloatOrd(self.negative_low_width).hash(state); + FloatOrd(self.negative_max).hash(state); + FloatOrd(self.positive_min).hash(state); + FloatOrd(self.negative_deadzone_width).hash(state); } } From 94f6f9be7e7d6e2ab2c085c8f36724898a680757 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sun, 28 Apr 2024 00:36:15 +0800 Subject: [PATCH 03/20] Migrate to new input processors and refactor the trait --- Cargo.toml | 14 +- README.md | 4 +- RELEASES.md | 49 +- examples/action_state_resource.rs | 2 +- examples/arpg_indirection.rs | 6 +- examples/axis_inputs.rs | 4 +- examples/clash_handling.rs | 1 - examples/consuming_actions.rs | 1 - examples/default_controls.rs | 4 +- examples/input_processing.rs | 69 + examples/mouse_position.rs | 3 +- examples/multiplayer.rs | 12 +- examples/send_actions_over_network.rs | 6 +- examples/twin_stick_controller.rs | 2 +- examples/virtual_dpad.rs | 1 + macros/src/actionlike.rs | 26 +- macros/src/lib.rs | 16 +- macros/src/typetag.rs | 70 + macros/src/utils.rs | 26 + src/action_state.rs | 31 +- src/axislike.rs | 540 +++----- src/clashing_inputs.rs | 8 +- src/display_impl.rs | 5 +- src/input_map.rs | 9 +- src/input_processing/dual_axis/circle.rs | 536 ++++++++ src/input_processing/dual_axis/custom.rs | 349 +++++ src/input_processing/dual_axis/mod.rs | 426 ++++++ src/input_processing/dual_axis/range.rs | 1391 ++++++++++++++++++++ src/input_processing/mod.rs | 94 ++ src/input_processing/single_axis/custom.rs | 344 +++++ src/input_processing/single_axis/mod.rs | 230 ++++ src/input_processing/single_axis/range.rs | 679 ++++++++++ src/input_streams.rs | 95 +- src/lib.rs | 10 +- src/plugin.rs | 22 +- src/typetag.rs | 9 + src/user_input.rs | 67 +- src/user_inputs/axislike_settings.rs | 285 ---- src/user_inputs/chord.rs | 48 + src/user_inputs/chord_inputs.rs | 30 - src/user_inputs/gamepad.rs | 1 + src/user_inputs/gamepad_inputs.rs | 1 - src/user_inputs/keyboard.rs | 118 ++ src/user_inputs/keyboard_inputs.rs | 126 -- src/user_inputs/mod.rs | 265 +++- src/user_inputs/mouse.rs | 43 + src/user_inputs/mouse_inputs.rs | 41 - tests/gamepad_axis.rs | 142 +- tests/mouse_motion.rs | 61 +- tests/mouse_wheel.rs | 60 +- tests/multiple_gamepads.rs | 123 ++ 51 files changed, 5314 insertions(+), 1191 deletions(-) create mode 100644 examples/input_processing.rs create mode 100644 macros/src/typetag.rs create mode 100644 macros/src/utils.rs create mode 100644 src/input_processing/dual_axis/circle.rs create mode 100644 src/input_processing/dual_axis/custom.rs create mode 100644 src/input_processing/dual_axis/mod.rs create mode 100644 src/input_processing/dual_axis/range.rs create mode 100644 src/input_processing/mod.rs create mode 100644 src/input_processing/single_axis/custom.rs create mode 100644 src/input_processing/single_axis/mod.rs create mode 100644 src/input_processing/single_axis/range.rs create mode 100644 src/typetag.rs delete mode 100644 src/user_inputs/axislike_settings.rs create mode 100644 src/user_inputs/chord.rs delete mode 100644 src/user_inputs/chord_inputs.rs create mode 100644 src/user_inputs/gamepad.rs delete mode 100644 src/user_inputs/gamepad_inputs.rs create mode 100644 src/user_inputs/keyboard.rs delete mode 100644 src/user_inputs/keyboard_inputs.rs create mode 100644 src/user_inputs/mouse.rs delete mode 100644 src/user_inputs/mouse_inputs.rs create mode 100644 tests/multiple_gamepads.rs diff --git a/Cargo.toml b/Cargo.toml index 55a9d9cf..125befbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ opt-level = 3 members = ["./", "tools/ci", "macros"] [features] -default = ['asset', 'ui'] +default = ['asset', 'ui', 'bevy/bevy_gilrs'] # Allow using the `InputMap` as `bevy::asset::Asset`. asset = ['bevy/bevy_asset'] @@ -40,16 +40,20 @@ egui = ['dep:bevy_egui'] leafwing_input_manager_macros = { path = "macros", version = "0.13" } bevy = { version = "0.13", default-features = false, features = [ "serialize", - "bevy_gilrs", ] } -bevy_egui = { version = "0.25", optional = true } +bevy_egui = { version = "0.27", optional = true } derive_more = { version = "0.99", default-features = false, features = [ "display", "error", ] } itertools = "0.12" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } +serde_flexitos = "0.2" +dyn-clone = "1.0" +dyn-eq = "0.1" +dyn-hash = "0.2" +once_cell = "1.19" [dev-dependencies] bevy = { version = "0.13", default-features = false, features = [ @@ -61,7 +65,7 @@ bevy = { version = "0.13", default-features = false, features = [ "bevy_core_pipeline", "x11", ] } -bevy_egui = "0.25" +bevy_egui = "0.27" serde_test = "1.0" criterion = "0.5" diff --git a/README.md b/README.md index f7563427..5dcea207 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,9 @@ fn jump(query: Query<&ActionState, With>) { } ``` -This snippet is the `minimal.rs` example from the [`examples`](https://github.com/Leafwing-Studios/leafwing-input-manager/tree/latest/examples) folder: check there for more in-depth learning materials! +This snippet is the `minimal.rs` example from the [`examples`](https://github.com/Leafwing-Studios/leafwing-input-manager/tree/main/examples) folder: check there for more in-depth learning materials! ## Crate Feature Flags This crate has four feature flags: `asset`, `ui`, `no_ui_priority`, and `egui`. -Please refer to the `[features]` section in the [`Cargo.toml`](https://github.com/Leafwing-Studios/leafwing-input-manager/tree/latest/Cargo.toml) for detailed information about their configurations. +Please refer to the `[features]` section in the [`Cargo.toml`](https://github.com/Leafwing-Studios/leafwing-input-manager/tree/main/Cargo.toml) for detailed information about their configurations. diff --git a/RELEASES.md b/RELEASES.md index b3e714b8..35e3a0da 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,6 +5,51 @@ ### Breaking Changes - removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. +- added input processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad` to refine input values: + - added processor enums: + - `AxisProcessor`: Handles single-axis values. + - `DualAxisProcessor`: Handles dual-axis values. + - added processor traits for defining custom processors: + - `CustomAxisProcessor`: Handles single-axis values. + - `CustomDualAxisProcessor`: Handles dual-axis values. + - added built-in processor variants (no variant versions implemented `Into`): + - Pipelines: Handle input values sequentially through a sequence of processors. + - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. + - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. + - you can also create them by these methods: + - `AxisProcessor::with_processor` or `From>::from` for `AxisProcessor::Pipeline`. + - `DualAxisProcessor::with_processor` or `From>::from` for `DualAxisProcessor::Pipeline`. + - Inversion: Reverses control (positive becomes negative, etc.) + - `AxisProcessor::Inverted`: Single-axis inversion. + - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. + - Sensitivity: Adjusts control responsiveness (doubling, halving, etc.). + - `AxisProcessor::Sensitivity`: Single-axis scaling. + - `DualAxisSensitivity`: Dual-axis scaling, implemented `Into`. + - Value Bounds: Define the boundaries for constraining input values. + - `AxisBounds`: Restricts single-axis values to a range, implemented `Into` and `Into`. + - `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into`. + - `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into`. + - Deadzones: Ignores near-zero values, treating them as zero. + - Unscaled versions: + - `AxisExclusion`: Excludes small single-axis values, implemented `Into` and `Into`. + - `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into`. + - `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into`. + - Scaled versions: + - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. + - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. + - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. + - removed `DeadZoneShape`. + - removed functions for inverting, adjusting sensitivity, and creating deadzones from `SingleAxis` and `DualAxis`. + - added `with_processor`, `replace_processor`, and `no_processor` to manage processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad`. + - added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. + - added `serde_typetag` procedural macro attribute for processor type tagging. +- made the dependency on bevy's `bevy_gilrs` feature optional. + - it is still enabled by leafwing-input-manager's default features. + - if you're using leafwing-input-manager with `default_features = false`, you can readd it by adding `bevy/bevy_gilrs` as a dependency. + +### Bugs + +- fixed a bug in `InputStreams::button_pressed()` where unrelated gamepads were not filtered out when an `associated_gamepad` is defined. ## Version 0.13.3 @@ -81,7 +126,7 @@ - improved deadzone handling for both `DualAxis` and `SingleAxis` deadzones - all deadzones now scale the input so that it is continuous. - - `DeadZoneShape::Cross` handles each axis seperately, making a per-axis "snapping" effect + - `DeadZoneShape::Cross` handles each axis separately, making a per-axis "snapping" effect - an input that falls on the exact boundary of a deadzone is now considered inside it - added support in `ActionDiff` for value and axis_pair changes @@ -449,7 +494,7 @@ - `InputManagerPlugin` now works even if some input stream resources are missing - added the `input_pressed` method to `InputMap`, to check if a single input is pressed - renamed `InputMap::assign_gamepad` to `InputMap::set_gamepad` for consistency and clarity (it does not uniquely assign a gamepad) -- removed `strum` dependency by reimplementing the funcitonality, allowing users to define actions with only the `Actionlike` trait +- removed `strum` dependency by reimplementing the functionality, allowing users to define actions with only the `Actionlike` trait - added the `get_at` and `index` methods on the `Actionlike` trait, allowing you to fetch a specific action by its position in the defining enum and vice versa - `Copy` bound on `Actionlike` trait relaxed to `Clone`, allowing you to store non-copy data in your enum variants - `Clone`, `PartialEq` and `Debug` trait impls for `ActionState` diff --git a/examples/action_state_resource.rs b/examples/action_state_resource.rs index e8711c36..3f4c5633 100644 --- a/examples/action_state_resource.rs +++ b/examples/action_state_resource.rs @@ -5,7 +5,7 @@ //! and include it as a resource in a bevy app. use bevy::prelude::*; -use leafwing_input_manager::{prelude::*, user_input::InputKind}; +use leafwing_input_manager::prelude::*; fn main() { App::new() diff --git a/examples/arpg_indirection.rs b/examples/arpg_indirection.rs index 3b789a68..8651275c 100644 --- a/examples/arpg_indirection.rs +++ b/examples/arpg_indirection.rs @@ -112,19 +112,19 @@ fn spawn_player(mut commands: Commands) { fn copy_action_state( mut query: Query<( - &ActionState, + &mut ActionState, &mut ActionState, &AbilitySlotMap, )>, ) { - for (slot_state, mut ability_state, ability_slot_map) in query.iter_mut() { + for (mut slot_state, mut ability_state, ability_slot_map) in query.iter_mut() { for slot in Slot::variants() { if let Some(matching_ability) = ability_slot_map.get(&slot) { // This copies the `ActionData` between the ActionStates, // including information about how long the buttons have been pressed or released ability_state.set_action_data( *matching_ability, - slot_state.action_data(&slot).unwrap().clone(), + slot_state.action_data_mut_or_default(&slot).clone(), ); } } diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs index 6e778401..c175b0dd 100644 --- a/examples/axis_inputs.rs +++ b/examples/axis_inputs.rs @@ -33,9 +33,11 @@ fn spawn_player(mut commands: Commands) { .insert(Action::Throttle, GamepadButtonType::RightTrigger2) // And we'll use the right stick's x-axis as a rudder control .insert( + // Add an AxisDeadzone to process horizontal values of the right stick. // This will trigger if the axis is moved 10% or more in either direction. Action::Rudder, - SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), + SingleAxis::new(GamepadAxisType::RightStickX) + .with_processor(AxisDeadZone::magnitude(0.1)), ) .build(); commands diff --git a/examples/clash_handling.rs b/examples/clash_handling.rs index 07f0e8c0..58b10649 100644 --- a/examples/clash_handling.rs +++ b/examples/clash_handling.rs @@ -4,7 +4,6 @@ //! See [`ClashStrategy`] for more details. use bevy::prelude::*; -use leafwing_input_manager::clashing_inputs::ClashStrategy; use leafwing_input_manager::prelude::*; fn main() { diff --git a/examples/consuming_actions.rs b/examples/consuming_actions.rs index eddc07f0..457751d2 100644 --- a/examples/consuming_actions.rs +++ b/examples/consuming_actions.rs @@ -1,6 +1,5 @@ //! Demonstrates how to "consume" actions, so they can only be responded to by a single system -use bevy::ecs::system::Resource; use bevy::prelude::*; use leafwing_input_manager::prelude::*; diff --git a/examples/default_controls.rs b/examples/default_controls.rs index 0c71b205..e43e14e3 100644 --- a/examples/default_controls.rs +++ b/examples/default_controls.rs @@ -1,7 +1,7 @@ //! Demonstrates how to create default controls for an `Actionlike` and add it to an `InputMap` use bevy::prelude::*; -use leafwing_input_manager::{prelude::*, user_input::InputKind}; +use leafwing_input_manager::prelude::*; fn main() { App::new() @@ -20,7 +20,7 @@ enum PlayerAction { } impl PlayerAction { - /// Define the default binding to the input + /// Define the default bindings to the input fn default_input_map() -> InputMap { let mut input_map = InputMap::default(); diff --git a/examples/input_processing.rs b/examples/input_processing.rs new file mode 100644 index 00000000..d1c6407b --- /dev/null +++ b/examples/input_processing.rs @@ -0,0 +1,69 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(InputManagerPlugin::::default()) + .add_systems(Startup, spawn_player) + .add_systems(Update, check_data) + .run(); +} + +#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)] +enum Action { + Move, + LookAround, +} + +#[derive(Component)] +struct Player; + +fn spawn_player(mut commands: Commands) { + let mut input_map = InputMap::default(); + input_map + .insert( + Action::Move, + VirtualDPad::wasd() + // You can add a processor to handle axis-like user inputs by using the `with_processor`. + // + // This processor is a circular deadzone that normalizes input values + // by clamping their magnitude to a maximum of 1.0, + // excluding those with a magnitude less than 0.1, + // and scaling other values linearly in between. + .with_processor(CircleDeadZone::new(0.1)) + // Followed by appending Y-axis inversion for the next processing step. + .with_processor(DualAxisInverted::ONLY_Y), + ) + .insert( + Action::Move, + DualAxis::left_stick() + // You can replace the currently used processor with another processor. + .replace_processor(CircleDeadZone::default()) + // Or remove the processor directly, leaving no processor applied. + .no_processor(), + ) + .insert( + Action::LookAround, + // You can also use a sequence of processors as the processing pipeline. + DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from(vec![ + // The first processor is a circular deadzone. + CircleDeadZone::new(0.1).into(), + // The next processor doubles inputs normalized by the deadzone. + DualAxisSensitivity::all(2.0).into(), + ])), + ); + commands + .spawn(InputManagerBundle::with_map(input_map)) + .insert(Player); +} + +fn check_data(query: Query<&ActionState, With>) { + let action_state = query.single(); + for action in action_state.get_pressed() { + println!( + "Pressed {action:?}! Its data: {:?}", + action_state.axis_pair(&action) + ); + } +} diff --git a/examples/mouse_position.rs b/examples/mouse_position.rs index f910f7f9..7d79f1f0 100644 --- a/examples/mouse_position.rs +++ b/examples/mouse_position.rs @@ -60,8 +60,7 @@ fn update_cursor_state_from_window( if let Some(val) = window.cursor_position() { action_state - .action_data_mut(&driver.action) - .unwrap() + .action_data_mut_or_default(&driver.action) .axis_pair = Some(DualAxisData::from_xy(val)); } } diff --git a/examples/multiplayer.rs b/examples/multiplayer.rs index 1218ea8b..406c402f 100644 --- a/examples/multiplayer.rs +++ b/examples/multiplayer.rs @@ -6,6 +6,7 @@ fn main() { .add_plugins(DefaultPlugins) .add_plugins(InputManagerPlugin::::default()) .add_systems(Startup, spawn_players) + .add_systems(Update, move_players) .run(); } @@ -16,7 +17,7 @@ enum Action { Jump, } -#[derive(Component)] +#[derive(Component, Debug)] enum Player { One, Two, @@ -75,3 +76,12 @@ fn spawn_players(mut commands: Commands) { input_manager: InputManagerBundle::with_map(PlayerBundle::input_map(Player::Two)), }); } + +fn move_players(player_query: Query<(&Player, &ActionState)>) { + for (player, action_state) in player_query.iter() { + let actions = action_state.get_just_pressed(); + if !actions.is_empty() { + info!("Player {player:?} performed actions {actions:?}"); + } + } +} diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs index f111313f..4763a00e 100644 --- a/examples/send_actions_over_network.rs +++ b/examples/send_actions_over_network.rs @@ -6,7 +6,7 @@ //! Note that [`ActionState`] can also be serialized and sent directly. //! This approach will be less bandwidth efficient, but involve less complexity and CPU work. -use bevy::ecs::event::{Events, ManualEventReader}; +use bevy::ecs::event::ManualEventReader; use bevy::input::InputPlugin; use bevy::prelude::*; use leafwing_input_manager::action_diff::ActionDiffEvent; @@ -23,10 +23,6 @@ enum FpsAction { Shoot, } -/// This identifier uniquely identifies entities across the network -#[derive(Component, Clone, PartialEq, Eq, Hash, Debug)] -struct StableId(u64); - /// Processes an [`Events`] stream of [`ActionDiff`] to update an [`ActionState`] /// /// In a real scenario, you would have to map the entities between the server and client world. diff --git a/examples/twin_stick_controller.rs b/examples/twin_stick_controller.rs index e55c5cc2..dc45d2fa 100644 --- a/examples/twin_stick_controller.rs +++ b/examples/twin_stick_controller.rs @@ -39,7 +39,7 @@ pub enum PlayerAction { } impl PlayerAction { - /// Define the default binding to the input + /// Define the default bindings to the input fn default_input_map() -> InputMap { let mut input_map = InputMap::default(); diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs index a11c76ca..d30f9d20 100644 --- a/examples/virtual_dpad.rs +++ b/examples/virtual_dpad.rs @@ -32,6 +32,7 @@ fn spawn_player(mut commands: Commands) { down: KeyCode::KeyS.into(), left: KeyCode::KeyA.into(), right: KeyCode::KeyD.into(), + processor: DualAxisProcessor::None, }, )]); commands diff --git a/macros/src/actionlike.rs b/macros/src/actionlike.rs index 6adcc79a..6bf73cb2 100644 --- a/macros/src/actionlike.rs +++ b/macros/src/actionlike.rs @@ -1,8 +1,7 @@ -use proc_macro2::Span; +use crate::utils; use proc_macro2::TokenStream; -use proc_macro_crate::{crate_name, FoundCrate}; use quote::quote; -use syn::{DeriveInput, Ident}; +use syn::DeriveInput; /// This approach and implementation is inspired by the `strum` crate, /// Copyright (c) 2019 Peter Glotfelty @@ -13,26 +12,7 @@ pub(crate) fn actionlike_inner(ast: &DeriveInput) -> TokenStream { let enum_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); - let crate_path = if let Ok(found_crate) = crate_name("leafwing_input_manager") { - // The crate was found in the Cargo.toml - match found_crate { - FoundCrate::Itself => quote!(leafwing_input_manager), - FoundCrate::Name(name) => { - let ident = Ident::new(&name, Span::call_site()); - quote!(#ident) - } - } - } else { - // The crate was not found in the Cargo.toml, - // so we assume that we are in the owning_crate itself - // - // In order for this to play nicely with unit tests within the crate itself, - // `use crate as leafwing_input_manager` at the top of each test module - // - // Note that doc tests, integration tests and examples want the full standard import, - // as they are evaluated as if they were external - quote!(leafwing_input_manager) - }; + let crate_path = utils::crate_path(); quote! { impl #impl_generics #crate_path::Actionlike for #enum_name #type_generics #where_clause {} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 2a26b989..39dff713 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -5,9 +5,14 @@ //! Copyright (c) 2019 Peter Glotfelty under the MIT License extern crate proc_macro; -mod actionlike; + use proc_macro::TokenStream; -use syn::DeriveInput; +use syn::{DeriveInput, ItemImpl}; + +mod actionlike; +mod typetag; + +mod utils; #[proc_macro_derive(Actionlike)] pub fn actionlike(input: TokenStream) -> TokenStream { @@ -15,3 +20,10 @@ pub fn actionlike(input: TokenStream) -> TokenStream { crate::actionlike::actionlike_inner(&ast).into() } + +#[proc_macro_attribute] +pub fn serde_typetag(_: TokenStream, input: TokenStream) -> TokenStream { + let ast = syn::parse_macro_input!(input as ItemImpl); + + crate::typetag::expand_serde_typetag(&ast).into() +} diff --git a/macros/src/typetag.rs b/macros/src/typetag.rs new file mode 100644 index 00000000..c495f662 --- /dev/null +++ b/macros/src/typetag.rs @@ -0,0 +1,70 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Error, ItemImpl, Type, TypePath}; + +use crate::utils; + +/// This approach and implementation is inspired by the `typetag` crate, +/// Copyright (c) 2019 David Tolnay +/// available under either of `Apache License, Version 2.0` or `MIT` license +/// at +pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> TokenStream { + let Some(trait_) = &input.trait_ else { + let impl_token = input.impl_token; + let ty = &input.self_ty; + let span = quote!(#impl_token, #ty); + let msg = "expected impl Trait for Type"; + return Error::new_spanned(span, msg).to_compile_error(); + }; + + let trait_path = &trait_.1; + + let where_clause = &input.generics.where_clause; + let generics_params = &input.generics.params; + + let self_ty = &input.self_ty; + + let ident = match type_name(self_ty) { + Some(name) => quote!(#name), + None => { + let impl_token = input.impl_token; + let span = quote!(#impl_token, #self_ty); + let msg = "expected explicit name for Type"; + return Error::new_spanned(span, msg).to_compile_error(); + } + }; + + let crate_path = utils::crate_path(); + + quote! { + #input + + impl<'de, #generics_params> #crate_path::typetag::RegisterTypeTag<'de, dyn #trait_path> for #self_ty #where_clause { + fn register_typetag( + registry: &mut #crate_path::typetag::MapRegistry, + ) { + #crate_path::typetag::Registry::register( + registry, + #ident, + |de| Ok(::std::boxed::Box::new( + ::bevy::reflect::erased_serde::deserialize::<#self_ty>(de)?, + )), + ) + } + } + } +} + +fn type_name(mut ty: &Type) -> Option { + loop { + match ty { + Type::Group(group) => { + ty = &group.elem; + } + Type::Path(TypePath { qself, path }) if qself.is_none() => { + return Some(path.segments.last().unwrap().ident.to_string()) + } + _ => return None, + } + } +} diff --git a/macros/src/utils.rs b/macros/src/utils.rs new file mode 100644 index 00000000..61963708 --- /dev/null +++ b/macros/src/utils.rs @@ -0,0 +1,26 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; + +pub fn crate_path() -> TokenStream { + if let Ok(found_crate) = crate_name("leafwing_input_manager") { + // The crate was found in the Cargo.toml + match found_crate { + FoundCrate::Itself => quote!(leafwing_input_manager), + FoundCrate::Name(name) => { + let ident = Ident::new(&name, Span::call_site()); + quote!(#ident) + } + } + } else { + // The crate was not found in the Cargo.toml, + // so we assume that we are in the owning_crate itself + // + // In order for this to play nicely with unit tests within the crate itself, + // `use crate as leafwing_input_manager` at the top of each test module + // + // Note that doc tests, integration tests and examples want the full standard import, + // as they are evaluated as if they were external + quote!(leafwing_input_manager) + } +} diff --git a/src/action_state.rs b/src/action_state.rs index d277be85..e07153ab 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -184,20 +184,43 @@ impl ActionState { }); } - /// A reference of the [`ActionData`] corresponding to the `action` if populated. + /// A reference of the [`ActionData`] corresponding to the `action` if triggered. /// /// Generally, it'll be clearer to call `pressed` or so on directly on the [`ActionState`]. /// However, accessing the raw data directly allows you to examine detailed metadata holistically. + /// + /// # Caution + /// + /// To access the [`ActionData`] regardless of whether the `action` has been triggered, + /// use [`unwrap_or_default`](Option::unwrap_or_default) on the returned [`Option`]. + /// + /// # Returns + /// + /// - `Some(ActionData)` if it exists. + /// - `None` if the `action` has never been triggered (pressed, clicked, etc.). #[inline] #[must_use] pub fn action_data(&self, action: &A) -> Option<&ActionData> { self.action_data.get(action) } - /// A mutable reference of the [`ActionData`] corresponding to the `action` if populated. + /// A mutable reference of the [`ActionData`] corresponding to the `action` if triggered. /// /// Generally, it'll be clearer to call `pressed` or so on directly on the [`ActionState`]. /// However, accessing the raw data directly allows you to examine detailed metadata holistically. + /// + /// # Caution + /// + /// - To access the [`ActionData`] regardless of whether the `action` has been triggered, + /// use [`unwrap_or_default`](Option::unwrap_or_default) on the returned [`Option`]. + /// + /// - To insert a default [`ActionData`] if it doesn't exist, + /// use [`action_data_mut_or_default`](Self::action_data_mut_or_default) method. + /// + /// # Returns + /// + /// - `Some(ActionData)` if it exists. + /// - `None` if the `action` has never been triggered (pressed, clicked, etc.). #[inline] #[must_use] pub fn action_data_mut(&mut self, action: &A) -> Option<&mut ActionData> { @@ -206,6 +229,10 @@ impl ActionState { /// A mutable reference of the [`ActionData`] corresponding to the `action`. /// + /// If the `action` has no data yet (because the `action` has not been triggered), + /// this method will create and insert a default [`ActionData`] for you, + /// avoiding potential errors from unwrapping [`None`]. + /// /// Generally, it'll be clearer to call `pressed` or so on directly on the [`ActionState`]. /// However, accessing the raw data directly allows you to examine detailed metadata holistically. #[inline] diff --git a/src/axislike.rs b/src/axislike.rs index 5fca15c3..2d9d8f07 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -1,41 +1,24 @@ //! Tools for working with directional axis-like user inputs (game sticks, D-Pads and emulated equivalents) +use bevy::prelude::{Direction2d, GamepadAxisType, GamepadButtonType, KeyCode, Reflect, Vec2}; +use serde::{Deserialize, Serialize}; + use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; +use crate::input_processing::*; use crate::orientation::Rotation; use crate::user_input::InputKind; -use bevy::input::{ - gamepad::{GamepadAxisType, GamepadButtonType}, - keyboard::KeyCode, -}; -use bevy::math::primitives::Direction2d; -use bevy::math::Vec2; -use bevy::reflect::Reflect; -use bevy::utils::FloatOrd; -use serde::{Deserialize, Serialize}; /// A single directional axis with a configurable trigger zone. /// /// These can be stored in a [`InputKind`] to create a virtual button. -/// -/// # Warning -/// -/// `positive_low` must be greater than or equal to `negative_low` for this type to be validly constructed. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)] +#[derive(Debug, Clone, Serialize, Deserialize, Reflect)] pub struct SingleAxis { /// The axis that is being checked. pub axis_type: AxisType, - /// Any axis value higher than this will trigger the input. - pub positive_low: f32, - /// Any axis value lower than this will trigger the input. - pub negative_low: f32, - /// Whether to invert output values from this axis. - pub inverted: bool, - /// How sensitive the axis is to input values. - /// - /// Since sensitivity is a multiplier, any value `>1.0` will increase sensitivity while any value `<1.0` will decrease sensitivity. - /// This value should always be strictly positive: a value of 0 will cause the axis to stop functioning, - /// while negative values will invert the direction. - pub sensitivity: f32, + + /// The processor used to handle input values. + pub processor: AxisProcessor, + /// The target value for this input, used for input mocking. /// /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) @@ -43,116 +26,102 @@ pub struct SingleAxis { } impl SingleAxis { - /// Creates a [`SingleAxis`] with the given `positive_low` and `negative_low` thresholds. + /// Creates a [`SingleAxis`] with the specified axis type. #[must_use] - pub const fn with_threshold( - axis_type: AxisType, - negative_low: f32, - positive_low: f32, - ) -> SingleAxis { - SingleAxis { - axis_type, - positive_low, - negative_low, - inverted: false, - sensitivity: 1.0, + pub fn new(axis_type: impl Into) -> Self { + Self { + axis_type: axis_type.into(), + processor: AxisProcessor::None, value: None, } } - /// Creates a [`SingleAxis`] with both `positive_low` and `negative_low` set to `threshold`. - #[must_use] - pub fn symmetric(axis_type: impl Into, threshold: f32) -> SingleAxis { - Self::with_threshold(axis_type.into(), -threshold, threshold) - } - - /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`. + /// Creates a [`SingleAxis`] with the specified axis type and `value`. /// - /// All thresholds are set to 0.0. /// Primarily useful for [input mocking](crate::input_mocking). #[must_use] - pub fn from_value(axis_type: impl Into, value: f32) -> SingleAxis { - SingleAxis { + pub fn from_value(axis_type: impl Into, value: f32) -> Self { + Self { axis_type: axis_type.into(), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, value: Some(value), } } /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseWheel`](bevy::input::mouse::MouseWheel) movement #[must_use] - pub const fn mouse_wheel_x() -> SingleAxis { - let axis_type = AxisType::MouseWheel(MouseWheelAxisType::X); - SingleAxis::with_threshold(axis_type, 0.0, 0.0) + pub const fn mouse_wheel_x() -> Self { + Self { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + processor: AxisProcessor::None, + value: None, + } } /// Creates a [`SingleAxis`] corresponding to vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement #[must_use] - pub const fn mouse_wheel_y() -> SingleAxis { - let axis_type = AxisType::MouseWheel(MouseWheelAxisType::Y); - SingleAxis::with_threshold(axis_type, 0.0, 0.0) + pub const fn mouse_wheel_y() -> Self { + Self { + axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + processor: AxisProcessor::None, + value: None, + } } /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseMotion`](bevy::input::mouse::MouseMotion) movement #[must_use] - pub const fn mouse_motion_x() -> SingleAxis { - let axis_type = AxisType::MouseMotion(MouseMotionAxisType::X); - SingleAxis::with_threshold(axis_type, 0.0, 0.0) + pub const fn mouse_motion_x() -> Self { + Self { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + processor: AxisProcessor::None, + value: None, + } } /// Creates a [`SingleAxis`] corresponding to vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement #[must_use] - pub const fn mouse_motion_y() -> SingleAxis { - let axis_type = AxisType::MouseMotion(MouseMotionAxisType::Y); - SingleAxis::with_threshold(axis_type, 0.0, 0.0) - } - - /// Creates a [`SingleAxis`] with the `axis_type` and `negative_low` set to `threshold`. - /// - /// Positive values will not trigger the input. - pub fn negative_only(axis_type: impl Into, threshold: f32) -> SingleAxis { - SingleAxis::with_threshold(axis_type.into(), threshold, f32::MAX) + pub const fn mouse_motion_y() -> Self { + Self { + axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + processor: AxisProcessor::None, + value: None, + } } - /// Creates a [`SingleAxis`] with the `axis_type` and `positive_low` set to `threshold`. - /// - /// Negative values will not trigger the input. - pub fn positive_only(axis_type: impl Into, threshold: f32) -> SingleAxis { - SingleAxis::with_threshold(axis_type.into(), f32::MIN, threshold) + /// Appends the given [`AxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self } - /// Returns this [`SingleAxis`] with the deadzone set to the specified value - #[must_use] - pub fn with_deadzone(mut self, deadzone: f32) -> SingleAxis { - self.negative_low = -deadzone; - self.positive_low = deadzone; + /// Replaces the current [`AxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); self } - /// Returns this [`SingleAxis`] with the sensitivity set to the specified value - #[must_use] - pub fn with_sensitivity(mut self, sensitivity: f32) -> SingleAxis { - self.sensitivity = sensitivity; + /// Remove the current used [`AxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = AxisProcessor::None; self } - /// Returns this [`SingleAxis`] inverted. + /// Get the "value" of this axis. + /// If a processor is set, it will compute and return the processed value. + /// Otherwise, pass the `input_value` through unchanged. #[must_use] - pub fn inverted(mut self) -> Self { - self.inverted = !self.inverted; - self + #[inline] + pub fn input_value(&self, input_value: f32) -> f32 { + self.processor.process(input_value) } } impl PartialEq for SingleAxis { fn eq(&self, other: &Self) -> bool { - self.axis_type == other.axis_type - && FloatOrd(self.positive_low) == FloatOrd(other.positive_low) - && FloatOrd(self.negative_low) == FloatOrd(other.negative_low) - && FloatOrd(self.sensitivity) == FloatOrd(other.sensitivity) + self.axis_type == other.axis_type && self.processor == other.processor } } @@ -161,9 +130,7 @@ impl Eq for SingleAxis {} impl std::hash::Hash for SingleAxis { fn hash(&self, state: &mut H) { self.axis_type.hash(state); - FloatOrd(self.positive_low).hash(state); - FloatOrd(self.negative_low).hash(state); - FloatOrd(self.sensitivity).hash(state); + self.processor.hash(state); } } @@ -173,59 +140,37 @@ impl std::hash::Hash for SingleAxis { /// /// This input will generate a [`DualAxis`] which can be read with /// [`ActionState::axis_pair`][crate::action_state::ActionState::axis_pair]. -/// -/// # Warning -/// -/// `positive_low` must be greater than or equal to `negative_low` for both `x` and `y` for this type to be validly constructed. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Reflect)] +#[derive(Debug, Clone, Serialize, Deserialize, Reflect)] pub struct DualAxis { - /// The axis representing horizontal movement. - pub x: SingleAxis, - /// The axis representing vertical movement. - pub y: SingleAxis, - /// The shape of the deadzone - pub deadzone: DeadZoneShape, -} + /// The horizontal axis that is being checked. + pub x_axis_type: AxisType, -impl DualAxis { - /// The default size of the deadzone used by constructor methods. - /// - /// This cannot be changed, but the struct can be easily manually constructed. - pub const DEFAULT_DEADZONE: f32 = 0.1; + /// The vertical axis that is being checked. + pub y_axis_type: AxisType, - /// The default shape of the deadzone used by constructor methods. - /// - /// This cannot be changed, but the struct can be easily manually constructed. - pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Ellipse { - radius_x: Self::DEFAULT_DEADZONE, - radius_y: Self::DEFAULT_DEADZONE, - }; + /// The processor used to handle input values. + pub processor: DualAxisProcessor, - /// A deadzone with a size of 0.0 used by constructor methods. + /// The target value for this input, used for input mocking. /// - /// This cannot be changed, but the struct can be easily manually constructed. - pub const ZERO_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Ellipse { - radius_x: 0.0, - radius_y: 0.0, - }; + /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) + pub value: Option, +} - /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold` with a `deadzone_shape`. +impl DualAxis { + /// Creates a [`DualAxis`] with the specified axis types. #[must_use] - pub fn symmetric( - x_axis_type: impl Into, - y_axis_type: impl Into, - deadzone_shape: DeadZoneShape, - ) -> DualAxis { - DualAxis { - x: SingleAxis::symmetric(x_axis_type, 0.0), - y: SingleAxis::symmetric(y_axis_type, 0.0), - deadzone: deadzone_shape, + pub fn new(x_axis_type: impl Into, y_axis_type: impl Into) -> Self { + Self { + x_axis_type: x_axis_type.into(), + y_axis_type: y_axis_type.into(), + processor: DualAxisProcessor::None, + value: None, } } - /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`. + /// Creates a [`DualAxis`] with the specified axis types and `value`. /// - /// All thresholds are set to 0.0. /// Primarily useful for [input mocking](crate::input_mocking). #[must_use] pub fn from_value( @@ -233,91 +178,103 @@ impl DualAxis { y_axis_type: impl Into, x_value: f32, y_value: f32, - ) -> DualAxis { - DualAxis { - x: SingleAxis::from_value(x_axis_type, x_value), - y: SingleAxis::from_value(y_axis_type, y_value), - deadzone: Self::DEFAULT_DEADZONE_SHAPE, + ) -> Self { + Self { + x_axis_type: x_axis_type.into(), + y_axis_type: y_axis_type.into(), + processor: DualAxisProcessor::None, + value: Some(Vec2::new(x_value, y_value)), } } /// Creates a [`DualAxis`] for the left analogue stick of the gamepad. #[must_use] - pub const fn left_stick() -> DualAxis { - let axis_x = AxisType::Gamepad(GamepadAxisType::LeftStickX); - let axis_y = AxisType::Gamepad(GamepadAxisType::LeftStickY); - DualAxis { - x: SingleAxis::with_threshold(axis_x, 0.0, 0.0), - y: SingleAxis::with_threshold(axis_y, 0.0, 0.0), - deadzone: Self::DEFAULT_DEADZONE_SHAPE, + pub fn left_stick() -> Self { + Self { + x_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + y_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + processor: CircleDeadZone::default().into(), + value: None, } } /// Creates a [`DualAxis`] for the right analogue stick of the gamepad. #[must_use] - pub const fn right_stick() -> DualAxis { - let axis_x = AxisType::Gamepad(GamepadAxisType::RightStickX); - let axis_y = AxisType::Gamepad(GamepadAxisType::RightStickY); - DualAxis { - x: SingleAxis::with_threshold(axis_x, 0.0, 0.0), - y: SingleAxis::with_threshold(axis_y, 0.0, 0.0), - deadzone: Self::DEFAULT_DEADZONE_SHAPE, + pub fn right_stick() -> Self { + Self { + x_axis_type: AxisType::Gamepad(GamepadAxisType::RightStickX), + y_axis_type: AxisType::Gamepad(GamepadAxisType::RightStickY), + processor: CircleDeadZone::default().into(), + value: None, } } /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement - pub const fn mouse_wheel() -> DualAxis { - DualAxis { - x: SingleAxis::mouse_wheel_x(), - y: SingleAxis::mouse_wheel_y(), - deadzone: Self::ZERO_DEADZONE_SHAPE, + pub const fn mouse_wheel() -> Self { + Self { + x_axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + y_axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + processor: DualAxisProcessor::None, + value: None, } } /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement - pub const fn mouse_motion() -> DualAxis { - DualAxis { - x: SingleAxis::mouse_motion_x(), - y: SingleAxis::mouse_motion_y(), - deadzone: Self::ZERO_DEADZONE_SHAPE, + pub const fn mouse_motion() -> Self { + Self { + x_axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + y_axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + processor: DualAxisProcessor::None, + value: None, } } - /// Returns this [`DualAxis`] with the deadzone set to the specified values and shape - #[must_use] - pub fn with_deadzone(mut self, deadzone: DeadZoneShape) -> DualAxis { - self.deadzone = deadzone; + /// Appends the given [`DualAxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); self } - /// Returns this [`DualAxis`] with the sensitivity set to the specified values - #[must_use] - pub fn with_sensitivity(mut self, x_sensitivity: f32, y_sensitivity: f32) -> DualAxis { - self.x.sensitivity = x_sensitivity; - self.y.sensitivity = y_sensitivity; + /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); self } - /// Returns this [`DualAxis`] with an inverted X-axis. - #[must_use] - pub fn inverted_x(mut self) -> DualAxis { - self.x = self.x.inverted(); + /// Remove the current used [`DualAxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = DualAxisProcessor::None; self } - /// Returns this [`DualAxis`] with an inverted Y-axis. + /// Get the "value" of these axes. + /// If a processor is set, it will compute and return the processed value. + /// Otherwise, pass the `input_value` through unchanged. #[must_use] - pub fn inverted_y(mut self) -> DualAxis { - self.y = self.y.inverted(); - self + #[inline] + pub fn input_value(&self, input_value: Vec2) -> Vec2 { + self.processor.process(input_value) } +} - /// Returns this [`DualAxis`] with both axes inverted. - #[must_use] - pub fn inverted(mut self) -> DualAxis { - self.x = self.x.inverted(); - self.y = self.y.inverted(); - self +impl PartialEq for DualAxis { + fn eq(&self, other: &Self) -> bool { + self.x_axis_type == other.x_axis_type + && self.y_axis_type == other.y_axis_type + && self.processor == other.processor + } +} + +impl Eq for DualAxis {} + +impl std::hash::Hash for DualAxis { + fn hash(&self, state: &mut H) { + self.x_axis_type.hash(state); + self.y_axis_type.hash(state); + self.processor.hash(state); } } @@ -338,16 +295,19 @@ pub struct VirtualDPad { pub left: InputKind, /// The input that represents the right direction in this virtual DPad pub right: InputKind, + /// The processor used to handle input values. + pub processor: DualAxisProcessor, } impl VirtualDPad { /// Generates a [`VirtualDPad`] corresponding to the arrow keyboard keycodes - pub const fn arrow_keys() -> VirtualDPad { + pub fn arrow_keys() -> VirtualDPad { VirtualDPad { up: InputKind::PhysicalKey(KeyCode::ArrowUp), down: InputKind::PhysicalKey(KeyCode::ArrowDown), left: InputKind::PhysicalKey(KeyCode::ArrowLeft), right: InputKind::PhysicalKey(KeyCode::ArrowRight), + processor: CircleDeadZone::default().into(), } } @@ -357,23 +317,25 @@ impl VirtualDPad { /// The _location_ of the keys is the same on all keyboard layouts. /// This ensures that the classic triangular shape is retained on all layouts, /// which enables comfortable movement controls. - pub const fn wasd() -> VirtualDPad { + pub fn wasd() -> VirtualDPad { VirtualDPad { up: InputKind::PhysicalKey(KeyCode::KeyW), down: InputKind::PhysicalKey(KeyCode::KeyS), left: InputKind::PhysicalKey(KeyCode::KeyA), right: InputKind::PhysicalKey(KeyCode::KeyD), + processor: CircleDeadZone::default().into(), } } #[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an unquoted item /// Generates a [`VirtualDPad`] corresponding to the DPad on a gamepad - pub const fn dpad() -> VirtualDPad { + pub fn dpad() -> VirtualDPad { VirtualDPad { up: InputKind::GamepadButton(GamepadButtonType::DPadUp), down: InputKind::GamepadButton(GamepadButtonType::DPadDown), left: InputKind::GamepadButton(GamepadButtonType::DPadLeft), right: InputKind::GamepadButton(GamepadButtonType::DPadRight), + processor: CircleDeadZone::default().into(), } } @@ -381,53 +343,67 @@ impl VirtualDPad { /// /// North corresponds to up, west corresponds to left, /// east corresponds to right, and south corresponds to down - pub const fn gamepad_face_buttons() -> VirtualDPad { + pub fn gamepad_face_buttons() -> VirtualDPad { VirtualDPad { up: InputKind::GamepadButton(GamepadButtonType::North), down: InputKind::GamepadButton(GamepadButtonType::South), left: InputKind::GamepadButton(GamepadButtonType::West), right: InputKind::GamepadButton(GamepadButtonType::East), + processor: CircleDeadZone::default().into(), } } /// Generates a [`VirtualDPad`] corresponding to discretized mousewheel movements - pub const fn mouse_wheel() -> VirtualDPad { + pub fn mouse_wheel() -> VirtualDPad { VirtualDPad { up: InputKind::MouseWheel(MouseWheelDirection::Up), down: InputKind::MouseWheel(MouseWheelDirection::Down), left: InputKind::MouseWheel(MouseWheelDirection::Left), right: InputKind::MouseWheel(MouseWheelDirection::Right), + processor: DualAxisProcessor::None, } } /// Generates a [`VirtualDPad`] corresponding to discretized mouse motions - pub const fn mouse_motion() -> VirtualDPad { + pub fn mouse_motion() -> VirtualDPad { VirtualDPad { up: InputKind::MouseMotion(MouseMotionDirection::Up), down: InputKind::MouseMotion(MouseMotionDirection::Down), left: InputKind::MouseMotion(MouseMotionDirection::Left), right: InputKind::MouseMotion(MouseMotionDirection::Right), + processor: DualAxisProcessor::None, } } - /// Returns this [`VirtualDPad`] but with `up` and `down` swapped. - pub fn inverted_y(mut self) -> Self { - std::mem::swap(&mut self.up, &mut self.down); + /// Appends the given [`DualAxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); self } - /// Returns this [`VirtualDPad`] but with `left` and `right` swapped. - pub fn inverted_x(mut self) -> Self { - std::mem::swap(&mut self.left, &mut self.right); + /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); self } - /// Returns this [`VirtualDPad`] but with inverted inputs. - pub fn inverted(mut self) -> Self { - std::mem::swap(&mut self.up, &mut self.down); - std::mem::swap(&mut self.left, &mut self.right); + /// Remove the current used [`DualAxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = DualAxisProcessor::None; self } + + /// Get the "value" of these axes. + /// If a processor is set, it will compute and return the processed value. + /// Otherwise, pass the `input_value` through unchanged. + #[must_use] + #[inline] + pub fn input_value(&self, input_value: Vec2) -> Vec2 { + self.processor.process(input_value) + } } /// A virtual Axis that you can get a value between -1 and 1 from. @@ -442,6 +418,8 @@ pub struct VirtualAxis { pub negative: InputKind, /// The input that represents the positive direction of this virtual axis pub positive: InputKind, + /// The processor used to handle input values. + pub processor: AxisProcessor, } impl VirtualAxis { @@ -451,6 +429,7 @@ impl VirtualAxis { VirtualAxis { negative: InputKind::PhysicalKey(negative), positive: InputKind::PhysicalKey(positive), + processor: AxisProcessor::None, } } @@ -480,6 +459,7 @@ impl VirtualAxis { VirtualAxis { negative: InputKind::GamepadButton(GamepadButtonType::DPadLeft), positive: InputKind::GamepadButton(GamepadButtonType::DPadRight), + processor: AxisProcessor::None, } } @@ -489,6 +469,7 @@ impl VirtualAxis { VirtualAxis { negative: InputKind::GamepadButton(GamepadButtonType::DPadDown), positive: InputKind::GamepadButton(GamepadButtonType::DPadUp), + processor: AxisProcessor::None, } } @@ -497,6 +478,7 @@ impl VirtualAxis { VirtualAxis { negative: InputKind::GamepadButton(GamepadButtonType::West), positive: InputKind::GamepadButton(GamepadButtonType::East), + processor: AxisProcessor::None, } } @@ -505,15 +487,39 @@ impl VirtualAxis { VirtualAxis { negative: InputKind::GamepadButton(GamepadButtonType::South), positive: InputKind::GamepadButton(GamepadButtonType::North), + processor: AxisProcessor::None, } } - /// Returns this [`VirtualAxis`] but with flipped positive/negative inputs. - #[must_use] - pub fn inverted(mut self) -> Self { - std::mem::swap(&mut self.positive, &mut self.negative); + /// Appends the given [`AxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self + } + + /// Replaces the current [`AxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); + self + } + + /// Removes the current used [`AxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = AxisProcessor::None; self } + + /// Get the "value" of the axis. + /// If a processor is set, it will compute and return the processed value. + /// Otherwise, pass the `input_value` through unchanged. + #[must_use] + #[inline] + pub fn input_value(&self, input_value: f32) -> f32 { + self.processor.process(input_value) + } } /// The type of axis used by a [`UserInput`](crate::user_input::UserInput). @@ -725,117 +731,3 @@ impl From for Vec2 { data.xy } } - -/// The shape of the deadzone for a [`DualAxis`] input. -/// -/// Input values that are on the boundary of the shape are counted as inside. -/// If the size of a shape is 0.0, then all input values are read, except for 0.0. -/// -/// All inputs are scaled to be continuous. -/// So with an ellipse deadzone with a radius of 0.1, the input range `0.1..=1.0` will be scaled to `0.0..=1.0`. -/// -/// Deadzone values should be in the range `0.0..=1.0`. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Reflect)] -pub enum DeadZoneShape { - /// Deadzone with the shape of a cross. - /// - /// The cross is represented by horizontal and vertical rectangles. - /// Each axis is handled separately which creates a per-axis "snapping" effect. - Cross { - /// The width of the horizontal axis. - /// - /// Affects the snapping of the y-axis. - horizontal_width: f32, - /// The width of the vertical axis. - /// - /// Affects the snapping of the x-axis. - vertical_width: f32, - }, - /// Deadzone with the shape of an ellipse. - Ellipse { - /// The horizontal radius of the ellipse. - radius_x: f32, - /// The vertical radius of the ellipse. - radius_y: f32, - }, -} - -impl Eq for DeadZoneShape {} - -impl std::hash::Hash for DeadZoneShape { - fn hash(&self, state: &mut H) { - match self { - DeadZoneShape::Cross { - horizontal_width, - vertical_width, - } => { - FloatOrd(*horizontal_width).hash(state); - FloatOrd(*vertical_width).hash(state); - } - DeadZoneShape::Ellipse { radius_x, radius_y } => { - FloatOrd(*radius_x).hash(state); - FloatOrd(*radius_y).hash(state); - } - } - } -} - -impl DeadZoneShape { - /// Computes the input value based on the deadzone. - pub fn deadzone_input_value(&self, x: f32, y: f32) -> Option { - match self { - DeadZoneShape::Cross { - horizontal_width, - vertical_width, - } => self.cross_deadzone_value(x, y, *horizontal_width, *vertical_width), - DeadZoneShape::Ellipse { radius_x, radius_y } => { - self.ellipse_deadzone_value(x, y, *radius_x, *radius_y) - } - } - } - - /// Computes the input value based on the cross deadzone. - fn cross_deadzone_value( - &self, - x: f32, - y: f32, - horizontal_width: f32, - vertical_width: f32, - ) -> Option { - let new_x = deadzone_axis_value(x, vertical_width); - let new_y = deadzone_axis_value(y, horizontal_width); - let is_outside_deadzone = new_x != 0.0 || new_y != 0.0; - is_outside_deadzone.then(|| DualAxisData::new(new_x, new_y)) - } - - /// Computes the input value based on the ellipse deadzone. - fn ellipse_deadzone_value( - &self, - x: f32, - y: f32, - radius_x: f32, - radius_y: f32, - ) -> Option { - let x_ratio = x / radius_x.max(f32::EPSILON); - let y_ratio = y / radius_y.max(f32::EPSILON); - let is_outside_deadzone = x_ratio.powi(2) + y_ratio.powi(2) >= 1.0; - is_outside_deadzone.then(|| { - let new_x = deadzone_axis_value(x, radius_x); - let new_y = deadzone_axis_value(y, radius_y); - DualAxisData::new(new_x, new_y) - }) - } -} - -/// Applies the given deadzone to the axis value. -/// -/// Returns 0.0 if the axis value is within the deadzone. -/// Otherwise, returns the normalized axis value between -1.0 and 1.0. -pub(crate) fn deadzone_axis_value(axis_value: f32, deadzone: f32) -> f32 { - let abs_axis_value = axis_value.abs(); - if abs_axis_value <= deadzone { - 0.0 - } else { - axis_value.signum() * (abs_axis_value - deadzone) / (1.0 - deadzone) - } -} diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 8dbda767..b92cdfc1 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -198,7 +198,7 @@ fn dpad_chord_clash(dpad: &VirtualDPad, chord: &[InputKind]) -> bool { chord.len() > 1 && chord .iter() - .any(|button| [dpad.up, dpad.down, dpad.left, dpad.right].contains(button)) + .any(|button| [&dpad.up, &dpad.down, &dpad.left, &dpad.right].contains(&button)) } #[must_use] @@ -313,6 +313,7 @@ fn resolve_clash( mod tests { use super::*; use crate as leafwing_input_manager; + use crate::input_processing::DualAxisProcessor; use bevy::app::App; use bevy::input::keyboard::KeyCode::*; use bevy::prelude::Reflect; @@ -352,6 +353,7 @@ mod tests { down: ArrowDown.into(), left: ArrowLeft.into(), right: ArrowRight.into(), + processor: DualAxisProcessor::None, }, ); input_map.insert_chord(CtrlUp, [ControlLeft, ArrowUp]); @@ -360,7 +362,6 @@ mod tests { } mod basic_functionality { - use crate::axislike::VirtualDPad; use crate::input_mocking::MockInput; use bevy::input::InputPlugin; use Action::*; @@ -380,6 +381,7 @@ mod tests { down: KeyX.into(), left: KeyY.into(), right: KeyZ.into(), + processor: DualAxisProcessor::None, } .into(); let abcd_dpad: UserInput = VirtualDPad { @@ -387,6 +389,7 @@ mod tests { down: KeyB.into(), left: KeyC.into(), right: KeyD.into(), + processor: DualAxisProcessor::None, } .into(); @@ -396,6 +399,7 @@ mod tests { down: ArrowDown.into(), left: ArrowLeft.into(), right: ArrowRight.into(), + processor: DualAxisProcessor::None, } .into(); diff --git a/src/display_impl.rs b/src/display_impl.rs index 1783bbf8..5c01b880 100644 --- a/src/display_impl.rs +++ b/src/display_impl.rs @@ -17,13 +17,16 @@ impl Display for UserInput { down, left, right, + .. }) => { write!( f, "VirtualDPad(up: {up}, down: {down}, left: {left}, right: {right})" ) } - UserInput::VirtualAxis(VirtualAxis { negative, positive }) => { + UserInput::VirtualAxis(VirtualAxis { + negative, positive, .. + }) => { write!(f, "VirtualDPad(negative: {negative}, positive: {positive})") } } diff --git a/src/input_map.rs b/src/input_map.rs index 13068573..9622422d 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -4,7 +4,7 @@ use crate::action_state::ActionData; use crate::buttonlike::ButtonState; use crate::clashing_inputs::ClashStrategy; use crate::input_streams::InputStreams; -use crate::user_input::{InputKind, Modifier, UserInput}; +use crate::user_input::*; use crate::Actionlike; #[cfg(feature = "asset")] @@ -49,7 +49,7 @@ enum Action { // Construction let mut input_map = InputMap::new([ - // Note that the type of your iterators must be homogenous; + // Note that the type of your iterators must be homogeneous; // you can use `InputKind` or `UserInput` if needed // as unifying types (Action::Run, GamepadButtonType::South), @@ -261,6 +261,9 @@ impl InputMap { /// Assigns a particular [`Gamepad`] to the entity controlled by this input map /// + /// Use this when an [`InputMap`] should exclusively accept input from a + /// particular gamepad. + /// /// If this is not called, input from any connected gamepad will be used. /// The first matching non-zero input will be accepted, /// as determined by gamepad registration order. @@ -325,7 +328,7 @@ impl InputMap { if input_streams.input_pressed(input) { action_datum.state = ButtonState::JustPressed; - action_datum.value += input_streams.input_value(input, true); + action_datum.value += input_streams.input_value(input); } } diff --git a/src/input_processing/dual_axis/circle.rs b/src/input_processing/dual_axis/circle.rs new file mode 100644 index 00000000..3a59840d --- /dev/null +++ b/src/input_processing/dual_axis/circle.rs @@ -0,0 +1,536 @@ +//! Circular range processors for dual-axis inputs + +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; + +use bevy::prelude::*; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +use super::DualAxisProcessor; + +/// Specifies a circular region defining acceptable ranges for valid dual-axis inputs, +/// with a radius defining the maximum threshold magnitude, +/// restricting all values stay within intended limits +/// to avoid unexpected behavior caused by extreme inputs. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Restrict magnitudes to no greater than 2 +/// let bounds = CircleBounds::new(2.0); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// assert_eq!(bounds.clamp(value), value.clamp_length_max(2.0)); +/// } +/// } +/// ``` +#[doc(alias = "RadialBounds")] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct CircleBounds { + /// The maximum radius of the circle. + pub(crate) radius: f32, +} + +impl CircleBounds { + /// Unlimited [`CircleBounds`]. + pub const FULL_RANGE: Self = Self { radius: f32::MAX }; + + /// Creates a [`CircleBounds`] that restricts input values to a maximum magnitude. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "magnitude")] + #[doc(alias = "from_radius")] + #[inline] + pub fn new(threshold: f32) -> Self { + assert!(threshold >= 0.0); + Self { radius: threshold } + } + + /// Returns the radius of the bounds. + #[must_use] + #[inline] + pub fn radius(&self) -> f32 { + self.radius + } + + /// Is the `input_value` is within the bounds? + #[must_use] + #[inline] + pub fn contains(&self, input_value: Vec2) -> bool { + input_value.length() <= self.radius + } + + /// Clamps the magnitude of `input_value` within the bounds. + #[must_use] + #[inline] + pub fn clamp(&self, input_value: Vec2) -> Vec2 { + input_value.clamp_length_max(self.radius) + } +} + +impl Default for CircleBounds { + /// Creates a [`CircleBounds`] that restricts the values to a maximum magnitude of `1.0`. + #[inline] + fn default() -> Self { + Self::new(1.0) + } +} + +impl From for DualAxisProcessor { + fn from(value: CircleBounds) -> Self { + Self::CircleBounds(value) + } +} + +impl Eq for CircleBounds {} + +impl Hash for CircleBounds { + fn hash(&self, state: &mut H) { + FloatOrd(self.radius).hash(state); + } +} + +/// Specifies a cross-shaped region for excluding dual-axis inputs, +/// with a radius defining the maximum excluded magnitude, +/// helping filter out minor fluctuations and unintended movements. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude magnitudes less than or equal to 0.2 +/// let exclusion = CircleExclusion::new(0.2); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// +/// if value.length() <= 0.2 { +/// assert!(exclusion.contains(value)); +/// assert_eq!(exclusion.exclude(value), Vec2::ZERO); +/// } else { +/// assert!(!exclusion.contains(value)); +/// assert_eq!(exclusion.exclude(value), value); +/// } +/// } +/// } +/// ``` +#[doc(alias = "RadialExclusion")] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct CircleExclusion { + /// Pre-calculated squared radius of the circle, preventing redundant calculations. + pub(crate) radius_squared: f32, +} + +impl CircleExclusion { + /// Zero-size [`CircleExclusion`], leaving values as is. + pub const ZERO: Self = Self { + radius_squared: 0.0, + }; + + /// Creates a [`CircleExclusion`] that ignores input values below a minimum magnitude. + /// + /// # Requirements + /// + /// - `radius` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "magnitude")] + #[doc(alias = "from_radius")] + #[inline] + pub fn new(threshold: f32) -> Self { + assert!(threshold >= 0.0); + Self { + radius_squared: threshold.powi(2), + } + } + + /// Returns the radius of the circle. + #[must_use] + #[inline] + pub fn radius(&self) -> f32 { + self.radius_squared.sqrt() + } + + /// Checks whether the `input_value` should be excluded. + #[must_use] + #[inline] + pub fn contains(&self, input_value: Vec2) -> bool { + input_value.length_squared() <= self.radius_squared + } + + /// Creates a [`CircleDeadZone`] using `self` as the exclusion range. + #[inline] + pub fn scaled(self) -> CircleDeadZone { + CircleDeadZone::new(self.radius()) + } + + /// Excludes input values with a magnitude less than the `radius`. + #[must_use] + #[inline] + pub fn exclude(&self, input_value: Vec2) -> Vec2 { + if self.contains(input_value) { + Vec2::ZERO + } else { + input_value + } + } +} + +impl Default for CircleExclusion { + /// Creates a [`CircleExclusion`] that ignores input values below a minimum magnitude of `0.1`. + fn default() -> Self { + Self::new(0.1) + } +} + +impl From for DualAxisProcessor { + fn from(value: CircleExclusion) -> Self { + Self::CircleExclusion(value) + } +} + +impl Eq for CircleExclusion {} + +impl Hash for CircleExclusion { + fn hash(&self, state: &mut H) { + FloatOrd(self.radius_squared).hash(state); + } +} + +/// A scaled version of [`CircleExclusion`] with the bounds +/// set to [`CircleBounds::new(1.0)`](CircleBounds::default) +/// that normalizes non-excluded input values into the "live zone", +/// the remaining range within the bounds after dead zone exclusion. +/// +/// It is worth considering that this normalizer reduces input values on diagonals. +/// If that is not your goal, you might want to explore alternative normalizers. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude magnitudes less than or equal to 0.2 +/// let deadzone = CircleDeadZone::new(0.2); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// +/// // Values within the dead zone are treated as zeros. +/// if value.length() <= 0.2 { +/// assert!(deadzone.within_exclusion(value)); +/// assert_eq!(deadzone.normalize(value), Vec2::ZERO); +/// } +/// +/// // Values within the live zone are scaled linearly. +/// else if value.length() <= 1.0 { +/// assert!(deadzone.within_livezone(value)); +/// +/// let expected_scale = f32::inverse_lerp(0.2, 1.0, value.length()); +/// let expected = value.normalize() * expected_scale; +/// let delta = (deadzone.normalize(value) - expected).abs(); +/// assert!(delta.x <= 0.00001); +/// assert!(delta.y <= 0.00001); +/// } +/// +/// // Values outside the bounds are restricted to the region. +/// else { +/// assert!(!deadzone.within_bounds(value)); +/// +/// let expected = value.clamp_length_max(1.0); +/// let delta = (deadzone.normalize(value) - expected).abs(); +/// assert!(delta.x <= 0.00001); +/// assert!(delta.y <= 0.00001); +/// } +/// } +/// } +/// ``` +#[doc(alias = "RadialDeadZone")] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct CircleDeadZone { + /// The radius of the circle. + pub(crate) radius: f32, + + /// Pre-calculated reciprocal of the live zone size, preventing division during normalization. + pub(crate) livezone_recip: f32, +} + +impl CircleDeadZone { + /// Zero-size [`CircleDeadZone`], only restricting values to a maximum magnitude of `1.0`. + pub const ZERO: Self = Self { + radius: 0.0, + livezone_recip: 1.0, + }; + + /// Creates a [`CircleDeadZone`] that excludes input values below a minimum magnitude. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "magnitude")] + #[doc(alias = "from_radius")] + #[inline] + pub fn new(threshold: f32) -> Self { + let bounds = CircleBounds::default(); + Self { + radius: threshold, + livezone_recip: (bounds.radius - threshold).recip(), + } + } + + /// Returns the radius of the circle. + #[must_use] + #[inline] + pub fn radius(&self) -> f32 { + self.radius + } + + /// Returns the [`CircleExclusion`] used by this deadzone. + #[inline] + pub fn exclusion(&self) -> CircleExclusion { + CircleExclusion::new(self.radius) + } + + /// Returns the [`CircleBounds`] used by this deadzone. + #[inline] + pub fn bounds(&self) -> CircleBounds { + CircleBounds::default() + } + + /// Returns the minimum and maximum radii of the live zone used by this deadzone. + /// + /// In simple terms, this returns `(self.radius, bounds.radius)`. + #[must_use] + #[inline] + pub fn livezone_min_max(&self) -> (f32, f32) { + (self.radius, self.bounds().radius) + } + + /// Is the given `input_value` within the exclusion range? + #[must_use] + #[inline] + pub fn within_exclusion(&self, input_value: Vec2) -> bool { + self.exclusion().contains(input_value) + } + + /// Is the given `input_value` within the bounds? + #[must_use] + #[inline] + pub fn within_bounds(&self, input_value: Vec2) -> bool { + self.bounds().contains(input_value) + } + + /// Is the given `input_value` within the live zone? + #[must_use] + #[inline] + pub fn within_livezone(&self, input_value: Vec2) -> bool { + let input_length = input_value.length(); + let (min, max) = self.livezone_min_max(); + min <= input_length && input_length <= max + } + + /// Normalizes input values into the live zone. + #[must_use] + pub fn normalize(&self, input_value: Vec2) -> Vec2 { + let input_length = input_value.length(); + if input_length == 0.0 { + return Vec2::ZERO; + } + + // Clamp out-of-bounds values to a maximum magnitude of 1.0, + // and then exclude values within the dead zone, + // and finally linearly scale the result to the live zone. + let (deadzone, bound) = self.livezone_min_max(); + let clamped_input_length = input_length.min(bound); + let offset_to_deadzone = (clamped_input_length - deadzone).max(0.0); + let magnitude_scale = (offset_to_deadzone * self.livezone_recip) / input_length; + input_value * magnitude_scale + } +} + +impl Default for CircleDeadZone { + /// Creates a [`CircleDeadZone`] that excludes input values below a minimum magnitude of `0.1`. + #[inline] + fn default() -> Self { + CircleDeadZone::new(0.1) + } +} + +impl From for DualAxisProcessor { + fn from(value: CircleDeadZone) -> Self { + Self::CircleDeadZone(value) + } +} + +impl Eq for CircleDeadZone {} + +impl Hash for CircleDeadZone { + fn hash(&self, state: &mut H) { + FloatOrd(self.radius).hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_circle_value_bounds() { + fn test_bounds(bounds: CircleBounds, radius: f32) { + assert_eq!(bounds.radius(), radius); + + let processor = DualAxisProcessor::CircleBounds(bounds); + assert_eq!(DualAxisProcessor::from(bounds), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), bounds.clamp(value)); + + if value.length() <= radius { + assert!(bounds.contains(value)); + } else { + assert!(!bounds.contains(value)); + } + + let expected = value.clamp_length_max(radius); + let delta = (bounds.clamp(value) - expected).abs(); + assert!(delta.x <= f32::EPSILON); + assert!(delta.y <= f32::EPSILON); + } + } + } + + let bounds = CircleBounds::FULL_RANGE; + test_bounds(bounds, f32::MAX); + + let bounds = CircleBounds::default(); + test_bounds(bounds, 1.0); + + let bounds = CircleBounds::new(2.0); + test_bounds(bounds, 2.0); + } + + #[test] + fn test_circle_exclusion() { + fn test_exclusion(exclusion: CircleExclusion, radius: f32) { + assert_eq!(exclusion.radius(), radius); + + let processor = DualAxisProcessor::CircleExclusion(exclusion); + assert_eq!(DualAxisProcessor::from(exclusion), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), exclusion.exclude(value)); + + if value.length() <= radius { + assert!(exclusion.contains(value)); + assert_eq!(exclusion.exclude(value), Vec2::ZERO); + } else { + assert!(!exclusion.contains(value)); + assert_eq!(exclusion.exclude(value), value); + } + } + } + } + + let exclusion = CircleExclusion::ZERO; + test_exclusion(exclusion, 0.0); + + let exclusion = CircleExclusion::default(); + test_exclusion(exclusion, 0.1); + + let exclusion = CircleExclusion::new(0.5); + test_exclusion(exclusion, 0.5); + } + + #[test] + fn test_circle_deadzone() { + fn test_deadzone(deadzone: CircleDeadZone, radius: f32) { + assert_eq!(deadzone.radius(), radius); + + let exclusion = CircleExclusion::new(radius); + assert_eq!(exclusion.scaled(), deadzone); + + let processor = DualAxisProcessor::CircleDeadZone(deadzone); + assert_eq!(DualAxisProcessor::from(deadzone), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), deadzone.normalize(value)); + + // Values within the dead zone are treated as zeros. + if value.length() <= radius { + assert!(deadzone.within_exclusion(value)); + assert_eq!(deadzone.normalize(value), Vec2::ZERO); + } + // Values within the live zone are scaled linearly. + else if value.length() <= 1.0 { + assert!(deadzone.within_livezone(value)); + + let expected_scale = f32::inverse_lerp(radius, 1.0, value.length()); + let expected = value.normalize() * expected_scale; + let delta = (deadzone.normalize(value) - expected).abs(); + assert!(delta.x <= 0.00001); + assert!(delta.y <= 0.00001); + } + // Values outside the bounds are restricted to the region. + else { + assert!(!deadzone.within_bounds(value)); + + let expected = value.clamp_length_max(1.0); + let delta = (deadzone.normalize(value) - expected).abs(); + assert!(delta.x <= 0.00001); + assert!(delta.y <= 0.00001); + } + } + } + } + + let deadzone = CircleDeadZone::ZERO; + test_deadzone(deadzone, 0.0); + + let deadzone = CircleDeadZone::default(); + test_deadzone(deadzone, 0.1); + + let deadzone = CircleDeadZone::new(0.5); + test_deadzone(deadzone, 0.5); + } +} diff --git a/src/input_processing/dual_axis/custom.rs b/src/input_processing/dual_axis/custom.rs new file mode 100644 index 00000000..1bd86c7e --- /dev/null +++ b/src/input_processing/dual_axis/custom.rs @@ -0,0 +1,349 @@ +use std::any::{Any, TypeId}; +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::RwLock; + +use bevy::app::App; +use bevy::prelude::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize, TypePath, Vec2}; +use bevy::reflect::utility::{reflect_hasher, GenericTypePathCell, NonGenericTypeInfoCell}; +use bevy::reflect::{ + erased_serde, FromType, GetTypeRegistration, ReflectFromPtr, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypeRegistration, Typed, ValueInfo, +}; +use dyn_clone::DynClone; +use dyn_eq::DynEq; +use dyn_hash::DynHash; +use once_cell::sync::Lazy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_flexitos::ser::require_erased_serialize_impl; +use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; + +use crate::input_processing::DualAxisProcessor; +use crate::typetag::RegisterTypeTag; + +/// A trait for creating custom processor that handles dual-axis input values, +/// accepting a [`Vec2`] input and producing a [`Vec2`] output. +/// +/// # Examples +/// +/// ```rust +/// use std::hash::{Hash, Hasher}; +/// use bevy::prelude::*; +/// use bevy::utils::FloatOrd; +/// use serde::{Deserialize, Serialize}; +/// use leafwing_input_manager::prelude::*; +/// +/// /// Doubles the input, takes its absolute value, +/// /// and discards results that meet the specified condition on the X-axis. +/// // If your processor includes fields not implemented Eq and Hash, +/// // implementation is necessary as shown below. +/// // Otherwise, you can derive Eq and Hash directly. +/// #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +/// pub struct DoubleAbsoluteValueThenRejectX(pub f32); +/// +/// // Add this attribute for ensuring proper serialization and deserialization. +/// #[serde_typetag] +/// impl CustomDualAxisProcessor for DoubleAbsoluteValueThenRejectX { +/// fn process(&self, input_value: Vec2) -> Vec2 { +/// // Implement the logic just like you would in a normal function. +/// +/// // You can use other processors within this function. +/// let value = DualAxisSensitivity::all(2.0).scale(input_value); +/// +/// let value = value.abs(); +/// let new_x = if value.x == self.0 { +/// 0.0 +/// } else { +/// value.x +/// }; +/// Vec2::new(new_x, value.y) +/// } +/// } +/// +/// // Unfortunately, manual implementation is required due to the float field. +/// impl Eq for DoubleAbsoluteValueThenRejectX {} +/// impl Hash for DoubleAbsoluteValueThenRejectX { +/// fn hash(&self, state: &mut H) { +/// // Encapsulate the float field for hashing. +/// FloatOrd(self.0).hash(state); +/// } +/// } +/// +/// // Remember to register your processor - it will ensure everything works smoothly! +/// let mut app = App::new(); +/// app.register_dual_axis_processor::(); +/// +/// // Now you can use it! +/// let processor = DoubleAbsoluteValueThenRejectX(4.0); +/// +/// // Rejected X! +/// assert_eq!(processor.process(Vec2::splat(2.0)), Vec2::new(0.0, 4.0)); +/// assert_eq!(processor.process(Vec2::splat(-2.0)), Vec2::new(0.0, 4.0)); +/// +/// // Others are just doubled absolute value. +/// assert_eq!(processor.process(Vec2::splat(6.0)), Vec2::splat(12.0)); +/// assert_eq!(processor.process(Vec2::splat(4.0)), Vec2::splat(8.0)); +/// assert_eq!(processor.process(Vec2::splat(0.0)), Vec2::splat(0.0)); +/// assert_eq!(processor.process(Vec2::splat(-4.0)), Vec2::splat(8.0)); +/// assert_eq!(processor.process(Vec2::splat(-6.0)), Vec2::splat(12.0)); +/// +/// // The ways to create a DualAxisProcessor. +/// let dual_axis_processor = DualAxisProcessor::Custom(Box::new(processor)); +/// assert_eq!(dual_axis_processor, DualAxisProcessor::from(processor)); +/// ``` +pub trait CustomDualAxisProcessor: + Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize +{ + /// Computes the result by processing the `input_value`. + fn process(&self, input_value: Vec2) -> Vec2; +} + +impl From

for DualAxisProcessor { + fn from(value: P) -> Self { + Self::Custom(Box::new(value)) + } +} + +dyn_clone::clone_trait_object!(CustomDualAxisProcessor); +dyn_eq::eq_trait_object!(CustomDualAxisProcessor); +dyn_hash::hash_trait_object!(CustomDualAxisProcessor); + +impl Reflect for Box { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(Self::type_info()) + } + + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn apply(&mut self, value: &dyn Reflect) { + let value = value.as_any(); + if let Some(value) = value.downcast_ref::() { + *self = value.clone(); + } else { + panic!( + "Value is not a std::boxed::Box.", + module_path!(), + ); + } + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Value + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Value(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Value(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Value(self) + } + + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + let type_id = TypeId::of::(); + Hash::hash(&type_id, &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + value + .as_any() + .downcast_ref::() + .map(|value| self.dyn_eq(value)) + .or(Some(false)) + } + + fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + +impl Typed for Box { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) + } +} + +impl TypePath for Box { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + { + format!( + "std::boxed::Box", + module_path!() + ) + } + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| "Box".to_string()) + } + + fn type_ident() -> Option<&'static str> { + Some("Box") + } + + fn crate_name() -> Option<&'static str> { + Some(module_path!().split(':').next().unwrap()) + } + + fn module_path() -> Option<&'static str> { + Some(module_path!()) + } +} + +impl GetTypeRegistration for Box { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for Box { + fn from_reflect(reflect: &dyn Reflect) -> Option { + Some(reflect.as_any().downcast_ref::()?.clone()) + } +} + +impl<'a> Serialize for dyn CustomDualAxisProcessor + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Check that `CustomDualAxisProcessor` has `erased_serde::Serialize` as a super trait, + // preventing infinite recursion at runtime. + const fn __check_erased_serialize_super_trait() { + require_erased_serialize_impl::(); + } + serialize_trait_object(serializer, self.reflect_short_type_path(), self) + } +} + +impl<'de> Deserialize<'de> for Box { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let registry = unsafe { PROCESSOR_REGISTRY.read().unwrap() }; + registry.deserialize_trait_object(deserializer) + } +} + +/// Registry of deserializers for [`CustomDualAxisProcessor`]s. +static mut PROCESSOR_REGISTRY: Lazy>> = + Lazy::new(|| RwLock::new(MapRegistry::new("CustomDualAxisProcessor"))); + +/// A trait for registering a specific [`CustomDualAxisProcessor`]. +pub trait RegisterDualAxisProcessor { + /// Registers the specified [`CustomDualAxisProcessor`]. + fn register_dual_axis_processor<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn CustomDualAxisProcessor> + GetTypeRegistration; +} + +impl RegisterDualAxisProcessor for App { + fn register_dual_axis_processor<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn CustomDualAxisProcessor> + GetTypeRegistration, + { + let mut registry = unsafe { PROCESSOR_REGISTRY.write().unwrap() }; + T::register_typetag(&mut registry); + self.register_type::(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as leafwing_input_manager; + use leafwing_input_manager_macros::serde_typetag; + use serde_test::{assert_tokens, Token}; + + #[test] + fn test_custom_dual_axis_processor() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] + struct CustomDualAxisInverted; + + #[serde_typetag] + impl CustomDualAxisProcessor for CustomDualAxisInverted { + fn process(&self, input_value: Vec2) -> Vec2 { + -input_value + } + } + + let mut app = App::new(); + app.register_dual_axis_processor::(); + + let custom: Box = Box::new(CustomDualAxisInverted); + assert_tokens( + &custom, + &[ + Token::Map { len: Some(1) }, + Token::BorrowedStr("CustomDualAxisInverted"), + Token::UnitStruct { + name: "CustomDualAxisInverted", + }, + Token::MapEnd, + ], + ); + + let processor = DualAxisProcessor::Custom(custom); + assert_eq!(DualAxisProcessor::from(CustomDualAxisInverted), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), -value); + assert_eq!(CustomDualAxisInverted.process(value), -value); + } + } + } +} diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs new file mode 100644 index 00000000..8cb83b21 --- /dev/null +++ b/src/input_processing/dual_axis/mod.rs @@ -0,0 +1,426 @@ +//! Processors for dual-axis input values + +use bevy::math::BVec2; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use bevy::prelude::{Reflect, Vec2}; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +pub use self::circle::*; +pub use self::custom::*; +pub use self::range::*; + +mod circle; +mod custom; +mod range; + +/// A processor for dual-axis input values, +/// accepting a [`Vec2`] input and producing a [`Vec2`] output. +#[must_use] +#[non_exhaustive] +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +pub enum DualAxisProcessor { + /// No processor is applied. + #[default] + None, + + /// A wrapper around [`DualAxisInverted`] to represent inversion. + Inverted(DualAxisInverted), + + /// A wrapper around [`DualAxisSensitivity`] to represent sensitivity. + Sensitivity(DualAxisSensitivity), + + /// A wrapper around [`DualAxisBounds`] to represent value bounds. + ValueBounds(DualAxisBounds), + + /// A wrapper around [`DualAxisExclusion`] to represent unscaled deadzone. + Exclusion(DualAxisExclusion), + + /// A wrapper around [`DualAxisDeadZone`] to represent scaled deadzone. + DeadZone(DualAxisDeadZone), + + /// A wrapper around [`CircleBounds`] to represent circular value bounds. + CircleBounds(CircleBounds), + + /// A wrapper around [`CircleExclusion`] to represent unscaled deadzone. + CircleExclusion(CircleExclusion), + + /// A wrapper around [`CircleDeadZone`] to represent scaled deadzone. + CircleDeadZone(CircleDeadZone), + + /// Processes input values sequentially through a sequence of [`DualAxisProcessor`]s. + /// one for the current step and the other for the next step. + /// + /// For a straightforward creation of a [`DualAxisProcessor::Pipeline`], + /// you can use [`DualAxisProcessor::with_processor`] or [`From>::from`] methods. + /// + /// ```rust + /// use std::sync::Arc; + /// use leafwing_input_manager::prelude::*; + /// + /// let expected = DualAxisProcessor::Pipeline(vec![ + /// Arc::new(DualAxisInverted::ALL.into()), + /// Arc::new(DualAxisSensitivity::all(2.0).into()), + /// ]); + /// + /// assert_eq!( + /// expected, + /// DualAxisProcessor::from(DualAxisInverted::ALL).with_processor(DualAxisSensitivity::all(2.0)) + /// ); + /// + /// assert_eq!( + /// expected, + /// DualAxisProcessor::from(vec![ + /// DualAxisInverted::ALL.into(), + /// DualAxisSensitivity::all(2.0).into(), + /// ]) + /// ); + Pipeline(Vec>), + + /// A user-defined processor that implements [`CustomDualAxisProcessor`]. + Custom(Box), +} + +impl DualAxisProcessor { + /// Computes the result by processing the `input_value`. + #[must_use] + #[inline] + pub fn process(&self, input_value: Vec2) -> Vec2 { + match self { + Self::None => input_value, + Self::Inverted(inversion) => inversion.invert(input_value), + Self::Sensitivity(sensitivity) => sensitivity.scale(input_value), + Self::ValueBounds(bounds) => bounds.clamp(input_value), + Self::Exclusion(exclusion) => exclusion.exclude(input_value), + Self::DeadZone(deadzone) => deadzone.normalize(input_value), + Self::CircleBounds(bounds) => bounds.clamp(input_value), + Self::CircleExclusion(exclusion) => exclusion.exclude(input_value), + Self::CircleDeadZone(deadzone) => deadzone.normalize(input_value), + Self::Pipeline(sequence) => sequence + .iter() + .fold(input_value, |value, next| next.process(value)), + Self::Custom(processor) => processor.process(input_value), + } + } + + /// Appends the given `next_processor` as the next processing step. + /// + /// - If either processor is [`DualAxisProcessor::None`], returns the other. + /// - If the current processor is [`DualAxisProcessor::Pipeline`], pushes the other into it. + /// - If the given processor is [`DualAxisProcessor::Pipeline`], prepends the current one into it. + /// - If both processors are [`DualAxisProcessor::Pipeline`], merges the two pipelines. + /// - If neither processor is [`DualAxisProcessor::None`] nor a pipeline, + /// creates a new pipeline containing them. + #[inline] + pub fn with_processor(self, next_processor: impl Into) -> Self { + let other = next_processor.into(); + match (self.clone(), other.clone()) { + (_, Self::None) => self, + (Self::None, _) => other, + (Self::Pipeline(mut self_seq), Self::Pipeline(mut next_seq)) => { + self_seq.append(&mut next_seq); + Self::Pipeline(self_seq) + } + (Self::Pipeline(mut self_seq), _) => { + self_seq.push(Arc::new(other)); + Self::Pipeline(self_seq) + } + (_, Self::Pipeline(mut next_seq)) => { + next_seq.insert(0, Arc::new(self)); + Self::Pipeline(next_seq) + } + (_, _) => Self::Pipeline(vec![Arc::new(self), Arc::new(other)]), + } + } +} + +impl From> for DualAxisProcessor { + fn from(value: Vec) -> Self { + Self::Pipeline(value.into_iter().map(Arc::new).collect()) + } +} + +/// Flips the sign of dual-axis input values, resulting in a directional reversal of control. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// let value = Vec2::new(1.5, 2.0); +/// let Vec2 { x, y } = value; +/// +/// assert_eq!(DualAxisInverted::ALL.invert(value), -value); +/// assert_eq!(DualAxisInverted::ALL.invert(-value), value); +/// +/// assert_eq!(DualAxisInverted::ONLY_X.invert(value), Vec2::new(-x, y)); +/// assert_eq!(DualAxisInverted::ONLY_X.invert(-value), Vec2::new(x, -y)); +/// +/// assert_eq!(DualAxisInverted::ONLY_Y.invert(value), Vec2::new(x, -y)); +/// assert_eq!(DualAxisInverted::ONLY_Y.invert(-value), Vec2::new(-x, y)); +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct DualAxisInverted(Vec2); + +impl DualAxisInverted { + /// The [`DualAxisInverted`] that inverts both axes. + pub const ALL: Self = Self(Vec2::NEG_ONE); + + /// The [`DualAxisInverted`] that only inverts the X-axis inputs. + pub const ONLY_X: Self = Self(Vec2::new(-1.0, 1.0)); + + /// The [`DualAxisInverted`] that only inverts the Y-axis inputs. + pub const ONLY_Y: Self = Self(Vec2::new(1.0, -1.0)); + + /// Are inputs inverted on both axes? + #[must_use] + #[inline] + pub fn inverted(&self) -> BVec2 { + self.0.cmpeq(Vec2::NEG_ONE) + } + + /// Multiples the `input_value` by the specified inversion vector. + #[must_use] + #[inline] + pub fn invert(&self, input_value: Vec2) -> Vec2 { + self.0 * input_value + } +} + +impl From for DualAxisProcessor { + fn from(value: DualAxisInverted) -> Self { + Self::Inverted(value) + } +} + +impl Eq for DualAxisInverted {} + +impl Hash for DualAxisInverted { + fn hash(&self, state: &mut H) { + FloatOrd(self.0.x).hash(state); + FloatOrd(self.0.y).hash(state); + } +} + +/// Scales dual-axis input values using a specified multiplier to fine-tune the responsiveness of control. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// let value = Vec2::new(1.5, 2.5); +/// let Vec2 { x, y } = value; +/// +/// // Negated X and halved Y +/// let neg_x_half_y = DualAxisSensitivity::new(-1.0, 0.5); +/// assert_eq!(neg_x_half_y.scale(value).x, -x); +/// assert_eq!(neg_x_half_y.scale(value).y, 0.5 * y); +/// +/// // Doubled X and doubled Y +/// let double = DualAxisSensitivity::all(2.0); +/// assert_eq!(double.scale(value), 2.0 * value); +/// +/// // Halved X +/// let half_x = DualAxisSensitivity::only_x(0.5); +/// assert_eq!(half_x.scale(value).x, 0.5 * x); +/// assert_eq!(half_x.scale(value).y, y); +/// +/// // Negated and doubled Y +/// let neg_double_y = DualAxisSensitivity::only_y(-2.0); +/// assert_eq!(neg_double_y.scale(value).x, x); +/// assert_eq!(neg_double_y.scale(value).y, -2.0 * y); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct DualAxisSensitivity(pub(crate) Vec2); + +impl DualAxisSensitivity { + /// Creates a [`DualAxisSensitivity`] with the given values for each axis separately. + #[inline] + pub const fn new(sensitivity_x: f32, sensitivity_y: f32) -> Self { + Self(Vec2::new(sensitivity_x, sensitivity_y)) + } + + /// Creates a [`DualAxisSensitivity`] with the same value for both axes. + #[inline] + pub const fn all(sensitivity: f32) -> Self { + Self::new(sensitivity, sensitivity) + } + + /// Creates a [`DualAxisSensitivity`] that only affects the X-axis using the given value. + #[inline] + pub const fn only_x(sensitivity: f32) -> Self { + Self::new(sensitivity, 1.0) + } + + /// Creates a [`DualAxisSensitivity`] that only affects the Y-axis using the given value. + #[inline] + pub const fn only_y(sensitivity: f32) -> Self { + Self::new(1.0, sensitivity) + } + + /// Returns the sensitivity values. + #[must_use] + #[inline] + pub fn sensitivities(&self) -> Vec2 { + self.0 + } + + /// Multiples the `input_value` by the specified sensitivity vector. + #[must_use] + #[inline] + pub fn scale(&self, input_value: Vec2) -> Vec2 { + self.0 * input_value + } +} + +impl From for DualAxisProcessor { + fn from(value: DualAxisSensitivity) -> Self { + Self::Sensitivity(value) + } +} + +impl Eq for DualAxisSensitivity {} + +impl Hash for DualAxisSensitivity { + fn hash(&self, state: &mut H) { + FloatOrd(self.0.x).hash(state); + FloatOrd(self.0.y).hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_axis_processor_pipeline() { + let pipeline = DualAxisProcessor::Pipeline(vec![ + Arc::new(DualAxisInverted::ALL.into()), + Arc::new(DualAxisSensitivity::all(2.0).into()), + ]); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(pipeline.process(value), value * -2.0); + } + } + } + + #[test] + fn test_dual_axis_processor_from_list() { + assert_eq!( + DualAxisProcessor::from(vec![]), + DualAxisProcessor::Pipeline(vec![]) + ); + + assert_eq!( + DualAxisProcessor::from(vec![DualAxisInverted::ALL.into()]), + DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) + ); + + assert_eq!( + DualAxisProcessor::from(vec![ + DualAxisInverted::ALL.into(), + DualAxisSensitivity::all(2.0).into(), + ]), + DualAxisProcessor::Pipeline(vec![ + Arc::new(DualAxisInverted::ALL.into()), + Arc::new(DualAxisSensitivity::all(2.0).into()), + ]) + ); + } + + #[test] + fn test_dual_axis_inverted() { + let all = DualAxisInverted::ALL; + assert_eq!(all.inverted(), BVec2::TRUE); + + let only_x = DualAxisInverted::ONLY_X; + assert_eq!(only_x.inverted(), BVec2::new(true, false)); + + let only_y = DualAxisInverted::ONLY_Y; + assert_eq!(only_y.inverted(), BVec2::new(false, true)); + + for x in -300..300 { + let x = x as f32 * 0.01; + + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + let processor = DualAxisProcessor::Inverted(all); + assert_eq!(DualAxisProcessor::from(all), processor); + assert_eq!(processor.process(value), all.invert(value)); + assert_eq!(all.invert(value), -value); + assert_eq!(all.invert(-value), value); + + let processor = DualAxisProcessor::Inverted(only_x); + assert_eq!(DualAxisProcessor::from(only_x), processor); + assert_eq!(processor.process(value), only_x.invert(value)); + assert_eq!(only_x.invert(value), Vec2::new(-x, y)); + assert_eq!(only_x.invert(-value), Vec2::new(x, -y)); + + let processor = DualAxisProcessor::Inverted(only_y); + assert_eq!(DualAxisProcessor::from(only_y), processor); + assert_eq!(processor.process(value), only_y.invert(value)); + assert_eq!(only_y.invert(value), Vec2::new(x, -y)); + assert_eq!(only_y.invert(-value), Vec2::new(-x, y)); + } + } + } + + #[test] + fn test_dual_axis_sensitivity() { + for x in -300..300 { + let x = x as f32 * 0.01; + + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + let sensitivity = x; + + let all = DualAxisSensitivity::all(sensitivity); + let processor = DualAxisProcessor::Sensitivity(all); + assert_eq!(DualAxisProcessor::from(all), processor); + assert_eq!(processor.process(value), all.scale(value)); + assert_eq!(all.sensitivities(), Vec2::splat(sensitivity)); + assert_eq!(all.scale(value), sensitivity * value); + + let only_x = DualAxisSensitivity::only_x(sensitivity); + let processor = DualAxisProcessor::Sensitivity(only_x); + assert_eq!(DualAxisProcessor::from(only_x), processor); + assert_eq!(processor.process(value), only_x.scale(value)); + assert_eq!(only_x.sensitivities(), Vec2::new(sensitivity, 1.0)); + assert_eq!(only_x.scale(value).x, x * sensitivity); + assert_eq!(only_x.scale(value).y, y); + + let only_y = DualAxisSensitivity::only_y(sensitivity); + let processor = DualAxisProcessor::Sensitivity(only_y); + assert_eq!(DualAxisProcessor::from(only_y), processor); + assert_eq!(processor.process(value), only_y.scale(value)); + assert_eq!(only_y.sensitivities(), Vec2::new(1.0, sensitivity)); + assert_eq!(only_y.scale(value).x, x); + assert_eq!(only_y.scale(value).y, y * sensitivity); + + let sensitivity2 = y; + let separate = DualAxisSensitivity::new(sensitivity, sensitivity2); + let processor = DualAxisProcessor::Sensitivity(separate); + assert_eq!(DualAxisProcessor::from(separate), processor); + assert_eq!(processor.process(value), separate.scale(value)); + assert_eq!( + separate.sensitivities(), + Vec2::new(sensitivity, sensitivity2) + ); + assert_eq!(separate.scale(value).x, x * sensitivity); + assert_eq!(separate.scale(value).y, y * sensitivity2); + } + } + } +} diff --git a/src/input_processing/dual_axis/range.rs b/src/input_processing/dual_axis/range.rs new file mode 100644 index 00000000..4e0f33cb --- /dev/null +++ b/src/input_processing/dual_axis/range.rs @@ -0,0 +1,1391 @@ +//! Range processors for dual-axis inputs + +use std::fmt::Debug; +use std::hash::Hash; + +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::DualAxisProcessor; +use crate::input_processing::single_axis::*; + +/// Specifies a square-shaped region defining acceptable ranges for valid dual-axis inputs, +/// with independent min-max ranges for each axis, restricting all values stay within intended limits +/// to avoid unexpected behavior caused by extreme inputs. +/// +/// In simple terms, this processor is just the dual-axis version of [`AxisBounds`]. +/// Helpers like [`AxisBounds::extend_dual()`] and its peers can be used to create a [`DualAxisBounds`]. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Restrict X to [-2.0, 2.5] and Y to [-1.0, 1.5]. +/// let bounds = DualAxisBounds::new((-2.0, 2.5), (-1.0, 1.5)); +/// assert_eq!(bounds.bounds_x().min_max(), (-2.0, 2.5)); +/// assert_eq!(bounds.bounds_y().min_max(), (-1.0, 1.5)); +/// +/// // Another way to create a DualAxisBounds. +/// let bounds_x = AxisBounds::new(-2.0, 2.5); +/// let bounds_y = AxisBounds::new(-1.0, 1.5); +/// assert_eq!(bounds_x.extend_dual_with_y(bounds_y), bounds); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// +/// assert_eq!(bounds.clamp(value).x, bounds_x.clamp(x)); +/// assert_eq!(bounds.clamp(value).y, bounds_y.clamp(y)); +/// } +/// } +/// ``` +#[doc(alias("SquareBounds", "AxialBounds"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct DualAxisBounds { + /// The [`AxisBounds`] for the X-axis inputs. + pub(crate) bounds_x: AxisBounds, + + /// The [`AxisBounds`] for the Y-axis inputs. + pub(crate) bounds_y: AxisBounds, +} + +impl DualAxisBounds { + /// Unlimited [`DualAxisBounds`]. + pub const FULL_RANGE: Self = AxisBounds::FULL_RANGE.extend_dual(); + + /// Creates a [`DualAxisBounds`] that restricts values within the range `[min, max]` on each axis. + /// + /// # Requirements + /// + /// - `min` <= `max` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new((x_min, x_max): (f32, f32), (y_min, y_max): (f32, f32)) -> Self { + Self { + bounds_x: AxisBounds::new(x_min, x_max), + bounds_y: AxisBounds::new(y_min, y_max), + } + } + + /// Creates a [`DualAxisBounds`] that restricts values within the same range `[min, max]` on both axes. + /// + /// # Requirements + /// + /// - `min` <= `max`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn all(min: f32, max: f32) -> Self { + let range = (min, max); + Self::new(range, range) + } + + /// Creates a [`DualAxisBounds`] that only restricts X values within the range `[min, max]`. + /// + /// # Requirements + /// + /// - `min` <= `max`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_x(min: f32, max: f32) -> Self { + Self { + bounds_x: AxisBounds::new(min, max), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that only restricts Y values within the range `[min, max]`. + /// + /// # Requirements + /// + /// - `min` <= `max`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_y(min: f32, max: f32) -> Self { + Self { + bounds_y: AxisBounds::new(min, max), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that restricts values within the range `[-threshold, threshold]` on each axis. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + Self { + bounds_x: AxisBounds::magnitude(threshold_x), + bounds_y: AxisBounds::magnitude(threshold_y), + } + } + + /// Creates a [`DualAxisBounds`] that restricts values within the range `[-threshold, threshold]` on both axes. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_all")] + #[inline] + pub fn magnitude_all(threshold: f32) -> Self { + Self::magnitude(threshold, threshold) + } + + /// Creates a [`DualAxisBounds`] that only restricts X values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_x")] + #[inline] + pub fn magnitude_only_x(threshold: f32) -> Self { + Self { + bounds_x: AxisBounds::magnitude(threshold), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that only restricts Y values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_y")] + #[inline] + pub fn magnitude_only_y(threshold: f32) -> Self { + Self { + bounds_y: AxisBounds::magnitude(threshold), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that restricts values to a minimum value on each axis. + #[inline] + pub const fn at_least(x_min: f32, y_min: f32) -> Self { + Self { + bounds_x: AxisBounds::at_least(x_min), + bounds_y: AxisBounds::at_least(y_min), + } + } + + /// Creates a [`DualAxisBounds`] that restricts values to a minimum value on both axes. + #[inline] + pub const fn at_least_all(min: f32) -> Self { + AxisBounds::at_least(min).extend_dual() + } + + /// Creates a [`DualAxisBounds`] that only restricts X values to a minimum value. + #[inline] + pub const fn at_least_only_x(min: f32) -> Self { + Self { + bounds_x: AxisBounds::at_least(min), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that only restricts Y values to a minimum value. + #[inline] + pub const fn at_least_only_y(min: f32) -> Self { + Self { + bounds_y: AxisBounds::at_least(min), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that restricts values to a maximum value on each axis. + #[inline] + pub const fn at_most(x_max: f32, y_max: f32) -> Self { + Self { + bounds_x: AxisBounds::at_most(x_max), + bounds_y: AxisBounds::at_most(y_max), + } + } + + /// Creates a [`DualAxisBounds`] that restricts values to a maximum value on both axes. + #[inline] + pub const fn at_most_all(max: f32) -> Self { + AxisBounds::at_most(max).extend_dual() + } + + /// Creates a [`DualAxisBounds`] that only restricts X values to a maximum value. + #[inline] + pub const fn at_most_only_x(max: f32) -> Self { + Self { + bounds_x: AxisBounds::at_most(max), + ..Self::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] that only restricts Y values to a maximum value. + #[inline] + pub const fn at_most_only_y(max: f32) -> Self { + Self { + bounds_y: AxisBounds::at_most(max), + ..Self::FULL_RANGE + } + } + + /// Returns the bounds for inputs along each axis. + #[inline] + pub fn bounds(&self) -> (AxisBounds, AxisBounds) { + (self.bounds_x, self.bounds_y) + } + + /// Returns the bounds for the X-axis inputs. + #[inline] + pub fn bounds_x(&self) -> AxisBounds { + self.bounds().0 + } + + /// Returns the bounds for the Y-axis inputs. + #[inline] + pub fn bounds_y(&self) -> AxisBounds { + self.bounds().1 + } + + /// Is `input_value` is within the bounds? + #[must_use] + #[inline] + pub fn contains(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.bounds_x.contains(input_value.x), + self.bounds_y.contains(input_value.y), + ) + } + + /// Clamps `input_value` within the bounds. + #[must_use] + #[inline] + pub fn clamp(&self, input_value: Vec2) -> Vec2 { + Vec2::new( + self.bounds_x.clamp(input_value.x), + self.bounds_y.clamp(input_value.y), + ) + } +} + +impl Default for DualAxisBounds { + /// Creates a [`DualAxisBounds`] that restricts values within the range `[-1.0, 1.0]` on both axes. + #[inline] + fn default() -> Self { + AxisBounds::default().extend_dual() + } +} + +impl From for DualAxisProcessor { + fn from(value: DualAxisBounds) -> Self { + Self::ValueBounds(value) + } +} + +impl AxisBounds { + /// Creates a [`DualAxisBounds`] using `self` for both axes. + #[inline] + pub const fn extend_dual(self) -> DualAxisBounds { + DualAxisBounds { + bounds_x: self, + bounds_y: self, + } + } + + /// Creates a [`DualAxisBounds`] only using `self` for the X-axis. + #[inline] + pub const fn extend_dual_only_x(self) -> DualAxisBounds { + DualAxisBounds { + bounds_x: self, + ..DualAxisBounds::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] only using `self` to the Y-axis. + #[inline] + pub const fn extend_dual_only_y(self) -> DualAxisBounds { + DualAxisBounds { + bounds_y: self, + ..DualAxisBounds::FULL_RANGE + } + } + + /// Creates a [`DualAxisBounds`] using `self` to the Y-axis with the given `bounds_x` to the X-axis. + #[inline] + pub const fn extend_dual_with_x(self, bounds_x: Self) -> DualAxisBounds { + DualAxisBounds { + bounds_x, + bounds_y: self, + } + } + + /// Creates a [`DualAxisBounds`] using `self` to the X-axis with the given `bounds_y` to the Y-axis. + #[inline] + pub const fn extend_dual_with_y(self, bounds_y: Self) -> DualAxisBounds { + DualAxisBounds { + bounds_x: self, + bounds_y, + } + } +} + +impl From for DualAxisProcessor { + fn from(bounds: AxisBounds) -> Self { + Self::ValueBounds(bounds.extend_dual()) + } +} + +/// Specifies a cross-shaped region for excluding dual-axis inputs, +/// with min-max independent min-max ranges for each axis, resulting in a per-axis "snapping" effect, +/// helping filter out minor fluctuations to enhance control precision for pure axial motion. +/// +/// In simple terms, this processor is just the dual-axis version of [`AxisExclusion`]. +/// Helpers like [`AxisExclusion::extend_dual()`] and its peers can be used to create a [`DualAxisExclusion`]. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude X within [-0.2, 0.3] and Y within [-0.1, 0.4]. +/// let exclusion = DualAxisExclusion::new((-0.2, 0.3), (-0.1, 0.4)); +/// assert_eq!(exclusion.exclusion_x().min_max(), (-0.2, 0.3)); +/// assert_eq!(exclusion.exclusion_y().min_max(), (-0.1, 0.4)); +/// +/// // Another way to create a DualAxisExclusion. +/// let exclusion_x = AxisExclusion::new(-0.2, 0.3); +/// let exclusion_y = AxisExclusion::new(-0.1, 0.4); +/// assert_eq!(exclusion_x.extend_dual_with_y(exclusion_y), exclusion); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// +/// assert_eq!(exclusion.exclude(value).x, exclusion_x.exclude(x)); +/// assert_eq!(exclusion.exclude(value).y, exclusion_y.exclude(y)); +/// } +/// } +/// ``` +#[doc(alias("CrossExclusion", "AxialExclusion"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct DualAxisExclusion { + /// The [`AxisExclusion`] for the X-axis inputs. + pub(crate) exclusion_x: AxisExclusion, + + /// The [`AxisExclusion`] for the Y-axis inputs. + pub(crate) exclusion_y: AxisExclusion, +} + +impl DualAxisExclusion { + /// Zero-size [`DualAxisExclusion`], leaving values as is. + pub const ZERO: Self = AxisExclusion::ZERO.extend_dual(); + + /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on each axis. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new( + (x_negative_max, x_positive_min): (f32, f32), + (y_negative_max, y_positive_min): (f32, f32), + ) -> Self { + Self { + exclusion_x: AxisExclusion::new(x_negative_max, x_positive_min), + exclusion_y: AxisExclusion::new(y_negative_max, y_positive_min), + } + } + + /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on both axis. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn all(negative_max: f32, positive_min: f32) -> Self { + let range = (negative_max, positive_min); + Self::new(range, range) + } + + /// Creates a [`DualAxisExclusion`] that only ignores X values within the range `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_x(negative_max: f32, positive_min: f32) -> Self { + Self { + exclusion_x: AxisExclusion::new(negative_max, positive_min), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisExclusion`] that only ignores Y values within the range `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_y(negative_max: f32, positive_min: f32) -> Self { + Self { + exclusion_y: AxisExclusion::new(negative_max, positive_min), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisExclusion`] that ignores values within the range `[-threshold, threshold]` on each axis. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + Self { + exclusion_x: AxisExclusion::magnitude(threshold_x), + exclusion_y: AxisExclusion::magnitude(threshold_y), + } + } + + /// Creates a [`DualAxisExclusion`] that ignores values within the range `[-threshold, threshold]` on both axes. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_all")] + #[inline] + pub fn magnitude_all(threshold: f32) -> Self { + Self::magnitude(threshold, threshold) + } + + /// Creates a [`DualAxisExclusion`] that only ignores X values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_x")] + #[inline] + pub fn magnitude_only_x(threshold: f32) -> Self { + Self { + exclusion_x: AxisExclusion::magnitude(threshold), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisExclusion`] that only ignores Y values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_y")] + #[inline] + pub fn magnitude_only_y(threshold: f32) -> Self { + Self { + exclusion_y: AxisExclusion::magnitude(threshold), + ..Self::ZERO + } + } + + /// Returns the exclusion ranges for inputs along each axis. + #[inline] + pub fn exclusions(&self) -> (AxisExclusion, AxisExclusion) { + (self.exclusion_x, self.exclusion_y) + } + + /// Returns the exclusion range for the X-axis inputs. + #[inline] + pub fn exclusion_x(&self) -> AxisExclusion { + self.exclusions().0 + } + + /// Returns the exclusion range for the Y-axis inputs. + #[inline] + pub fn exclusion_y(&self) -> AxisExclusion { + self.exclusions().1 + } + + /// Is the `input_value` within the exclusion range? + #[must_use] + #[inline] + pub fn contains(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.exclusion_x.contains(input_value.x), + self.exclusion_y.contains(input_value.y), + ) + } + + /// Excludes values within the specified region. + #[must_use] + #[inline] + pub fn exclude(&self, input_value: Vec2) -> Vec2 { + Vec2::new( + self.exclusion_x.exclude(input_value.x), + self.exclusion_y.exclude(input_value.y), + ) + } + + /// Creates a [`DualAxisDeadZone`] using `self` as the exclusion range. + pub fn scaled(self) -> DualAxisDeadZone { + DualAxisDeadZone::new(self.exclusion_x.min_max(), self.exclusion_y.min_max()) + } +} + +impl Default for DualAxisExclusion { + /// Creates a [`DualAxisExclusion`] that excludes input values within `[-1.0, 1.0]` on both axes. + #[inline] + fn default() -> Self { + AxisExclusion::default().extend_dual() + } +} + +impl From for DualAxisProcessor { + fn from(value: DualAxisExclusion) -> Self { + Self::Exclusion(value) + } +} + +impl AxisExclusion { + /// Creates a [`DualAxisExclusion`] using `self` for both axes. + #[inline] + pub const fn extend_dual(self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_x: self, + exclusion_y: self, + } + } + + /// Creates a [`DualAxisExclusion`] only using `self` for the X-axis. + #[inline] + pub const fn extend_dual_only_x(self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_x: self, + ..DualAxisExclusion::ZERO + } + } + + /// Creates a [`DualAxisExclusion`] only using `self` to the Y-axis. + #[inline] + pub const fn extend_dual_only_y(self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_y: self, + ..DualAxisExclusion::ZERO + } + } + + /// Creates a [`DualAxisExclusion`] using `self` to the Y-axis with the given `bounds_x` to the X-axis. + #[inline] + pub const fn extend_dual_with_x(self, exclusion_x: Self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_x, + exclusion_y: self, + } + } + + /// Creates a [`DualAxisExclusion`] using `self` to the X-axis with the given `bounds_y` to the Y-axis. + #[inline] + pub const fn extend_dual_with_y(self, exclusion_y: Self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_x: self, + exclusion_y, + } + } +} + +impl From for DualAxisProcessor { + fn from(exclusion: AxisExclusion) -> Self { + Self::Exclusion(exclusion.extend_dual()) + } +} + +/// A scaled version of [`DualAxisExclusion`] with the bounds +/// set to [`DualAxisBounds::magnitude_all(1.0)`](DualAxisBounds::default) +/// that normalizes non-excluded input values into the "live zone", +/// the remaining range within the bounds after dead zone exclusion. +/// +/// Each axis is processed individually, resulting in a per-axis "snapping" effect, +/// which enhances control precision for pure axial motion. +/// +/// It is worth considering that this normalizer increases the magnitude of diagonal values. +/// If that is not your goal, you might want to explore alternative normalizers. +/// +/// In simple terms, this processor is just the dual-axis version of [`AxisDeadZone`]. +/// Helpers like [`AxisDeadZone::extend_dual()`] and its peers can be used to create a [`DualAxisDeadZone`]. +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude X within [-0.2, 0.3] and Y within [-0.1, 0.4]. +/// let deadzone = DualAxisDeadZone::new((-0.2, 0.3), (-0.1, 0.4)); +/// assert_eq!(deadzone.deadzone_x().exclusion().min_max(), (-0.2, 0.3)); +/// assert_eq!(deadzone.deadzone_y().exclusion().min_max(), (-0.1, 0.4)); +/// +/// // Another way to create a DualAxisDeadZone. +/// let deadzone_x = AxisDeadZone::new(-0.2, 0.3); +/// let deadzone_y = AxisDeadZone::new(-0.1, 0.4); +/// assert_eq!(deadzone_x.extend_dual_with_y(deadzone_y), deadzone); +/// +/// for x in -300..300 { +/// let x = x as f32 * 0.01; +/// for y in -300..300 { +/// let y = y as f32 * 0.01; +/// let value = Vec2::new(x, y); +/// +/// assert_eq!(deadzone.normalize(value).x, deadzone_x.normalize(x)); +/// assert_eq!(deadzone.normalize(value).y, deadzone_y.normalize(y)); +/// } +/// } +/// ``` +#[doc(alias("CrossDeadZone", "AxialDeadZone"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct DualAxisDeadZone { + /// The [`AxisDeadZone`] for the X-axis inputs. + pub(crate) deadzone_x: AxisDeadZone, + + /// The [`AxisDeadZone`] for the Y-axis inputs. + pub(crate) deadzone_y: AxisDeadZone, +} + +impl DualAxisDeadZone { + /// Zero-size [`DualAxisDeadZone`], only restricting values to the range `[-1.0, 1.0]` on both axes. + pub const ZERO: Self = AxisDeadZone::ZERO.extend_dual(); + + /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[negative_max, positive_min]` on each axis. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new( + (x_negative_max, x_positive_min): (f32, f32), + (y_negative_max, y_positive_min): (f32, f32), + ) -> Self { + Self { + deadzone_x: AxisDeadZone::new(x_negative_max, x_positive_min), + deadzone_y: AxisDeadZone::new(y_negative_max, y_positive_min), + } + } + + /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[negative_max, positive_min]` on both axes. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn all(negative_max: f32, positive_min: f32) -> Self { + let range = (negative_max, positive_min); + Self::new(range, range) + } + + /// Creates a [`DualAxisDeadZone`] that only excludes X values within the range `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_x(negative_max: f32, positive_min: f32) -> Self { + Self { + deadzone_x: AxisDeadZone::new(negative_max, positive_min), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisDeadZone`] that only excludes Y values within the range `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn only_y(negative_max: f32, positive_min: f32) -> Self { + Self { + deadzone_y: AxisDeadZone::new(negative_max, positive_min), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[-threshold, threshold]` on each axis. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0` on each axis. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + Self { + deadzone_x: AxisDeadZone::magnitude(threshold_x), + deadzone_y: AxisDeadZone::magnitude(threshold_y), + } + } + + /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[-threshold, threshold]` on both axes. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_all")] + #[inline] + pub fn magnitude_all(threshold: f32) -> Self { + Self::magnitude(threshold, threshold) + } + + /// Creates a [`DualAxisDeadZone`] that only excludes X values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_x")] + #[inline] + pub fn magnitude_only_x(threshold: f32) -> Self { + Self { + deadzone_x: AxisDeadZone::magnitude(threshold), + ..Self::ZERO + } + } + + /// Creates a [`DualAxisDeadZone`] that only excludes Y values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric_only_y")] + #[inline] + pub fn magnitude_only_y(threshold: f32) -> Self { + Self { + deadzone_y: AxisDeadZone::magnitude(threshold), + ..Self::ZERO + } + } + + /// Returns the dead zones for inputs along each axis. + #[inline] + pub fn deadzones(&self) -> (AxisDeadZone, AxisDeadZone) { + (self.deadzone_x, self.deadzone_y) + } + + /// Returns the dead zone for the X-axis inputs. + #[inline] + pub fn deadzone_x(&self) -> AxisDeadZone { + self.deadzones().0 + } + + /// Returns the dead zone for the Y-axis inputs. + #[inline] + pub fn deadzone_y(&self) -> AxisDeadZone { + self.deadzones().1 + } + + /// Returns the [`DualAxisExclusion`] used by this deadzone. + #[inline] + pub fn exclusion(&self) -> DualAxisExclusion { + DualAxisExclusion { + exclusion_x: self.deadzone_x.exclusion(), + exclusion_y: self.deadzone_y.exclusion(), + } + } + + /// Returns the [`DualAxisBounds`] used by this deadzone. + #[inline] + pub fn bounds(&self) -> DualAxisBounds { + DualAxisBounds::default() + } + + /// Is the given `input_value` within the exclusion ranges? + #[must_use] + #[inline] + pub fn within_exclusion(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.deadzone_x.within_exclusion(input_value.x), + self.deadzone_y.within_exclusion(input_value.y), + ) + } + + /// Is the given `input_value` within the bounds? + #[must_use] + #[inline] + pub fn within_bounds(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.deadzone_x.within_bounds(input_value.x), + self.deadzone_y.within_bounds(input_value.y), + ) + } + + /// Is the given `input_value` within the lower live zone? + #[must_use] + #[inline] + pub fn within_livezone_lower(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.deadzone_x.within_livezone_lower(input_value.x), + self.deadzone_y.within_livezone_lower(input_value.y), + ) + } + + /// Is the given `input_value` within the upper live zone? + #[must_use] + #[inline] + pub fn within_livezone_upper(&self, input_value: Vec2) -> BVec2 { + BVec2::new( + self.deadzone_x.within_livezone_upper(input_value.x), + self.deadzone_y.within_livezone_upper(input_value.y), + ) + } + + /// Normalizes input values into the live zone. + #[must_use] + #[inline] + pub fn normalize(&self, input_value: Vec2) -> Vec2 { + Vec2::new( + self.deadzone_x.normalize(input_value.x), + self.deadzone_y.normalize(input_value.y), + ) + } +} + +impl Default for DualAxisDeadZone { + /// Creates a [`DualAxisDeadZone`] that excludes input values within the deadzone `[-0.1, 0.1]` on both axes. + fn default() -> Self { + AxisDeadZone::default().extend_dual() + } +} + +impl From for DualAxisProcessor { + fn from(value: DualAxisDeadZone) -> Self { + Self::DeadZone(value) + } +} + +impl AxisDeadZone { + /// Creates a [`DualAxisDeadZone`] using `self` for both axes. + #[inline] + pub const fn extend_dual(self) -> DualAxisDeadZone { + DualAxisDeadZone { + deadzone_x: self, + deadzone_y: self, + } + } + + /// Creates a [`DualAxisDeadZone`] only using `self` for the X-axis. + #[inline] + pub const fn extend_dual_only_x(self) -> DualAxisDeadZone { + DualAxisDeadZone { + deadzone_x: self, + ..DualAxisDeadZone::ZERO + } + } + + /// Creates a [`DualAxisDeadZone`] only using `self` to the Y-axis. + #[inline] + pub const fn extend_dual_only_y(self) -> DualAxisDeadZone { + DualAxisDeadZone { + deadzone_y: self, + ..DualAxisDeadZone::ZERO + } + } + + /// Creates a [`DualAxisDeadZone`] using `self` to the Y-axis with the given `bounds_x` to the X-axis. + #[inline] + pub const fn extend_dual_with_x(self, deadzone_x: Self) -> DualAxisDeadZone { + DualAxisDeadZone { + deadzone_x, + deadzone_y: self, + } + } + + /// Creates a [`DualAxisDeadZone`] using `self` to the X-axis with the given `bounds_y` to the Y-axis. + #[inline] + pub const fn extend_dual_with_y(self, deadzone_y: Self) -> DualAxisDeadZone { + DualAxisDeadZone { + deadzone_x: self, + deadzone_y, + } + } +} + +impl From for DualAxisProcessor { + fn from(deadzone: AxisDeadZone) -> Self { + Self::DeadZone(deadzone.extend_dual()) + } +} + +impl From for DualAxisDeadZone { + fn from(exclusion: DualAxisExclusion) -> Self { + Self::new( + exclusion.exclusion_x.min_max(), + exclusion.exclusion_y.min_max(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dual_axis_value_bounds() { + fn test_bounds( + bounds: DualAxisBounds, + (x_min, x_max): (f32, f32), + (y_min, y_max): (f32, f32), + ) { + assert_eq!(bounds.bounds_x().min_max(), (x_min, x_max)); + assert_eq!(bounds.bounds_y().min_max(), (y_min, y_max)); + + let bounds_x = AxisBounds::new(x_min, x_max); + let bounds_y = AxisBounds::new(y_min, y_max); + assert_eq!(bounds_x.extend_dual_with_y(bounds_y), bounds); + assert_eq!(bounds_y.extend_dual_with_x(bounds_x), bounds); + + let (bx, by) = bounds.bounds(); + assert_eq!(bx, bounds_x); + assert_eq!(by, bounds_y); + + assert_eq!( + DualAxisProcessor::from(bounds_x), + DualAxisProcessor::ValueBounds(DualAxisBounds::all(x_min, x_max)) + ); + + let processor = DualAxisProcessor::ValueBounds(bounds); + assert_eq!(DualAxisProcessor::from(bounds), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), bounds.clamp(value)); + + let expected = BVec2::new(bounds_x.contains(x), bounds_y.contains(y)); + assert_eq!(bounds.contains(value), expected); + + let expected = Vec2::new(bounds_x.clamp(x), bounds_y.clamp(y)); + assert_eq!(bounds.clamp(value), expected); + } + } + } + + let full_range = (f32::MIN, f32::MAX); + + let bounds = DualAxisBounds::FULL_RANGE; + test_bounds(bounds, full_range, full_range); + + let bounds = DualAxisBounds::default(); + test_bounds(bounds, (-1.0, 1.0), (-1.0, 1.0)); + + let bounds = DualAxisBounds::new((-2.0, 2.5), (-1.0, 1.5)); + test_bounds(bounds, (-2.0, 2.5), (-1.0, 1.5)); + + let bounds = DualAxisBounds::all(-2.0, 2.5); + test_bounds(bounds, (-2.0, 2.5), (-2.0, 2.5)); + + let bounds = DualAxisBounds::only_x(-2.0, 2.5); + test_bounds(bounds, (-2.0, 2.5), full_range); + + let bounds = DualAxisBounds::only_y(-1.0, 1.5); + test_bounds(bounds, full_range, (-1.0, 1.5)); + + let bounds = DualAxisBounds::magnitude(2.0, 2.5); + test_bounds(bounds, (-2.0, 2.0), (-2.5, 2.5)); + + let bounds = DualAxisBounds::magnitude_all(2.5); + test_bounds(bounds, (-2.5, 2.5), (-2.5, 2.5)); + + let bounds = DualAxisBounds::magnitude_only_x(2.5); + test_bounds(bounds, (-2.5, 2.5), full_range); + + let bounds = DualAxisBounds::magnitude_only_y(2.5); + test_bounds(bounds, full_range, (-2.5, 2.5)); + + let bounds = DualAxisBounds::at_least(2.0, 2.5); + test_bounds(bounds, (2.0, f32::MAX), (2.5, f32::MAX)); + + let bounds = DualAxisBounds::at_least_all(2.5); + test_bounds(bounds, (2.5, f32::MAX), (2.5, f32::MAX)); + + let bounds = DualAxisBounds::at_least_only_x(2.5); + test_bounds(bounds, (2.5, f32::MAX), full_range); + + let bounds = DualAxisBounds::at_least_only_y(2.5); + test_bounds(bounds, full_range, (2.5, f32::MAX)); + + let bounds = DualAxisBounds::at_most(2.0, 2.5); + test_bounds(bounds, (f32::MIN, 2.0), (f32::MIN, 2.5)); + + let bounds = DualAxisBounds::at_most_all(2.5); + test_bounds(bounds, (f32::MIN, 2.5), (f32::MIN, 2.5)); + + let bounds = DualAxisBounds::at_most_only_x(2.5); + test_bounds(bounds, (f32::MIN, 2.5), full_range); + + let bounds = DualAxisBounds::at_most_only_y(2.5); + test_bounds(bounds, full_range, (f32::MIN, 2.5)); + + let bounds_x = AxisBounds::new(-2.0, 2.5); + let bounds_y = AxisBounds::new(-1.0, 1.5); + + test_bounds(bounds_x.extend_dual(), (-2.0, 2.5), (-2.0, 2.5)); + + test_bounds(bounds_y.extend_dual(), (-1.0, 1.5), (-1.0, 1.5)); + + test_bounds(bounds_x.extend_dual_only_x(), (-2.0, 2.5), full_range); + + test_bounds(bounds_y.extend_dual_only_y(), full_range, (-1.0, 1.5)); + + test_bounds( + bounds_x.extend_dual_with_y(bounds_y), + (-2.0, 2.5), + (-1.0, 1.5), + ); + + test_bounds( + bounds_y.extend_dual_with_x(bounds_x), + (-2.0, 2.5), + (-1.0, 1.5), + ); + } + + #[test] + fn test_dual_axis_exclusion() { + fn test_exclusion( + exclusion: DualAxisExclusion, + (x_negative_max, x_positive_min): (f32, f32), + (y_negative_max, y_positive_min): (f32, f32), + ) { + assert_eq!( + exclusion.exclusion_x.min_max(), + (x_negative_max, x_positive_min) + ); + assert_eq!( + exclusion.exclusion_y.min_max(), + (y_negative_max, y_positive_min) + ); + + let exclusion_x = AxisExclusion::new(x_negative_max, x_positive_min); + let exclusion_y = AxisExclusion::new(y_negative_max, y_positive_min); + assert_eq!(exclusion_x.extend_dual_with_y(exclusion_y), exclusion); + + let (ex, ey) = exclusion.exclusions(); + assert_eq!(ex, exclusion_x); + assert_eq!(ey, exclusion_y); + + assert_eq!( + DualAxisProcessor::from(exclusion_x), + DualAxisProcessor::Exclusion(DualAxisExclusion::all( + x_negative_max, + x_positive_min + )) + ); + + let processor = DualAxisProcessor::Exclusion(exclusion); + assert_eq!(DualAxisProcessor::from(exclusion), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), exclusion.exclude(value)); + + assert_eq!( + exclusion.contains(value), + BVec2::new(exclusion_x.contains(x), exclusion_y.contains(y)) + ); + + assert_eq!( + exclusion.exclude(value), + Vec2::new(exclusion_x.exclude(x), exclusion_y.exclude(y)) + ); + } + } + } + + let zero_size = (0.0, 0.0); + + let exclusion = DualAxisExclusion::ZERO; + test_exclusion(exclusion, zero_size, zero_size); + + let exclusion = DualAxisExclusion::default(); + test_exclusion(exclusion, (-0.1, 0.1), (-0.1, 0.1)); + + let exclusion = DualAxisExclusion::new((-0.2, 0.3), (-0.1, 0.4)); + test_exclusion(exclusion, (-0.2, 0.3), (-0.1, 0.4)); + + let exclusion = DualAxisExclusion::all(-0.2, 0.3); + test_exclusion(exclusion, (-0.2, 0.3), (-0.2, 0.3)); + + let exclusion = DualAxisExclusion::only_x(-0.2, 0.3); + test_exclusion(exclusion, (-0.2, 0.3), zero_size); + + let exclusion = DualAxisExclusion::only_y(-0.1, 0.4); + test_exclusion(exclusion, zero_size, (-0.1, 0.4)); + + let exclusion = DualAxisExclusion::magnitude(0.2, 0.3); + test_exclusion(exclusion, (-0.2, 0.2), (-0.3, 0.3)); + + let exclusion = DualAxisExclusion::magnitude_all(0.3); + test_exclusion(exclusion, (-0.3, 0.3), (-0.3, 0.3)); + + let exclusion = DualAxisExclusion::magnitude_only_x(0.3); + test_exclusion(exclusion, (-0.3, 0.3), zero_size); + + let exclusion = DualAxisExclusion::magnitude_only_y(0.3); + test_exclusion(exclusion, zero_size, (-0.3, 0.3)); + + let exclusion_x = AxisExclusion::new(-0.2, 0.3); + let exclusion_y = AxisExclusion::new(-0.1, 0.4); + + test_exclusion(exclusion_x.extend_dual(), (-0.2, 0.3), (-0.2, 0.3)); + + test_exclusion(exclusion_y.extend_dual(), (-0.1, 0.4), (-0.1, 0.4)); + + test_exclusion(exclusion_x.extend_dual_only_x(), (-0.2, 0.3), zero_size); + + test_exclusion(exclusion_y.extend_dual_only_y(), zero_size, (-0.1, 0.4)); + + test_exclusion( + exclusion_x.extend_dual_with_y(exclusion_y), + (-0.2, 0.3), + (-0.1, 0.4), + ); + + test_exclusion( + exclusion_y.extend_dual_with_x(exclusion_x), + (-0.2, 0.3), + (-0.1, 0.4), + ); + } + + #[test] + fn test_dual_axis_deadzone() { + fn test_deadzone( + deadzone: DualAxisDeadZone, + (x_negative_max, x_positive_min): (f32, f32), + (y_negative_max, y_positive_min): (f32, f32), + ) { + assert_eq!( + deadzone.deadzone_x.exclusion().min_max(), + (x_negative_max, x_positive_min) + ); + assert_eq!( + deadzone.deadzone_y.exclusion().min_max(), + (y_negative_max, y_positive_min) + ); + + let deadzone_x = AxisDeadZone::new(x_negative_max, x_positive_min); + let deadzone_y = AxisDeadZone::new(y_negative_max, y_positive_min); + assert_eq!(deadzone_x.extend_dual_with_y(deadzone_y), deadzone); + + let exclusion = DualAxisExclusion::new( + (x_negative_max, x_positive_min), + (y_negative_max, y_positive_min), + ); + assert_eq!(exclusion.scaled(), deadzone); + + let (dx, dy) = deadzone.deadzones(); + assert_eq!(dx, deadzone_x); + assert_eq!(dy, deadzone_y); + + assert_eq!( + DualAxisProcessor::from(deadzone_x), + DualAxisProcessor::DeadZone(DualAxisDeadZone::all(x_negative_max, x_positive_min)) + ); + + let processor = DualAxisProcessor::DeadZone(deadzone); + assert_eq!(DualAxisProcessor::from(deadzone), processor); + + for x in -300..300 { + let x = x as f32 * 0.01; + for y in -300..300 { + let y = y as f32 * 0.01; + let value = Vec2::new(x, y); + + assert_eq!(processor.process(value), deadzone.normalize(value)); + + assert_eq!( + deadzone.within_exclusion(value), + BVec2::new( + deadzone_x.within_exclusion(x), + deadzone_y.within_exclusion(y) + ) + ); + + assert_eq!( + deadzone.within_bounds(value), + BVec2::new(deadzone_x.within_bounds(x), deadzone_y.within_bounds(y)) + ); + + assert_eq!( + deadzone.within_livezone_lower(value), + BVec2::new( + deadzone_x.within_livezone_lower(x), + deadzone_y.within_livezone_lower(y) + ) + ); + + assert_eq!( + deadzone.within_livezone_upper(value), + BVec2::new( + deadzone_x.within_livezone_upper(x), + deadzone_y.within_livezone_upper(y) + ) + ); + + assert_eq!( + deadzone.normalize(value), + Vec2::new(deadzone_x.normalize(x), deadzone_y.normalize(y)) + ); + } + } + } + + let zero_size = (0.0, 0.0); + + let deadzone = DualAxisDeadZone::ZERO; + test_deadzone(deadzone, zero_size, zero_size); + + let deadzone = DualAxisDeadZone::default(); + test_deadzone(deadzone, (-0.1, 0.1), (-0.1, 0.1)); + + let deadzone = DualAxisDeadZone::new((-0.2, 0.3), (-0.1, 0.4)); + test_deadzone(deadzone, (-0.2, 0.3), (-0.1, 0.4)); + + let deadzone = DualAxisDeadZone::all(-0.2, 0.3); + test_deadzone(deadzone, (-0.2, 0.3), (-0.2, 0.3)); + + let deadzone = DualAxisDeadZone::only_x(-0.2, 0.3); + test_deadzone(deadzone, (-0.2, 0.3), zero_size); + + let deadzone = DualAxisDeadZone::only_y(-0.1, 0.4); + test_deadzone(deadzone, zero_size, (-0.1, 0.4)); + + let deadzone = DualAxisDeadZone::magnitude(0.2, 0.3); + test_deadzone(deadzone, (-0.2, 0.2), (-0.3, 0.3)); + + let deadzone = DualAxisDeadZone::magnitude_all(0.3); + test_deadzone(deadzone, (-0.3, 0.3), (-0.3, 0.3)); + + let deadzone = DualAxisDeadZone::magnitude_only_x(0.3); + test_deadzone(deadzone, (-0.3, 0.3), zero_size); + + let deadzone = DualAxisDeadZone::magnitude_only_y(0.3); + test_deadzone(deadzone, zero_size, (-0.3, 0.3)); + + let deadzone_x = AxisDeadZone::new(-0.2, 0.3); + let deadzone_y = AxisDeadZone::new(-0.1, 0.4); + + test_deadzone(deadzone_x.extend_dual(), (-0.2, 0.3), (-0.2, 0.3)); + + test_deadzone(deadzone_y.extend_dual(), (-0.1, 0.4), (-0.1, 0.4)); + + test_deadzone(deadzone_x.extend_dual_only_x(), (-0.2, 0.3), zero_size); + + test_deadzone(deadzone_y.extend_dual_only_y(), zero_size, (-0.1, 0.4)); + + test_deadzone( + deadzone_x.extend_dual_with_y(deadzone_y), + (-0.2, 0.3), + (-0.1, 0.4), + ); + + test_deadzone( + deadzone_y.extend_dual_with_x(deadzone_x), + (-0.2, 0.3), + (-0.1, 0.4), + ); + } +} diff --git a/src/input_processing/mod.rs b/src/input_processing/mod.rs new file mode 100644 index 00000000..6c4e666a --- /dev/null +++ b/src/input_processing/mod.rs @@ -0,0 +1,94 @@ +//! Processors for input values +//! +//! This module simplifies input handling in your application by providing processors +//! for refining and manipulating values before reaching the application logic. +//! +//! The foundation of this module lies in these enums. +//! +//! - [`AxisProcessor`]: Handles `f32` values for single-axis inputs. +//! - [`DualAxisProcessor`]: Handles [`Vec2`](bevy::prelude::Vec2) values for dual-axis inputs. +//! +//! Need something specific? You can also create your own processors by implementing these traits for specific needs. +//! +//! - [`CustomAxisProcessor`]: Handles `f32` values for single-axis inputs. +//! - [`CustomDualAxisProcessor`]: Handles [`Vec2`](bevy::prelude::Vec2) values for dual-axis inputs. +//! +//! Feel free to suggest additions to the built-in processors if you have a common use case! +//! +//! # Built-in Processors +//! +//! ## Pipelines +//! +//! Pipelines handle input values sequentially through a sequence of processors. +//! +//! - [`AxisProcessor::Pipeline`]: Pipeline for single-axis inputs. +//! - [`DualAxisProcessor::Pipeline`]: Pipeline for dual-axis inputs. +//! +//! You can also use these methods to create a pipeline. +//! +//! - [`AxisProcessor::with_processor`] or [`From>::from`] for [`AxisProcessor::Pipeline`]. +//! - [`DualAxisProcessor::with_processor`] or [`From>::from`] for [`DualAxisProcessor::Pipeline`]. +//! +//! ## Inversion +//! +//! Inversion flips the sign of input values, resulting in a directional reversal of control. +//! For example, positive values become negative, and up becomes down. +//! +//! - [`AxisProcessor::Inverted`]: Single-axis inversion. +//! - [`DualAxisInverted`]: Dual-axis inversion, implemented [`Into`]. +//! +//! ## Sensitivity +//! +//! Sensitivity scales input values with a specified multiplier (doubling, halving, etc.), +//! allowing fine-tuning the responsiveness of controls. +//! +//! - [`AxisProcessor::Sensitivity`]: Single-axis scaling. +//! - [`DualAxisSensitivity`]: Dual-axis scaling, implemented [`Into`]. +//! +//! ## Value Bounds +//! +//! Value bounds define an acceptable range for input values, +//! clamping out-of-bounds inputs to the nearest valid value and leaving others as is +//! to avoid unexpected behavior caused by extreme inputs. +//! +//! - [`AxisBounds`]: A min-max range for valid single-axis inputs, +//! implemented [`Into`] and [`Into`]. +//! - [`DualAxisBounds`]: A square-shaped region for valid dual-axis inputs, +//! with independent min-max ranges for each axis, implemented [`Into`]. +//! - [`CircleBounds`]: A circular region for valid dual-axis inputs, +//! with a radius defining the maximum magnitude, implemented [`Into`]. +//! +//! ## Dead Zones +//! +//! ### Unscaled Versions +//! +//! Unscaled dead zones specify regions where input values within the regions +//! are considered excluded from further processing and treated as zeros, +//! helping filter out minor fluctuations and unintended movements. +//! +//! - [`AxisExclusion`]: A min-max range for excluding single-axis input values, +//! implemented [`Into`] and [`Into`]. +//! - [`DualAxisExclusion`]: A cross-shaped region for excluding dual-axis inputs, +//! with independent min-max ranges for each axis, implemented [`Into`]. +//! - [`CircleExclusion`]: A circular region for excluding dual-axis inputs, +//! with a radius defining the maximum excluded magnitude, implemented [`Into`]. +//! +//! ### Scaled Versions +//! +//! Scaled dead zones transform input values by restricting values within the default bounds, +//! and then scaling non-excluded values linearly into the "live zone", +//! the remaining region within the bounds after dead zone exclusion. +//! +//! - [`AxisDeadZone`]: A scaled version of [`AxisExclusion`] with the bounds +//! set to [`AxisBounds::magnitude(1.0)`](AxisBounds::default), +//! implemented [`Into`] and [`Into`]. +//! - [`DualAxisDeadZone`]: A scaled version of [`DualAxisExclusion`] with the bounds +//! set to [`DualAxisBounds::magnitude_all(1.0)`](DualAxisBounds::default), implemented [`Into`]. +//! - [`CircleDeadZone`]: A scaled version of [`CircleExclusion`] with the bounds +//! set to [`CircleBounds::new(1.0)`](CircleBounds::default), implemented [`Into`]. + +pub use self::dual_axis::*; +pub use self::single_axis::*; + +pub mod dual_axis; +pub mod single_axis; diff --git a/src/input_processing/single_axis/custom.rs b/src/input_processing/single_axis/custom.rs new file mode 100644 index 00000000..d7c76197 --- /dev/null +++ b/src/input_processing/single_axis/custom.rs @@ -0,0 +1,344 @@ +use std::any::{Any, TypeId}; +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::RwLock; + +use bevy::app::App; +use bevy::prelude::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize, TypePath}; +use bevy::reflect::utility::{reflect_hasher, GenericTypePathCell, NonGenericTypeInfoCell}; +use bevy::reflect::{ + erased_serde, FromType, GetTypeRegistration, ReflectFromPtr, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypeRegistration, Typed, ValueInfo, +}; +use dyn_clone::DynClone; +use dyn_eq::DynEq; +use dyn_hash::DynHash; +use once_cell::sync::Lazy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_flexitos::ser::require_erased_serialize_impl; +use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; + +use crate::input_processing::AxisProcessor; +use crate::typetag::RegisterTypeTag; + +/// A trait for creating custom processor that handles single-axis input values, +/// accepting a `f32` input and producing a `f32` output. +/// +/// # Examples +/// +/// ```rust +/// use std::hash::{Hash, Hasher}; +/// use bevy::prelude::*; +/// use bevy::utils::FloatOrd; +/// use serde::{Deserialize, Serialize}; +/// use leafwing_input_manager::prelude::*; +/// +/// /// Doubles the input, takes the absolute value, +/// /// and discards results that meet the specified condition. +/// // If your processor includes fields not implemented Eq and Hash, +/// // implementation is necessary as shown below. +/// // Otherwise, you can derive Eq and Hash directly. +/// #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +/// pub struct DoubleAbsoluteValueThenIgnored(pub f32); +/// +/// // Add this attribute for ensuring proper serialization and deserialization. +/// #[serde_typetag] +/// impl CustomAxisProcessor for DoubleAbsoluteValueThenIgnored { +/// fn process(&self, input_value: f32) -> f32 { +/// // Implement the logic just like you would in a normal function. +/// +/// // You can use other processors within this function. +/// let value = AxisProcessor::Sensitivity(2.0).process(input_value); +/// +/// let value = value.abs(); +/// if value == self.0 { +/// 0.0 +/// } else { +/// value +/// } +/// } +/// } +/// +/// // Unfortunately, manual implementation is required due to the float field. +/// impl Eq for DoubleAbsoluteValueThenIgnored {} +/// impl Hash for DoubleAbsoluteValueThenIgnored { +/// fn hash(&self, state: &mut H) { +/// // Encapsulate the float field for hashing. +/// FloatOrd(self.0).hash(state); +/// } +/// } +/// +/// // Remember to register your processor - it will ensure everything works smoothly! +/// let mut app = App::new(); +/// app.register_axis_processor::(); +/// +/// // Now you can use it! +/// let processor = DoubleAbsoluteValueThenIgnored(4.0); +/// +/// // Rejected! +/// assert_eq!(processor.process(2.0), 0.0); +/// assert_eq!(processor.process(-2.0), 0.0); +/// +/// // Others are just doubled absolute value. +/// assert_eq!(processor.process(6.0), 12.0); +/// assert_eq!(processor.process(4.0), 8.0); +/// assert_eq!(processor.process(0.0), 0.0); +/// assert_eq!(processor.process(-4.0), 8.0); +/// assert_eq!(processor.process(-6.0), 12.0); +/// +/// // The ways to create an AxisProcessor. +/// let axis_processor = AxisProcessor::Custom(Box::new(processor)); +/// assert_eq!(axis_processor, AxisProcessor::from(processor)); +/// ``` +pub trait CustomAxisProcessor: + Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize +{ + /// Computes the result by processing the `input_value`. + fn process(&self, input_value: f32) -> f32; +} + +impl From

for AxisProcessor { + fn from(value: P) -> Self { + Self::Custom(Box::new(value)) + } +} + +dyn_clone::clone_trait_object!(CustomAxisProcessor); +dyn_eq::eq_trait_object!(CustomAxisProcessor); +dyn_hash::hash_trait_object!(CustomAxisProcessor); + +impl Reflect for Box { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(Self::type_info()) + } + + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn apply(&mut self, value: &dyn Reflect) { + let value = value.as_any(); + if let Some(value) = value.downcast_ref::() { + *self = value.clone(); + } else { + panic!( + "Value is not a std::boxed::Box.", + module_path!(), + ); + } + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Value + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Value(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Value(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Value(self) + } + + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + let type_id = TypeId::of::(); + Hash::hash(&type_id, &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + value + .as_any() + .downcast_ref::() + .map(|value| self.dyn_eq(value)) + .or(Some(false)) + } + + fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + +impl Typed for Box { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) + } +} + +impl TypePath for Box { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + { + format!( + "std::boxed::Box", + module_path!() + ) + } + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| "Box".to_string()) + } + + fn type_ident() -> Option<&'static str> { + Some("Box") + } + + fn crate_name() -> Option<&'static str> { + Some(module_path!().split(':').next().unwrap()) + } + + fn module_path() -> Option<&'static str> { + Some(module_path!()) + } +} + +impl GetTypeRegistration for Box { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for Box { + fn from_reflect(reflect: &dyn Reflect) -> Option { + Some(reflect.as_any().downcast_ref::()?.clone()) + } +} + +impl<'a> Serialize for dyn CustomAxisProcessor + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Check that `CustomAxisProcessor` has `erased_serde::Serialize` as a super trait, + // preventing infinite recursion at runtime. + const fn __check_erased_serialize_super_trait() { + require_erased_serialize_impl::(); + } + serialize_trait_object(serializer, self.reflect_short_type_path(), self) + } +} + +impl<'de> Deserialize<'de> for Box { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let registry = unsafe { PROCESSOR_REGISTRY.read().unwrap() }; + registry.deserialize_trait_object(deserializer) + } +} + +/// Registry of deserializers for [`CustomAxisProcessor`]s. +static mut PROCESSOR_REGISTRY: Lazy>> = + Lazy::new(|| RwLock::new(MapRegistry::new("CustomAxisProcessor"))); + +/// A trait for registering a specific [`CustomAxisProcessor`]. +pub trait RegisterCustomAxisProcessor { + /// Registers the specified [`CustomAxisProcessor`]. + fn register_axis_processor<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn CustomAxisProcessor> + GetTypeRegistration; +} + +impl RegisterCustomAxisProcessor for App { + fn register_axis_processor<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn CustomAxisProcessor> + GetTypeRegistration, + { + let mut registry = unsafe { PROCESSOR_REGISTRY.write().unwrap() }; + T::register_typetag(&mut registry); + self.register_type::(); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as leafwing_input_manager; + use leafwing_input_manager_macros::serde_typetag; + use serde_test::{assert_tokens, Token}; + + #[test] + fn test_custom_axis_processor() { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] + struct CustomAxisInverted; + + #[serde_typetag] + impl CustomAxisProcessor for CustomAxisInverted { + fn process(&self, input_value: f32) -> f32 { + -input_value + } + } + + let mut app = App::new(); + app.register_axis_processor::(); + + let custom: Box = Box::new(CustomAxisInverted); + assert_tokens( + &custom, + &[ + Token::Map { len: Some(1) }, + Token::BorrowedStr("CustomAxisInverted"), + Token::UnitStruct { + name: "CustomAxisInverted", + }, + Token::MapEnd, + ], + ); + + let processor = AxisProcessor::Custom(custom); + assert_eq!(AxisProcessor::from(CustomAxisInverted), processor); + + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(processor.process(value), -value); + assert_eq!(CustomAxisInverted.process(value), -value); + } + } +} diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs new file mode 100644 index 00000000..79ef51b8 --- /dev/null +++ b/src/input_processing/single_axis/mod.rs @@ -0,0 +1,230 @@ +//! Processors for single-axis input values + +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use bevy::prelude::Reflect; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +pub use self::custom::*; +pub use self::range::*; + +mod custom; +mod range; + +/// A processor for single-axis input values, +/// accepting a `f32` input and producing a `f32` output. +#[must_use] +#[non_exhaustive] +#[derive(Default, Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] +pub enum AxisProcessor { + /// No processor is applied. + #[default] + None, + + /// Flips the sign of input values, resulting in a directional reversal of control. + /// + /// ```rust + /// use leafwing_input_manager::prelude::*; + /// + /// assert_eq!(AxisProcessor::Inverted.process(2.5), -2.5); + /// assert_eq!(AxisProcessor::Inverted.process(-2.5), 2.5); + /// ``` + Inverted, + + /// Scales input values using a specified multiplier to fine-tune the responsiveness of control. + /// + /// ```rust + /// use leafwing_input_manager::prelude::*; + /// + /// // Doubled! + /// assert_eq!(AxisProcessor::Sensitivity(2.0).process(2.0), 4.0); + /// + /// // Halved! + /// assert_eq!(AxisProcessor::Sensitivity(0.5).process(2.0), 1.0); + /// + /// // Negated and halved! + /// assert_eq!(AxisProcessor::Sensitivity(-0.5).process(2.0), -1.0); + /// ``` + Sensitivity(f32), + + /// A wrapper around [`AxisBounds`] to represent value bounds. + ValueBounds(AxisBounds), + + /// A wrapper around [`AxisExclusion`] to represent unscaled deadzone. + Exclusion(AxisExclusion), + + /// A wrapper around [`AxisDeadZone`] to represent scaled deadzone. + DeadZone(AxisDeadZone), + + /// Processes input values sequentially through a sequence of [`AxisProcessor`]s. + /// + /// For a straightforward creation of a [`AxisProcessor::Pipeline`], + /// you can use [`AxisProcessor::with_processor`] or [`From>::from`] methods. + /// + /// ```rust + /// use std::sync::Arc; + /// use leafwing_input_manager::prelude::*; + /// + /// let expected = AxisProcessor::Pipeline(vec![ + /// Arc::new(AxisProcessor::Inverted), + /// Arc::new(AxisProcessor::Sensitivity(2.0)), + /// ]); + /// + /// assert_eq!( + /// expected, + /// AxisProcessor::Inverted.with_processor(AxisProcessor::Sensitivity(2.0)) + /// ); + /// + /// assert_eq!( + /// expected, + /// AxisProcessor::from(vec![ + /// AxisProcessor::Inverted, + /// AxisProcessor::Sensitivity(2.0), + /// ]) + /// ); + /// ``` + Pipeline(Vec>), + + /// A user-defined processor that implements [`CustomAxisProcessor`]. + Custom(Box), +} + +impl AxisProcessor { + /// Computes the result by processing the `input_value`. + #[must_use] + #[inline] + pub fn process(&self, input_value: f32) -> f32 { + match self { + Self::None => input_value, + Self::Inverted => -input_value, + Self::Sensitivity(sensitivity) => sensitivity * input_value, + Self::ValueBounds(bounds) => bounds.clamp(input_value), + Self::Exclusion(exclusion) => exclusion.exclude(input_value), + Self::DeadZone(deadzone) => deadzone.normalize(input_value), + Self::Pipeline(sequence) => sequence + .iter() + .fold(input_value, |value, next| next.process(value)), + Self::Custom(processor) => processor.process(input_value), + } + } + + /// Appends the given `next_processor` as the next processing step. + /// + /// - If either processor is [`AxisProcessor::None`], returns the other. + /// - If the current processor is [`AxisProcessor::Pipeline`], pushes the other into it. + /// - If the given processor is [`AxisProcessor::Pipeline`], prepends the current one into it. + /// - If both processors are [`AxisProcessor::Pipeline`], merges the two pipelines. + /// - If neither processor is [`AxisProcessor::None`] nor a pipeline, + /// creates a new pipeline containing them. + #[inline] + pub fn with_processor(self, next_processor: impl Into) -> Self { + let other = next_processor.into(); + match (self.clone(), other.clone()) { + (_, Self::None) => self, + (Self::None, _) => other, + (Self::Pipeline(mut self_seq), Self::Pipeline(mut next_seq)) => { + self_seq.append(&mut next_seq); + Self::Pipeline(self_seq) + } + (Self::Pipeline(mut self_seq), _) => { + self_seq.push(Arc::new(other)); + Self::Pipeline(self_seq) + } + (_, Self::Pipeline(mut next_seq)) => { + next_seq.insert(0, Arc::new(self)); + Self::Pipeline(next_seq) + } + (_, _) => Self::Pipeline(vec![Arc::new(self), Arc::new(other)]), + } + } +} + +impl From> for AxisProcessor { + fn from(value: Vec) -> Self { + Self::Pipeline(value.into_iter().map(Arc::new).collect()) + } +} + +impl Eq for AxisProcessor {} + +impl Hash for AxisProcessor { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::None => {} + Self::Inverted => {} + Self::Sensitivity(sensitivity) => FloatOrd(*sensitivity).hash(state), + Self::ValueBounds(bounds) => bounds.hash(state), + Self::Exclusion(exclusion) => exclusion.hash(state), + Self::DeadZone(deadzone) => deadzone.hash(state), + Self::Pipeline(sequence) => sequence.hash(state), + Self::Custom(processor) => processor.hash(state), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_axis_inversion_processor() { + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(AxisProcessor::Inverted.process(value), -value); + assert_eq!(AxisProcessor::Inverted.process(-value), value); + } + } + + #[test] + fn test_axis_sensitivity_processor() { + for value in -300..300 { + let value = value as f32 * 0.01; + + for sensitivity in -300..300 { + let sensitivity = sensitivity as f32 * 0.01; + + let processor = AxisProcessor::Sensitivity(sensitivity); + assert_eq!(processor.process(value), sensitivity * value); + } + } + } + + #[test] + fn test_axis_processor_pipeline() { + let pipeline = AxisProcessor::Pipeline(vec![ + Arc::new(AxisProcessor::Inverted), + Arc::new(AxisProcessor::Sensitivity(2.0)), + ]); + + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(pipeline.process(value), value * -2.0); + } + } + + #[test] + fn test_axis_processor_from_list() { + assert_eq!(AxisProcessor::from(vec![]), AxisProcessor::Pipeline(vec![])); + + assert_eq!( + AxisProcessor::from(vec![AxisProcessor::Inverted]), + AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), + ); + + assert_eq!( + AxisProcessor::from(vec![ + AxisProcessor::Inverted, + AxisProcessor::Sensitivity(2.0), + ]), + AxisProcessor::Pipeline(vec![ + Arc::new(AxisProcessor::Inverted), + Arc::new(AxisProcessor::Sensitivity(2.0)), + ]) + ); + } +} diff --git a/src/input_processing/single_axis/range.rs b/src/input_processing/single_axis/range.rs new file mode 100644 index 00000000..ce686a43 --- /dev/null +++ b/src/input_processing/single_axis/range.rs @@ -0,0 +1,679 @@ +//! Range processors for single-axis inputs + +use std::hash::{Hash, Hasher}; + +use bevy::prelude::Reflect; +use bevy::utils::FloatOrd; +use serde::{Deserialize, Serialize}; + +use super::AxisProcessor; + +/// Specifies an acceptable min-max range for valid single-axis inputs, +/// restricting all value stays within intended limits +/// to avoid unexpected behavior caused by extreme inputs. +/// +/// ```rust +/// use leafwing_input_manager::prelude::*; +/// +/// // Restrict values to [-2.0, 1.5]. +/// let bounds = AxisBounds::new(-2.0, 1.5); +/// +/// // The ways to create an AxisProcessor. +/// let processor = AxisProcessor::from(bounds); +/// assert_eq!(processor, AxisProcessor::ValueBounds(bounds)); +/// +/// for value in -300..300 { +/// let value = value as f32 * 0.01; +/// assert_eq!(bounds.clamp(value), value.clamp(-2.0, 1.5)); +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct AxisBounds { + /// The minimum value of valid inputs. + pub(crate) min: f32, + + /// The maximum value of valid inputs. + pub(crate) max: f32, +} + +impl AxisBounds { + /// Unlimited [`AxisBounds`]. + pub const FULL_RANGE: Self = Self { + min: f32::MIN, + max: f32::MAX, + }; + + /// Creates an [`AxisBounds`] that restricts values to the given range `[min, max]`. + /// + /// # Requirements + /// + /// - `min` <= `max`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new(min: f32, max: f32) -> Self { + // PartialOrd for f32 ensures that NaN values are checked during comparisons. + assert!(min <= max); + Self { min, max } + } + + /// Creates an [`AxisBounds`] that restricts values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold: f32) -> Self { + Self::new(-threshold, threshold) + } + + /// Creates an [`AxisBounds`] that restricts values to a minimum value. + #[inline] + pub const fn at_least(min: f32) -> Self { + Self { + min, + ..Self::FULL_RANGE + } + } + + /// Creates an [`AxisBounds`] that restricts values to a maximum value. + #[inline] + pub const fn at_most(max: f32) -> Self { + Self { + max, + ..Self::FULL_RANGE + } + } + + /// Returns the minimum and maximum bounds. + #[must_use] + #[inline] + pub fn min_max(&self) -> (f32, f32) { + (self.min(), self.max()) + } + + /// Returns the minimum bound. + #[must_use] + #[inline] + pub fn min(&self) -> f32 { + self.min + } + + /// Returns the maximum bound. + #[must_use] + #[inline] + pub fn max(&self) -> f32 { + self.max + } + + /// Is the given `input_value` within the bounds? + #[must_use] + #[inline] + pub fn contains(&self, input_value: f32) -> bool { + self.min <= input_value && input_value <= self.max + } + + /// Clamps `input_value` within the bounds. + #[must_use] + #[inline] + pub fn clamp(&self, input_value: f32) -> f32 { + // clamp() includes checks if either bound is set to NaN, + // but the constructors guarantee that all bounds will not be NaN. + input_value.min(self.max).max(self.min) + } +} + +impl Default for AxisBounds { + /// Creates an [`AxisBounds`] that restricts values to the range `[-1.0, 1.0]`. + #[inline] + fn default() -> Self { + Self { + min: -1.0, + max: 1.0, + } + } +} + +impl From for AxisProcessor { + fn from(value: AxisBounds) -> Self { + Self::ValueBounds(value) + } +} + +impl Eq for AxisBounds {} + +impl Hash for AxisBounds { + fn hash(&self, state: &mut H) { + FloatOrd(self.min).hash(state); + FloatOrd(self.max).hash(state); + } +} + +/// Specifies an exclusion range for excluding single-axis inputs, +/// helping filter out minor fluctuations and unintended movements. +/// +/// In simple terms, this processor behaves like an [`AxisDeadZone`] without normalization. +/// +/// # Examples +/// +/// ```rust +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude values between -0.2 and 0.3 +/// let exclusion = AxisExclusion::new(-0.2, 0.3); +/// +/// // The ways to create an AxisProcessor. +/// let processor = AxisProcessor::from(exclusion); +/// assert_eq!(processor, AxisProcessor::Exclusion(exclusion)); +/// +/// for value in -300..300 { +/// let value = value as f32 * 0.01; +/// +/// if -0.2 <= value && value <= 0.3 { +/// assert!(exclusion.contains(value)); +/// assert_eq!(exclusion.exclude(value), 0.0); +/// } else { +/// assert!(!exclusion.contains(value)); +/// assert_eq!(exclusion.exclude(value), value); +/// } +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct AxisExclusion { + /// The maximum negative value treated as zero. + pub(crate) negative_max: f32, + + /// The minimum positive value treated as zero. + pub(crate) positive_min: f32, +} + +impl AxisExclusion { + /// Zero-size [`AxisExclusion`], leaving values as is. + pub const ZERO: Self = Self { + negative_max: 0.0, + positive_min: 0.0, + }; + + /// Creates an [`AxisExclusion`] that ignores values within the range `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new(negative_max: f32, positive_min: f32) -> Self { + assert!(negative_max <= 0.0); + assert!(positive_min >= 0.0); + Self { + negative_max, + positive_min, + } + } + + /// Creates an [`AxisExclusion`] that ignores values within the range `[-threshold, threshold]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold: f32) -> Self { + Self::new(-threshold, threshold) + } + + /// Returns the minimum and maximum bounds. + #[must_use] + #[inline] + pub fn min_max(&self) -> (f32, f32) { + (self.negative_max, self.positive_min) + } + + /// Returns the minimum bound. + #[must_use] + #[inline] + pub fn min(&self) -> f32 { + self.negative_max + } + + /// Returns the maximum bounds. + #[must_use] + #[inline] + pub fn max(&self) -> f32 { + self.positive_min + } + + /// Is `input_value` within the deadzone? + #[must_use] + #[inline] + pub fn contains(&self, input_value: f32) -> bool { + self.negative_max <= input_value && input_value <= self.positive_min + } + + /// Excludes values within the specified range. + #[must_use] + #[inline] + pub fn exclude(&self, input_value: f32) -> f32 { + if self.contains(input_value) { + 0.0 + } else { + input_value + } + } + + /// Creates an [`AxisDeadZone`] using `self` as the exclusion range. + #[inline] + pub fn scaled(self) -> AxisDeadZone { + AxisDeadZone::new(self.negative_max, self.positive_min) + } +} + +impl Default for AxisExclusion { + /// Creates an [`AxisExclusion`] that ignores values within the range `[-0.1, 0.1]`. + #[inline] + fn default() -> Self { + Self { + negative_max: -0.1, + positive_min: 0.1, + } + } +} + +impl From for AxisProcessor { + fn from(value: AxisExclusion) -> Self { + Self::Exclusion(value) + } +} + +impl Eq for AxisExclusion {} + +impl Hash for AxisExclusion { + fn hash(&self, state: &mut H) { + FloatOrd(self.negative_max).hash(state); + FloatOrd(self.positive_min).hash(state); + } +} + +/// A scaled version of [`AxisExclusion`] with the bounds +/// set to [`AxisBounds::magnitude(1.0)`](AxisBounds::default) +/// that normalizes non-excluded input values into the "live zone", +/// the remaining range within the bounds after dead zone exclusion. +/// +/// # Examples +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// +/// // Exclude values between -0.2 and 0.3 +/// let deadzone = AxisDeadZone::new(-0.2, 0.3); +/// +/// // Another way to create an AxisDeadzone. +/// let exclusion = AxisExclusion::new(-0.2, 0.3); +/// assert_eq!(exclusion.scaled(), deadzone); +/// +/// // The ways to create an AxisProcessor. +/// let processor = AxisProcessor::from(deadzone); +/// assert_eq!(processor, AxisProcessor::DeadZone(deadzone)); +/// +/// // The bounds after normalization. +/// let bounds = deadzone.bounds(); +/// assert_eq!(bounds.min(), -1.0); +/// assert_eq!(bounds.max(), 1.0); +/// +/// for value in -300..300 { +/// let value = value as f32 * 0.01; +/// +/// // Values within the dead zone are treated as zero. +/// if -0.2 <= value && value <= 0.3 { +/// assert!(deadzone.within_exclusion(value)); +/// assert_eq!(deadzone.normalize(value), 0.0); +/// } +/// +/// // Values within the live zone are scaled linearly. +/// else if -1.0 <= value && value < -0.2 { +/// assert!(deadzone.within_livezone_lower(value)); +/// +/// let expected = f32::inverse_lerp(-1.0, -0.2, value) - 1.0; +/// assert!((deadzone.normalize(value) - expected).abs() <= f32::EPSILON); +/// } else if 0.3 < value && value <= 1.0 { +/// assert!(deadzone.within_livezone_upper(value)); +/// +/// let expected = f32::inverse_lerp(0.3, 1.0, value); +/// assert!((deadzone.normalize(value) - expected).abs() <= f32::EPSILON); +/// } +/// +/// // Values outside the bounds are restricted to the range. +/// else { +/// assert!(!deadzone.within_bounds(value)); +/// assert_eq!(deadzone.normalize(value), value.clamp(-1.0, 1.0)); +/// } +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct AxisDeadZone { + /// The [`AxisExclusion`] used for normalization. + pub(crate) exclusion: AxisExclusion, + + /// Pre-calculated reciprocal of the lower live zone size, + /// preventing division during normalization. + pub(crate) livezone_lower_recip: f32, + + /// Pre-calculated reciprocal of the upper live zone size, + /// preventing division during normalization. + pub(crate) livezone_upper_recip: f32, +} + +impl AxisDeadZone { + /// Zero-size [`AxisDeadZone`], only restricting values to the range `[-1.0, 1.0]`. + pub const ZERO: Self = Self { + exclusion: AxisExclusion::ZERO, + livezone_lower_recip: 1.0, + livezone_upper_recip: 1.0, + }; + + /// Creates an [`AxisDeadZone`] that excludes input values + /// within the given deadzone `[negative_max, positive_min]`. + /// + /// # Requirements + /// + /// - `negative_max` <= `0.0` <= `positive_min`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[inline] + pub fn new(negative_max: f32, positive_min: f32) -> Self { + let (bound_min, bound_max) = AxisBounds::default().min_max(); + Self { + exclusion: AxisExclusion::new(negative_max, positive_min), + livezone_lower_recip: (negative_max - bound_min).recip(), + livezone_upper_recip: (bound_max - positive_min).recip(), + } + } + + /// Creates an [`AxisDeadZone`] that excludes input values below a `threshold` magnitude + /// and then normalizes non-excluded input values into the valid range `[-1.0, 1.0]`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirements aren't met. + #[doc(alias = "symmetric")] + #[inline] + pub fn magnitude(threshold: f32) -> Self { + AxisDeadZone::new(-threshold, threshold) + } + + /// Returns the [`AxisExclusion`] used by this deadzone. + #[inline] + pub fn exclusion(&self) -> AxisExclusion { + self.exclusion + } + + /// Returns the [`AxisBounds`] used by this deadzone. + #[inline] + pub fn bounds(&self) -> AxisBounds { + AxisBounds::default() + } + + /// Returns the minimum and maximum bounds of the lower live zone used for normalization. + /// + /// In simple terms, this returns `(bounds.min, exclusion.min)`. + #[must_use] + #[inline] + pub fn livezone_lower_min_max(&self) -> (f32, f32) { + (self.bounds().min(), self.exclusion.min()) + } + + /// Returns the minimum and maximum bounds of the upper live zone used for normalization. + /// + /// In simple terms, this returns `(exclusion.max, bounds.max)`. + #[must_use] + #[inline] + pub fn livezone_upper_min_max(&self) -> (f32, f32) { + (self.exclusion.max(), self.bounds().max()) + } + + /// Is the given `input_value` within the exclusion range? + #[must_use] + #[inline] + pub fn within_exclusion(&self, input_value: f32) -> bool { + self.exclusion.contains(input_value) + } + + /// Is the given `input_value` within the bounds? + #[must_use] + #[inline] + pub fn within_bounds(&self, input_value: f32) -> bool { + self.bounds().contains(input_value) + } + + /// Is the given `input_value` within the lower live zone? + #[must_use] + #[inline] + pub fn within_livezone_lower(&self, input_value: f32) -> bool { + let (min, max) = self.livezone_lower_min_max(); + min <= input_value && input_value <= max + } + + /// Is the given `input_value` within the upper live zone? + #[must_use] + #[inline] + pub fn within_livezone_upper(&self, input_value: f32) -> bool { + let (min, max) = self.livezone_upper_min_max(); + min <= input_value && input_value <= max + } + + /// Normalizes input values into the live zone. + #[must_use] + pub fn normalize(&self, input_value: f32) -> f32 { + // Clamp out-of-bounds values to [-1, 1], + // and then exclude values within the dead zone, + // and finally linearly scale the result to the live zone. + if input_value <= 0.0 { + let (bound, deadzone) = self.livezone_lower_min_max(); + let clamped_input = input_value.max(bound); + let distance_to_deadzone = (clamped_input - deadzone).min(0.0); + distance_to_deadzone * self.livezone_lower_recip + } else { + let (deadzone, bound) = self.livezone_upper_min_max(); + let clamped_input = input_value.min(bound); + let distance_to_deadzone = (clamped_input - deadzone).max(0.0); + distance_to_deadzone * self.livezone_upper_recip + } + } +} + +impl Default for AxisDeadZone { + /// Creates an [`AxisDeadZone`] that excludes input values within the deadzone `[-0.1, 0.1]`. + #[inline] + fn default() -> Self { + AxisDeadZone::new(-0.1, 0.1) + } +} + +impl From for AxisProcessor { + fn from(value: AxisDeadZone) -> Self { + Self::DeadZone(value) + } +} + +impl Eq for AxisDeadZone {} + +impl Hash for AxisDeadZone { + fn hash(&self, state: &mut H) { + self.exclusion.hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy::prelude::FloatExt; + + #[test] + fn test_axis_value_bounds() { + fn test_bounds(bounds: AxisBounds, min: f32, max: f32) { + assert_eq!(bounds.min(), min); + assert_eq!(bounds.max(), max); + assert_eq!(bounds.min_max(), (min, max)); + + let processor = AxisProcessor::ValueBounds(bounds); + assert_eq!(AxisProcessor::from(bounds), processor); + + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(bounds.clamp(value), processor.process(value)); + + if min <= value && value <= max { + assert!(bounds.contains(value)); + } else { + assert!(!bounds.contains(value)); + } + + assert_eq!(bounds.clamp(value), value.clamp(min, max)); + } + } + + let bounds = AxisBounds::FULL_RANGE; + test_bounds(bounds, f32::MIN, f32::MAX); + + let bounds = AxisBounds::default(); + test_bounds(bounds, -1.0, 1.0); + + let bounds = AxisBounds::new(-2.0, 2.5); + test_bounds(bounds, -2.0, 2.5); + + let bounds = AxisBounds::magnitude(2.0); + test_bounds(bounds, -2.0, 2.0); + + let bounds = AxisBounds::at_least(-1.0); + test_bounds(bounds, -1.0, f32::MAX); + + let bounds = AxisBounds::at_most(1.5); + test_bounds(bounds, f32::MIN, 1.5); + } + + #[test] + fn test_axis_exclusion() { + fn test_exclusion(exclusion: AxisExclusion, min: f32, max: f32) { + assert_eq!(exclusion.min(), min); + assert_eq!(exclusion.max(), max); + assert_eq!(exclusion.min_max(), (min, max)); + + let processor = AxisProcessor::Exclusion(exclusion); + assert_eq!(AxisProcessor::from(exclusion), processor); + + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(exclusion.exclude(value), processor.process(value)); + + if min <= value && value <= max { + assert!(exclusion.contains(value)); + assert_eq!(exclusion.exclude(value), 0.0); + } else { + assert!(!exclusion.contains(value)); + assert_eq!(exclusion.exclude(value), value); + } + } + } + + let exclusion = AxisExclusion::ZERO; + test_exclusion(exclusion, 0.0, 0.0); + + let exclusion = AxisExclusion::default(); + test_exclusion(exclusion, -0.1, 0.1); + + let exclusion = AxisExclusion::new(-2.0, 2.5); + test_exclusion(exclusion, -2.0, 2.5); + + let exclusion = AxisExclusion::magnitude(1.5); + test_exclusion(exclusion, -1.5, 1.5); + } + + #[test] + fn test_axis_deadzone() { + fn test_deadzone(deadzone: AxisDeadZone, min: f32, max: f32) { + let exclusion = deadzone.exclusion(); + assert_eq!(exclusion.min_max(), (min, max)); + + assert_eq!(deadzone.livezone_lower_min_max(), (-1.0, min)); + let width_recip = (min + 1.0).recip(); + assert!((deadzone.livezone_lower_recip - width_recip).abs() <= f32::EPSILON); + + assert_eq!(deadzone.livezone_upper_min_max(), (max, 1.0)); + let width_recip = (1.0 - max).recip(); + assert!((deadzone.livezone_upper_recip - width_recip).abs() <= f32::EPSILON); + + assert_eq!(AxisExclusion::new(min, max).scaled(), deadzone); + + let processor = AxisProcessor::DeadZone(deadzone); + assert_eq!(AxisProcessor::from(deadzone), processor); + + for value in -300..300 { + let value = value as f32 * 0.01; + + assert_eq!(deadzone.normalize(value), processor.process(value)); + + // Values within the dead zone are treated as zero. + if min <= value && value <= max { + assert!(deadzone.within_exclusion(value)); + assert_eq!(deadzone.normalize(value), 0.0); + } + // Values within the live zone are scaled linearly. + else if -1.0 <= value && value < min { + assert!(deadzone.within_livezone_lower(value)); + + let expected = f32::inverse_lerp(-1.0, min, value) - 1.0; + let delta = (deadzone.normalize(value) - expected).abs(); + assert!(delta <= f32::EPSILON); + } else if max < value && value <= 1.0 { + assert!(deadzone.within_livezone_upper(value)); + + let expected = f32::inverse_lerp(max, 1.0, value); + let delta = (deadzone.normalize(value) - expected).abs(); + assert!(delta <= f32::EPSILON); + } + // Values outside the bounds are restricted to the nearest valid value. + else { + assert!(!deadzone.within_bounds(value)); + assert_eq!(deadzone.normalize(value), value.clamp(-1.0, 1.0)); + } + } + } + + let deadzone = AxisDeadZone::ZERO; + test_deadzone(deadzone, 0.0, 0.0); + + let deadzone = AxisDeadZone::default(); + test_deadzone(deadzone, -0.1, 0.1); + + let deadzone = AxisDeadZone::new(-0.2, 0.3); + test_deadzone(deadzone, -0.2, 0.3); + + let deadzone = AxisDeadZone::magnitude(0.4); + test_deadzone(deadzone, -0.4, 0.4); + } +} diff --git a/src/input_streams.rs b/src/input_streams.rs index 162266d7..a63a895a 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -12,8 +12,7 @@ use bevy::math::Vec2; use bevy::utils::HashSet; use crate::axislike::{ - deadzone_axis_value, AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, - SingleAxis, + AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, }; use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; use crate::prelude::DualAxis; @@ -80,7 +79,7 @@ impl<'a> InputStreams<'a> { pub fn input_pressed(&self, input: &UserInput) -> bool { match input { UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), - _ => input.iter().any(|button| self.button_pressed(button)), + _ => input.iter().any(|button| self.button_pressed(&button)), } } @@ -92,28 +91,30 @@ impl<'a> InputStreams<'a> { /// Is the `button` pressed? #[must_use] - pub fn button_pressed(&self, button: InputKind) -> bool { + pub fn button_pressed(&self, button: &InputKind) -> bool { match button { InputKind::DualAxis(axis) => self - .extract_dual_axis_data(&axis) + .extract_dual_axis_data(axis) .is_some_and(|data| data.xy() != Vec2::ZERO), - InputKind::SingleAxis(axis) => { - let value = self.input_value(&button.into(), false); - - value < axis.negative_low || value > axis.positive_low + InputKind::SingleAxis(_) => { + let input: UserInput = button.clone().into(); + self.input_value(&input) != 0.0 } - InputKind::GamepadButton(button_type) => self - .associated_gamepad - .into_iter() - .chain(self.gamepads.iter()) - .any(|gamepad| { + InputKind::GamepadButton(button_type) => { + let button_pressed = |gamepad: Gamepad| -> bool { self.gamepad_buttons.pressed(GamepadButton { gamepad, - button_type, + button_type: *button_type, }) - }), + }; + if let Some(gamepad) = self.associated_gamepad { + button_pressed(gamepad) + } else { + self.gamepads.iter().any(button_pressed) + } + } InputKind::PhysicalKey(keycode) => { - matches!(self.keycodes, Some(keycodes) if keycodes.pressed(keycode)) + matches!(self.keycodes, Some(keycodes) if keycodes.pressed(*keycode)) } InputKind::Modifier(modifier) => { let key_codes = modifier.key_codes(); @@ -121,7 +122,7 @@ impl<'a> InputStreams<'a> { matches!(self.keycodes, Some(keycodes) if keycodes.pressed(key_codes[0]) | keycodes.pressed(key_codes[1])) } InputKind::Mouse(mouse_button) => { - matches!(self.mouse_buttons, Some(mouse_buttons) if mouse_buttons.pressed(mouse_button)) + matches!(self.mouse_buttons, Some(mouse_buttons) if mouse_buttons.pressed(*mouse_button)) } InputKind::MouseWheel(mouse_wheel_direction) => { let Some(mouse_wheel) = &self.mouse_wheel else { @@ -160,7 +161,7 @@ impl<'a> InputStreams<'a> { /// Are all the `buttons` pressed? #[must_use] pub fn all_buttons_pressed(&self, buttons: &[InputKind]) -> bool { - buttons.iter().all(|button| self.button_pressed(*button)) + buttons.iter().all(|button| self.button_pressed(button)) } /// Get the "value" of the input. @@ -175,31 +176,9 @@ impl<'a> InputStreams<'a> { /// /// If you need to ensure that this value is always in the range `[-1., 1.]`, /// be sure to clamp the returned data. - pub fn input_value(&self, input: &UserInput, include_deadzone: bool) -> f32 { + pub fn input_value(&self, input: &UserInput) -> f32 { let use_button_value = || -> f32 { f32::from(self.input_pressed(input)) }; - // Helper that takes the value returned by an axis and returns 0.0 if it is not within the - // triggering range. - let value_in_axis_range = |axis: &SingleAxis, mut value: f32| -> f32 { - if include_deadzone { - if value >= axis.negative_low && value <= axis.positive_low { - return 0.0; - } - - let deadzone = if value.is_sign_positive() { - axis.positive_low.abs() - } else { - axis.negative_low.abs() - }; - value = deadzone_axis_value(value, deadzone); - } - if axis.inverted { - value *= -1.0; - } - - value * axis.sensitivity - }; - match input { UserInput::Single(InputKind::SingleAxis(single_axis)) => { match single_axis.axis_type { @@ -211,13 +190,13 @@ impl<'a> InputStreams<'a> { }; if let Some(gamepad) = self.associated_gamepad { let value = get_gamepad_value(gamepad); - value_in_axis_range(single_axis, value) + single_axis.input_value(value) } else { self.gamepads .iter() .map(get_gamepad_value) .find(|value| *value != 0.0) - .map_or(0.0, |value| value_in_axis_range(single_axis, value)) + .map_or(0.0, |value| single_axis.input_value(value)) } } AxisType::MouseWheel(axis_type) => { @@ -234,7 +213,7 @@ impl<'a> InputStreams<'a> { MouseWheelAxisType::X => x, MouseWheelAxisType::Y => y, }; - value_in_axis_range(single_axis, movement) + single_axis.input_value(movement) } AxisType::MouseMotion(axis_type) => { // The compiler will compile this into a direct f64 accumulation when opt-level >= 1. @@ -243,12 +222,13 @@ impl<'a> InputStreams<'a> { MouseMotionAxisType::X => x, MouseMotionAxisType::Y => y, }; - value_in_axis_range(single_axis, movement) + single_axis.input_value(movement) } } } UserInput::VirtualAxis(axis) => { - self.extract_single_axis_data(&axis.positive, &axis.negative) + let data = self.extract_single_axis_data(&axis.positive, &axis.negative); + axis.input_value(data) } UserInput::Single(InputKind::DualAxis(_)) => { self.input_axis_pair(input).unwrap_or_default().length() @@ -265,15 +245,15 @@ impl<'a> InputStreams<'a> { value += match input { InputKind::SingleAxis(axis) => { has_axis = true; - self.input_value(&InputKind::SingleAxis(*axis).into(), true) + self.input_value(&InputKind::SingleAxis(axis.clone()).into()) } InputKind::MouseWheel(axis) => { has_axis = true; - self.input_value(&InputKind::MouseWheel(*axis).into(), true) + self.input_value(&InputKind::MouseWheel(*axis).into()) } InputKind::MouseMotion(axis) => { has_axis = true; - self.input_value(&InputKind::MouseMotion(*axis).into(), true) + self.input_value(&InputKind::MouseMotion(*axis).into()) } _ => 0.0, } @@ -338,24 +318,27 @@ impl<'a> InputStreams<'a> { UserInput::VirtualDPad(dpad) => { let x = self.extract_single_axis_data(&dpad.right, &dpad.left); let y = self.extract_single_axis_data(&dpad.up, &dpad.down); - Some(DualAxisData::new(x, y)) + + let data = dpad.input_value(Vec2::new(x, y)); + Some(DualAxisData::from_xy(data)) } _ => None, } } fn extract_single_axis_data(&self, positive: &InputKind, negative: &InputKind) -> f32 { - let positive = self.input_value(&UserInput::Single(*positive), true); - let negative = self.input_value(&UserInput::Single(*negative), true); + let positive = self.input_value(&UserInput::Single(positive.clone())); + let negative = self.input_value(&UserInput::Single(negative.clone())); positive.abs() - negative.abs() } fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> Option { - let x = self.input_value(&dual_axis.x.into(), false); - let y = self.input_value(&dual_axis.y.into(), false); + let x = self.input_value(&SingleAxis::new(dual_axis.x_axis_type).into()); + let y = self.input_value(&SingleAxis::new(dual_axis.y_axis_type).into()); - dual_axis.deadzone.deadzone_input_value(x, y) + let data = dual_axis.input_value(Vec2::new(x, y)); + Some(DualAxisData::from_xy(data)) } } diff --git a/src/lib.rs b/src/lib.rs index 1abf4a6e..9031351f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![forbid(missing_docs)] -#![forbid(unsafe_code)] #![warn(clippy::doc_markdown)] #![doc = include_str!("../README.md")] @@ -20,11 +19,13 @@ mod display_impl; pub mod errors; pub mod input_map; pub mod input_mocking; +pub mod input_processing; pub mod input_streams; pub mod orientation; pub mod plugin; pub mod systems; pub mod timing; +pub mod typetag; pub mod user_input; pub mod user_inputs; @@ -35,20 +36,21 @@ pub use leafwing_input_manager_macros::Actionlike; pub mod prelude { pub use crate::action_driver::ActionStateDriver; pub use crate::action_state::ActionState; - pub use crate::axislike::{ - DeadZoneShape, DualAxis, MouseWheelAxisType, SingleAxis, VirtualAxis, VirtualDPad, - }; + pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualAxis, VirtualDPad}; pub use crate::buttonlike::MouseWheelDirection; pub use crate::clashing_inputs::ClashStrategy; pub use crate::input_map::InputMap; #[cfg(feature = "ui")] pub use crate::input_mocking::MockUIInteraction; pub use crate::input_mocking::{MockInput, QueryInput}; + pub use crate::input_processing::*; pub use crate::user_input::{InputKind, Modifier, UserInput}; pub use crate::plugin::InputManagerPlugin; pub use crate::plugin::ToggleActions; pub use crate::{Actionlike, InputManagerBundle}; + + pub use leafwing_input_manager_macros::serde_typetag; } /// Allows a type to be used as a gameplay action in an input-agnostic fashion diff --git a/src/plugin.rs b/src/plugin.rs index 89a5c65d..70644e4a 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -2,12 +2,13 @@ use crate::action_state::{ActionData, ActionState}; use crate::axislike::{ - AxisType, DeadZoneShape, DualAxis, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, - SingleAxis, VirtualAxis, VirtualDPad, + AxisType, DualAxis, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, + VirtualAxis, VirtualDPad, }; use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; use crate::clashing_inputs::ClashStrategy; use crate::input_map::InputMap; +use crate::input_processing::*; use crate::timing::Timing; use crate::user_input::{InputKind, Modifier, UserInput}; use crate::Actionlike; @@ -176,10 +177,23 @@ impl Plugin for InputManagerPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() + // Processors + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() // Resources .init_resource::>() .init_resource::(); @@ -229,7 +243,7 @@ impl Default for ToggleActions { pub enum InputManagerSystem { /// Advances action timers. /// - /// Cleans up the state of the input manager, clearing `just_pressed` and just_released` + /// Cleans up the state of the input manager, clearing `just_pressed` and `just_released` Tick, /// Collects input data to update the [`ActionState`] Update, diff --git a/src/typetag.rs b/src/typetag.rs new file mode 100644 index 00000000..eac99193 --- /dev/null +++ b/src/typetag.rs @@ -0,0 +1,9 @@ +//! Type tag registration for trait objects + +pub use serde_flexitos::{MapRegistry, Registry}; + +/// A trait for registering type tags. +pub trait RegisterTypeTag<'de, T: ?Sized> { + /// Registers the specified type tag into the [`MapRegistry`]. + fn register_typetag(registry: &mut MapRegistry); +} diff --git a/src/user_input.rs b/src/user_input.rs index 36273590..4a362c97 100644 --- a/src/user_input.rs +++ b/src/user_input.rs @@ -27,7 +27,7 @@ pub enum UserInput { // So a vec it is! // RIP your uniqueness guarantees Chord(Vec), - /// A virtual DPad that you can get an [`DualAxis`] from + /// A virtual D-pad that you can get a [`DualAxis`] from VirtualDPad(VirtualDPad), /// A virtual axis that you can get a [`SingleAxis`] from VirtualAxis(VirtualAxis), @@ -53,7 +53,7 @@ impl UserInput { let vec: Vec = inputs.into_iter().map(|input| input.into()).collect(); match vec.len() { - 1 => UserInput::Single(vec[0]), + 1 => UserInput::Single(vec[0].clone()), _ => UserInput::Chord(vec), } } @@ -110,16 +110,16 @@ impl UserInput { pub(crate) fn iter(&self) -> UserInputIter { match self { - UserInput::Single(button) => UserInputIter::Single(Some(*button)), + UserInput::Single(button) => UserInputIter::Single(Some(button.clone())), UserInput::Chord(buttons) => UserInputIter::Chord(buttons.iter()), - UserInput::VirtualDPad(dpad) => UserInputIter::VirtualDpad( - Some(dpad.up), - Some(dpad.down), - Some(dpad.left), - Some(dpad.right), + UserInput::VirtualDPad(dpad) => UserInputIter::VirtualDPad( + Some(dpad.up.clone()), + Some(dpad.down.clone()), + Some(dpad.left.clone()), + Some(dpad.right.clone()), ), UserInput::VirtualAxis(axis) => { - UserInputIter::VirtualAxis(Some(axis.negative), Some(axis.positive)) + UserInputIter::VirtualAxis(Some(axis.negative.clone()), Some(axis.positive.clone())) } } } @@ -128,7 +128,7 @@ impl UserInput { pub(crate) enum UserInputIter<'a> { Single(Option), Chord(std::slice::Iter<'a, InputKind>), - VirtualDpad( + VirtualDPad( Option, Option, Option, @@ -143,8 +143,8 @@ impl<'a> Iterator for UserInputIter<'a> { fn next(&mut self) -> Option { match self { Self::Single(ref mut input) => input.take(), - Self::Chord(ref mut iter) => iter.next().copied(), - Self::VirtualDpad(ref mut up, ref mut down, ref mut left, ref mut right) => up + Self::Chord(ref mut iter) => iter.next().cloned(), + Self::VirtualDPad(ref mut up, ref mut down, ref mut left, ref mut right) => up .take() .or_else(|| down.take().or_else(|| left.take().or_else(|| right.take()))), Self::VirtualAxis(ref mut negative, ref mut positive) => { @@ -229,7 +229,7 @@ impl From for UserInput { /// /// Please contact the maintainers if you need support for another type! #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] pub enum InputKind { /// A button on a gamepad GamepadButton(GamepadButtonType), @@ -352,22 +352,27 @@ pub struct RawInputs { impl RawInputs { /// Merges the data from the given `input_kind` into `self`. fn merge_input_data(&mut self, input_kind: &InputKind) { - match *input_kind { - InputKind::DualAxis(DualAxis { x, y, .. }) => { - self.axis_data.push((x.axis_type, x.value)); - self.axis_data.push((y.axis_type, y.value)); + match input_kind { + InputKind::DualAxis(DualAxis { + x_axis_type, + y_axis_type, + value, + .. + }) => { + self.axis_data.push((*x_axis_type, value.map(|v| v.x))); + self.axis_data.push((*y_axis_type, value.map(|v| v.y))); } InputKind::SingleAxis(single_axis) => self .axis_data .push((single_axis.axis_type, single_axis.value)), - InputKind::GamepadButton(button) => self.gamepad_buttons.push(button), - InputKind::PhysicalKey(key_code) => self.keycodes.push(key_code), + InputKind::GamepadButton(button) => self.gamepad_buttons.push(*button), + InputKind::PhysicalKey(key_code) => self.keycodes.push(*key_code), InputKind::Modifier(modifier) => { self.keycodes.extend_from_slice(&modifier.key_codes()); } - InputKind::Mouse(button) => self.mouse_buttons.push(button), - InputKind::MouseWheel(button) => self.mouse_wheel.push(button), - InputKind::MouseMotion(button) => self.mouse_motion.push(button), + InputKind::Mouse(button) => self.mouse_buttons.push(*button), + InputKind::MouseWheel(button) => self.mouse_wheel.push(*button), + InputKind::MouseMotion(button) => self.mouse_motion.push(*button), } } } @@ -412,8 +417,8 @@ impl RawInputs { fn from_dual_axis(axis: DualAxis) -> RawInputs { RawInputs { axis_data: vec![ - (axis.x.axis_type, axis.x.value), - (axis.y.axis_type, axis.y.value), + (axis.x_axis_type, axis.value.map(|v| v.x)), + (axis.y_axis_type, axis.value.map(|v| v.y)), ], ..Default::default() } @@ -456,7 +461,7 @@ mod raw_input_tests { let chord = UserInput::chord([ InputKind::GamepadButton(GamepadButtonType::Start), - InputKind::SingleAxis(SingleAxis::symmetric(GamepadAxisType::LeftZ, 0.)), + InputKind::SingleAxis(SingleAxis::new(GamepadAxisType::LeftZ)), ]); let raw = chord.raw_inputs(); @@ -488,7 +493,7 @@ mod raw_input_tests { use bevy::input::gamepad::GamepadAxisType; let direction = SingleAxis::from_value(GamepadAxisType::LeftStickX, 1.0); - let expected = RawInputs::from_single_axis(direction); + let expected = RawInputs::from_single_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } @@ -504,7 +509,7 @@ mod raw_input_tests { 0.5, 0.7, ); - let expected = RawInputs::from_dual_axis(direction); + let expected = RawInputs::from_dual_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } @@ -576,7 +581,7 @@ mod raw_input_tests { use crate::axislike::{MouseWheelAxisType, SingleAxis}; let direction = SingleAxis::from_value(MouseWheelAxisType::X, 1.0); - let expected = RawInputs::from_single_axis(direction); + let expected = RawInputs::from_single_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } @@ -587,7 +592,7 @@ mod raw_input_tests { let direction = DualAxis::from_value(MouseWheelAxisType::X, MouseWheelAxisType::Y, 1.0, 1.0); - let expected = RawInputs::from_dual_axis(direction); + let expected = RawInputs::from_dual_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } @@ -597,7 +602,7 @@ mod raw_input_tests { use crate::axislike::{MouseMotionAxisType, SingleAxis}; let direction = SingleAxis::from_value(MouseMotionAxisType::X, 1.0); - let expected = RawInputs::from_single_axis(direction); + let expected = RawInputs::from_single_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } @@ -608,7 +613,7 @@ mod raw_input_tests { let direction = DualAxis::from_value(MouseMotionAxisType::X, MouseMotionAxisType::Y, 1.0, 1.0); - let expected = RawInputs::from_dual_axis(direction); + let expected = RawInputs::from_dual_axis(direction.clone()); let raw = UserInput::from(direction).raw_inputs(); assert_eq!(expected, raw) } diff --git a/src/user_inputs/axislike_settings.rs b/src/user_inputs/axislike_settings.rs deleted file mode 100644 index ec786727..00000000 --- a/src/user_inputs/axislike_settings.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Utilities for handling axis-like input settings. -//! -//! This module provides a set of tools for managing settings related to axis-like inputs, -//! commonly used in applications such as mouse motion, game controllers, and joysticks. -//! -//! # Deadzones -//! -//! Deadzones are regions around the center of the input range where no action is taken. -//! This helps to eliminate small fluctuations or inaccuracies in input devices, -//! providing smoother and more precise control. -//! -//! # Sensitivity -//! -//! Sensitivity adjustments allow users to fine-tune the responsiveness of input devices. -//! By scaling input values, sensitivity settings can affect the rate at which changes in input are reflected in the output. -//! -//! # Inversion -//! -//! Input inversion allows for changing the directionality of input devices. -//! For example, inverting the input axis of a joystick or mouse can reverse the direction of movement. - -use bevy::prelude::Reflect; -use bevy::utils::FloatOrd; -use serde::{Deserialize, Serialize}; - -/// Settings for single-axis inputs. -#[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] -pub struct SingleAxisSettings { - /// Sensitivity of the input. - sensitivity: f32, - - /// The [`SingleAxisDeadzone`] settings for the input. - deadzone: SingleAxisDeadzone, - - /// The inversion factor of the input. - /// - /// Using an `f32` here instead of a `bool` reduces performance cost, - /// directly indicating inversion status without conditionals. - /// - /// # Values - /// - /// - `1.0` means the input isn't inverted - /// - `-1.0` means the input is inverted - inversion_factor: f32, -} - -impl SingleAxisSettings { - /// The default [`SingleAxisSettings`]. - /// - /// - Sensitivity: `1.0` - /// - Deadzone: Default deadzone (excludes input values within the range [-0.1, 0.1]) - /// - Inversion: Not inverted - pub const DEFAULT: Self = Self { - sensitivity: 1.0, - deadzone: SingleAxisDeadzone::DEFAULT, - inversion_factor: 1.0, - }; - - /// The inverted [`SingleAxisSettings`] with default other settings. - /// - /// - Sensitivity: `1.0` - /// - Deadzone: Default deadzone (excludes input values within the range [-0.1, 0.1]) - /// - Inversion: Inverted - pub const DEFAULT_INVERTED: Self = Self { - sensitivity: 1.0, - deadzone: SingleAxisDeadzone::DEFAULT, - inversion_factor: -1.0, - }; - - /// The default [`SingleAxisSettings`] with a zero deadzone. - /// - /// - Sensitivity: `1.0` - /// - Deadzone: Zero deadzone (excludes only the zeroes) - /// - Inversion: Not inverted - pub const ZERO_DEADZONE: Self = Self { - sensitivity: 1.0, - deadzone: SingleAxisDeadzone::ZERO, - inversion_factor: 1.0, - }; - - /// The inverted [`SingleAxisSettings`] with a zero deadzone. - /// - /// - Sensitivity: `1.0` - /// - Deadzone: Zero deadzone (excludes only the zeroes) - /// - Inversion: Inverted - pub const ZERO_DEADZONE_INVERTED: Self = Self { - sensitivity: 1.0, - deadzone: SingleAxisDeadzone::ZERO, - inversion_factor: -1.0, - }; - - /// Creates a new [`SingleAxisSettings`] with the given settings. - pub const fn new(sensitivity: f32, deadzone: SingleAxisDeadzone) -> Self { - Self { - deadzone, - sensitivity, - inversion_factor: 1.0, - } - } - - /// Creates a new [`SingleAxisSettings`] with the given sensitivity. - pub const fn with_sensitivity(sensitivity: f32) -> Self { - Self::new(sensitivity, SingleAxisDeadzone::DEFAULT) - } - - /// Creates a new [`SingleAxisSettings`] with the given deadzone. - pub const fn with_deadzone(deadzone: SingleAxisDeadzone) -> Self { - Self::new(1.0, deadzone) - } - - /// Creates a new [`SingleAxisSettings`] with only negative values being filtered. - /// - /// # Arguments - /// - /// - `negative_max` - The maximum limit for negative values. - pub fn with_negative_only(negative_max: f32) -> Self { - let deadzone = SingleAxisDeadzone::negative_only(negative_max); - Self::new(1.0, deadzone) - } - - /// Creates a new [`SingleAxisSettings`] with only positive values being filtered. - /// - /// # Arguments - /// - /// - `positive_min` - The minimum limit for positive values. - pub fn with_positive_only(positive_min: f32) -> Self { - let deadzone = SingleAxisDeadzone::positive_only(positive_min); - Self::new(1.0, deadzone) - } - - /// Returns a new [`SingleAxisSettings`] with inversion applied. - pub fn with_inverted(self) -> SingleAxisSettings { - Self { - sensitivity: self.sensitivity, - deadzone: self.deadzone, - inversion_factor: -self.inversion_factor, - } - } - - /// Returns the input value after applying these settings. - pub fn apply_settings(&self, input_value: f32) -> f32 { - let deadzone_value = self.deadzone.apply_deadzone(input_value); - - deadzone_value * self.sensitivity * self.inversion_factor - } - - /// Returns the input value after applying these settings without the deadzone. - pub fn apply_settings_without_deadzone(&self, input_value: f32) -> f32 { - input_value * self.sensitivity * self.inversion_factor - } -} - -/// Deadzone settings for single-axis inputs. -#[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] -pub struct SingleAxisDeadzone { - /// The maximum limit for negative values. - negative_max: f32, - - /// The minimum limit for positive values. - positive_min: f32, - - /// The width of the deadzone around the negative axis. - /// - /// This value represents the absolute value of `negative_max`, - /// reducing the performance cost of using `abs` during value computation. - negative_deadzone_width: f32, -} - -impl SingleAxisDeadzone { - /// The default deadzone with a small offset to filter out near-zero input values. - /// - /// This deadzone excludes input values within the range `[-0.1, 0.1]`. - pub const DEFAULT: Self = Self { - negative_max: -0.1, - positive_min: 0.1, - negative_deadzone_width: 0.1, - }; - - /// The deadzone that only filters out the zeroes. - /// - /// This deadzone doesn't filter out near-zero negative or positive values. - pub const ZERO: Self = Self { - negative_max: 0.0, - positive_min: 0.0, - negative_deadzone_width: 0.0, - }; - - /// Creates a new [`SingleAxisDeadzone`] to filter out input values within the range `[negative_max, positive_min]`. - /// - /// # Arguments - /// - /// - `negative_max` - The maximum limit for negative values, clamped to `0.0` if greater than `0.0`. - /// - `positive_min` - The minimum limit for positive values, clamped to `0.0` if less than `0.0`. - pub fn new(negative_max: f32, positive_min: f32) -> Self { - Self { - negative_max: negative_max.min(0.0), - positive_min: positive_min.max(0.0), - negative_deadzone_width: negative_max.abs(), - } - } - - /// Creates a new [`SingleAxisDeadzone`] with only negative values being filtered. - /// - /// # Arguments - /// - /// - `negative_max` - The maximum limit for negative values, clamped to `0.0` if greater than `0.0`. - pub fn negative_only(negative_max: f32) -> Self { - Self { - negative_max: negative_max.min(0.0), - positive_min: f32::MAX, - negative_deadzone_width: negative_max.abs(), - } - } - - /// Creates a new [`SingleAxisDeadzone`] with only positive values being filtered. - /// - /// # Arguments - /// - /// - `positive_min` - The minimum limit for negative values, clamped to `0.0` if less than `0.0`. - pub fn positive_only(positive_min: f32) -> Self { - Self { - negative_max: f32::MIN, - positive_min: positive_min.max(0.0), - negative_deadzone_width: f32::MAX, - } - } - - /// Returns the input value after applying the deadzone. - /// - /// This function calculates the deadzone width based on the input value and the deadzone settings. - /// If the input value falls within the deadzone range, it returns `0.0`. - /// Otherwise, it normalizes the input value into the range `[-1.0, 1.0]` by subtracting the deadzone width. - /// - /// # Panics - /// - /// Panic if both the negative and positive deadzone ranges are active, must never be reached. - /// - /// If this happens, you might be exploring the quantum realm! - /// Consider offering your computer a cup of coffee and politely asking for a less mysterious explanation. - pub fn apply_deadzone(&self, input_value: f32) -> f32 { - let is_negative_active = self.negative_max > input_value; - let is_positive_active = self.positive_min < input_value; - - let deadzone_width = match (is_negative_active, is_positive_active) { - // The input value is within the deadzone and falls back to `0.0` - (false, false) => return 0.0, - // The input value is outside the negative deadzone range - (true, false) => self.negative_deadzone_width, - // The input value is outside the positive deadzone range - (false, true) => self.positive_min, - // This case must never be reached. - // Unless you've discovered the elusive quantum deadzone! - // Please check your quantum computer and contact the Rust Team. - (true, true) => unreachable!("Quantum deadzone detected!"), - }; - - // Normalize the input value into the range [-1.0, 1.0] - input_value.signum() * (input_value.abs() - deadzone_width) / (1.0 - deadzone_width) - } -} - -// Unfortunately, Rust doesn't let us automatically derive `Eq` and `Hash` for `f32`. -// It's like teaching a fish to ride a bike – a bit nonsensical! -// But if that fish really wants to pedal, we'll make it work. -// So here we are, showing Rust who's boss! - -impl Eq for SingleAxisSettings {} - -impl std::hash::Hash for SingleAxisSettings { - fn hash(&self, state: &mut H) { - FloatOrd(self.sensitivity).hash(state); - self.deadzone.hash(state); - FloatOrd(self.inversion_factor).hash(state); - } -} - -impl Eq for SingleAxisDeadzone {} - -impl std::hash::Hash for SingleAxisDeadzone { - fn hash(&self, state: &mut H) { - FloatOrd(self.negative_max).hash(state); - FloatOrd(self.positive_min).hash(state); - FloatOrd(self.negative_deadzone_width).hash(state); - } -} diff --git a/src/user_inputs/chord.rs b/src/user_inputs/chord.rs new file mode 100644 index 00000000..f9527c91 --- /dev/null +++ b/src/user_inputs/chord.rs @@ -0,0 +1,48 @@ +//! This module contains [`ChordInput`] and its supporting methods and impls.. + +use bevy::prelude::{Reflect, Vec2}; +use leafwing_input_manager_macros::serde_typetag; +use serde::{Deserialize, Serialize}; + +use crate::input_streams::InputStreams; +use crate::user_inputs::UserInput; + +/// A combined input that holds multiple [`UserInput`]s to represent simultaneous button presses. +/// +/// # Behaviors +/// +/// When it treated as a button, it checks if all inner inputs are active simultaneously. +/// When it treated as a single-axis input, it uses the sum of values from all inner single-axis inputs. +/// When it treated as a dual-axis input, it only uses the value of the first inner dual-axis input. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +pub struct ChordInput(Vec>); + +// #[serde_typetag] +impl UserInput for ChordInput { + /// Checks if all the inner inputs are active. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + self.0.iter().all(|input| input.is_active(input_streams)) + } + + /// Returns a combined value representing the input. + /// + /// # Returns + /// + /// + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + match self.0.iter().next() { + Some(input) => input.value(input_streams), + None => 0.0, + } + } + + /// Retrieves the value of the first inner dual-axis input. + #[inline] + fn axis_pair(&self, input_streams: &InputStreams) -> Option { + self.0 + .iter() + .find_map(|input| input.axis_pair(input_streams)) + } +} diff --git a/src/user_inputs/chord_inputs.rs b/src/user_inputs/chord_inputs.rs deleted file mode 100644 index 96e3cd04..00000000 --- a/src/user_inputs/chord_inputs.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! A combination of buttons, pressed simultaneously. - -use bevy::prelude::Reflect; -use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; - -use crate::user_inputs::UserInput; - -/// A combined input with two inner [`UserInput`]s. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize)] -pub struct Combined2Inputs<'a, U1, U2> -where - U1: UserInput<'a> + Deserialize<'a>, - U2: UserInput<'a> + Deserialize<'a>, -{ - inner: (U1, U2), - _phantom_data: PhantomData<(&'a U1, &'a U2)>, -} - -/// A combined input with three inner [`UserInput`]s. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize)] -pub struct Combined3Inputs<'a, U1, U2, U3> -where - U1: UserInput<'a> + Deserialize<'a>, - U2: UserInput<'a> + Deserialize<'a>, - U3: UserInput<'a> + Deserialize<'a>, -{ - inner: (U1, U2, U3), - _phantom_data: PhantomData<(&'a U1, &'a U2, &'a U3)>, -} diff --git a/src/user_inputs/gamepad.rs b/src/user_inputs/gamepad.rs new file mode 100644 index 00000000..8f01f48d --- /dev/null +++ b/src/user_inputs/gamepad.rs @@ -0,0 +1 @@ +//! Gamepad inputs diff --git a/src/user_inputs/gamepad_inputs.rs b/src/user_inputs/gamepad_inputs.rs deleted file mode 100644 index 13f6c8d6..00000000 --- a/src/user_inputs/gamepad_inputs.rs +++ /dev/null @@ -1 +0,0 @@ -//! Utilities for handling gamepad inputs. diff --git a/src/user_inputs/keyboard.rs b/src/user_inputs/keyboard.rs new file mode 100644 index 00000000..999f2d10 --- /dev/null +++ b/src/user_inputs/keyboard.rs @@ -0,0 +1,118 @@ +//! Keyboard inputs + +use bevy::prelude::{KeyCode, Reflect, Vec2}; +use leafwing_input_manager_macros::serde_typetag; +use serde::{Deserialize, Serialize}; + +use crate::input_streams::InputStreams; +use crate::user_inputs::UserInput; + +// Built-in support for KeyCode +#[serde_typetag] +impl UserInput for KeyCode { + /// Checks if the specified [`KeyCode`] is currently pressed down. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + input_streams + .keycodes + .is_some_and(|keys| keys.pressed(*self)) + } + + /// Retrieves the strength of the key press for the specified [`KeyCode`]. + /// + /// # Returns + /// + /// - `1.0`: The key is currently pressed down, indicating an active input. + /// - `0.0`: The key is not pressed, signifying no input. + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + f32::from(self.is_active(input_streams)) + } + + /// Always returns [`None`] as [`KeyCode`]s don't represent dual-axis input. + #[inline] + fn axis_pair(&self, _input_streams: &InputStreams) -> Option { + None + } +} + +/// Defines different keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). +/// +/// Each variant represents a pair of [`KeyCode`]s, the left and right version of the modifier key, +/// allowing for handling modifiers regardless of which side is pressed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum ModifierKey { + /// The Alt key, representing either [`KeyCode::AltLeft`] or [`KeyCode::AltRight`]. + Alt, + + /// The Control key, representing either [`KeyCode::ControlLeft`] or [`KeyCode::ControlRight`]. + Control, + + /// The Shift key, representing either [`KeyCode::ShiftLeft`] or [`KeyCode::ShiftRight`]. + Shift, + + /// The Super (OS symbol) key, representing either [`KeyCode::SuperLeft`] or [`KeyCode::SuperRight`]. + Super, +} + +impl ModifierKey { + /// Returns a pair of [`KeyCode`]s corresponding to both modifier keys. + pub const fn keys(&self) -> [KeyCode; 2] { + [self.left(), self.right()] + } + + /// Returns the [`KeyCode`] corresponding to the left modifier key. + pub const fn left(&self) -> KeyCode { + match self { + ModifierKey::Alt => KeyCode::AltLeft, + ModifierKey::Control => KeyCode::ControlLeft, + ModifierKey::Shift => KeyCode::ShiftLeft, + ModifierKey::Super => KeyCode::SuperLeft, + } + } + + /// Returns the [`KeyCode`] corresponding to the right modifier key. + pub const fn right(&self) -> KeyCode { + match self { + ModifierKey::Alt => KeyCode::AltRight, + ModifierKey::Control => KeyCode::ControlRight, + ModifierKey::Shift => KeyCode::ShiftRight, + ModifierKey::Super => KeyCode::SuperRight, + } + } +} + +#[serde_typetag] +impl UserInput for ModifierKey { + /// Checks if the specified [`ModifierKey`] is currently pressed down. + /// + /// # Returns + /// + /// - `true`: The key is currently pressed down, indicating an active input. + /// - `false`: The key is not pressed, signifying no input. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let modifiers = self.keys(); + input_streams + .keycodes + .is_some_and(|keys| keys.pressed(modifiers[0]) | keys.pressed(modifiers[1])) + } + + /// Gets the strength of the key press for the specified [`ModifierKey`]. + /// + /// # Returns + /// + /// - `1.0`: The key is currently pressed down, indicating an active input. + /// - `0.0`: The key is not pressed, signifying no input. + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + f32::from(self.is_active(input_streams)) + } + + /// Always returns [`None`] as [`ModifierKey`]s don't represent dual-axis input. + #[inline] + fn axis_pair(&self, _input_streams: &InputStreams) -> Option { + None + } +} diff --git a/src/user_inputs/keyboard_inputs.rs b/src/user_inputs/keyboard_inputs.rs deleted file mode 100644 index b9db04c0..00000000 --- a/src/user_inputs/keyboard_inputs.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Utilities for handling keyboard inputs. -//! -//! This module provides utilities for working with keyboard inputs in the applications. -//! It includes support for querying the state of individual keys, -//! as well as modifiers like Alt, Control, Shift, and Super (OS symbol key). -//! -//! # Usage -//! -//! The [`UserInput`] trait is implemented for [`KeyCode`], -//! allowing you to easily query the state of specific keys. -//! -//! Additionally, the [`ModifierKey`] enum represents keyboard modifiers -//! and provides methods for querying their state. - -use bevy::prelude::{KeyCode, Reflect}; -use serde::{Deserialize, Serialize}; - -use crate::input_streams::InputStreams; -use crate::user_inputs::UserInput; - -/// Built-in support for Bevy's [`KeyCode`]. -impl UserInput<'_> for KeyCode { - /// Retrieves the strength of the key press. - /// - /// # Returns - /// - /// - `None` if the keyboard input tracking is unavailable. - /// - `Some(0.0)` if this tracked key isn't pressed. - /// - `Some(1.0)` if this tracked key is currently pressed. - fn value(&self, input_query: InputStreams<'_>) -> Option { - let keycode_stream = input_query; - keycode_stream - .keycodes - .map(|keyboard| keyboard.pressed(*self)) - .map(f32::from) - } - - /// Checks if this tracked key is being pressed down during the current tick. - fn started(&self, input_query: InputStreams<'_>) -> bool { - let keycode_stream = input_query; - keycode_stream - .keycodes - .is_some_and(|keyboard| keyboard.just_pressed(*self)) - } - - /// Checks if this tracked key is being released during the current tick. - fn finished(&self, input_query: InputStreams<'_>) -> bool { - let keycode_stream = input_query; - keycode_stream - .keycodes - .is_some_and(|keyboard| keyboard.just_released(*self)) - } -} - -/// The keyboard modifier combining two [`KeyCode`] values into one representation. -/// -/// Each variant corresponds to a pair of [`KeyCode`] modifiers, -/// such as Alt, Control, Shift, or Super (OS symbol key), -/// one for the left and one for the right key, -/// indicating the modifier's activation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub enum ModifierKey { - /// The Alt key, corresponds to [`KeyCode::AltLeft`] and [`KeyCode::AltRight`]. - Alt, - /// The Control key, corresponds to [`KeyCode::ControlLeft`] and [`KeyCode::ControlRight`]. - Control, - /// The Shift key, corresponds to [`KeyCode::ShiftLeft`] and [`KeyCode::ShiftRight`]. - Shift, - /// The Super (OS symbol) key, corresponds to [`KeyCode::SuperLeft`] and [`KeyCode::SuperRight`]. - Super, -} - -impl ModifierKey { - /// Returns the [`KeyCode`] corresponding to the left key of the modifier. - pub const fn left(&self) -> KeyCode { - match self { - ModifierKey::Alt => KeyCode::AltLeft, - ModifierKey::Control => KeyCode::ControlLeft, - ModifierKey::Shift => KeyCode::ShiftLeft, - ModifierKey::Super => KeyCode::SuperLeft, - } - } - - /// Returns the [`KeyCode`] corresponding to the right key of the modifier. - pub const fn right(&self) -> KeyCode { - match self { - ModifierKey::Alt => KeyCode::AltRight, - ModifierKey::Control => KeyCode::ControlRight, - ModifierKey::Shift => KeyCode::ShiftRight, - ModifierKey::Super => KeyCode::SuperRight, - } - } -} - -impl UserInput<'_> for ModifierKey { - /// Retrieves the strength of the key press. - /// - /// # Returns - /// - /// - `None` if the keyboard input tracking is unavailable. - /// - `Some(0.0)` if these tracked keys aren't pressed. - /// - `Some(1.0)` if these tracked keys are currently pressed. - fn value(&self, input_query: InputStreams<'_>) -> Option { - let keycode_stream = input_query; - keycode_stream - .keycodes - .map(|keyboard| keyboard.pressed(self.left()) | keyboard.pressed(self.right())) - .map(f32::from) - } - - /// Checks if these tracked keys are being pressed down during the current tick. - fn started(&self, input_query: InputStreams<'_>) -> bool { - let keycode_stream = input_query; - keycode_stream.keycodes.is_some_and(|keyboard| { - keyboard.just_pressed(self.left()) | keyboard.just_pressed(self.right()) - }) - } - - /// Checks if these tracked keys are being released during the current tick. - fn finished(&self, input_query: InputStreams<'_>) -> bool { - let keycode_stream = input_query; - keycode_stream.keycodes.is_some_and(|keyboard| { - keyboard.just_released(self.left()) | keyboard.just_released(self.right()) - }) - } -} diff --git a/src/user_inputs/mod.rs b/src/user_inputs/mod.rs index b547a4f5..1a501d01 100644 --- a/src/user_inputs/mod.rs +++ b/src/user_inputs/mod.rs @@ -15,59 +15,256 @@ //! //! ## General Input Settings //! -//! - [`axislike_settings`]: Utilities for configuring axis-like input. +//! - [`axislike_processors`]: Utilities for configuring axis-like inputs. //! //! ## General Inputs //! -//! - [`gamepad_inputs`]: Utilities for handling gamepad inputs. -//! - [`keyboard_inputs`]: Utilities for handling keyboard inputs. -//! - [`mouse_inputs`]: Utilities for handling mouse inputs. +//! - [`gamepad`]: Utilities for handling gamepad inputs. +//! - [`keyboard`]: Utilities for handling keyboard inputs. +//! - [`mouse`]: Utilities for handling mouse inputs. //! -//! ## Specific Inputs: +//! ## Specific Inputs //! -//! - [`chord_inputs`]: A combination of buttons, pressed simultaneously. +//! - [`chord`]: A combination of buttons, pressed simultaneously. -use bevy::prelude::{Reflect, Vec2}; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::hash::Hash; +use std::any::{Any, TypeId}; +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::RwLock; -use crate::input_streams::InputStreams; +use bevy::prelude::{App, Vec2}; +use bevy::reflect::utility::{reflect_hasher, GenericTypePathCell, NonGenericTypeInfoCell}; +use bevy::reflect::{ + erased_serde, DynamicTypePath, FromReflect, FromType, GetTypeRegistration, Reflect, + ReflectDeserialize, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + ReflectSerialize, TypeInfo, TypePath, TypeRegistration, Typed, ValueInfo, +}; +use dyn_clone::DynClone; +use dyn_eq::DynEq; +use dyn_hash::DynHash; +use once_cell::sync::Lazy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_flexitos::ser::require_erased_serialize_impl; +use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; -pub mod axislike_settings; +use crate::input_streams::InputStreams; +use crate::typetag::RegisterTypeTag; -pub mod gamepad_inputs; -pub mod keyboard_inputs; -pub mod mouse_inputs; +pub mod chord; +pub mod gamepad; +pub mod keyboard; +pub mod mouse; -pub mod chord_inputs; +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] +pub enum InputKind { + Button, + Axis, + DualAxis, +} -/// Allows defining a specific kind of user input. -pub trait UserInput<'a>: - Send + Sync + Debug + Clone + PartialEq + Eq + Hash + Reflect + Serialize + Deserialize<'a> +/// A trait for defining the behavior expected from different user input sources. +/// +/// Implementers of this trait should provide methods for accessing and +/// processing user input data. +pub trait UserInput: + Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize { - /// Checks if this input is currently active. - fn is_active(&self, input_query: InputStreams<'a>) -> bool { - self.value(input_query).is_some_and(|value| value != 0.0) + /// Checks if the user input is currently active. + fn is_active(&self, input_streams: &InputStreams) -> bool; + + /// Retrieves the current value of the user input. + fn value(&self, input_streams: &InputStreams) -> f32; + + /// Retrieves the current dual-axis value of the user input if available. + fn axis_pair(&self, input_streams: &InputStreams) -> Option; +} + +dyn_clone::clone_trait_object!(UserInput); +dyn_eq::eq_trait_object!(UserInput); +dyn_hash::hash_trait_object!(UserInput); + +impl Reflect for Box { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(Self::type_info()) + } + + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn apply(&mut self, value: &dyn Reflect) { + let value = value.as_any(); + if let Some(value) = value.downcast_ref::() { + *self = value.clone(); + } else { + panic!( + "Value is not a std::boxed::Box.", + module_path!(), + ); + } + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Value + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Value(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Value(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Value(self) } - /// Retrieves the current value from input if available. - fn value(&self, _input_query: InputStreams<'a>) -> Option { - None + fn clone_value(&self) -> Box { + Box::new(self.clone()) } - /// Retrieves the current two-dimensional values from input if available. - fn pair_values(&self, _input_query: InputStreams<'a>) -> Option { - None + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + let type_id = TypeId::of::(); + Hash::hash(&type_id, &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) } - /// Checks if this input is being active during the current tick. - fn started(&self, _input_query: InputStreams<'a>) -> bool { - false + fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + value + .as_any() + .downcast_ref::() + .map(|value| self.dyn_eq(value)) + .or(Some(false)) } - /// Checks if this input is being inactive during the current tick. - fn finished(&self, _input_query: InputStreams<'a>) -> bool { - false + fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} + +impl Typed for Box { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) + } +} + +impl TypePath for Box { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + { + format!("std::boxed::Box", module_path!()) + } + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| "Box".to_string()) + } + + fn type_ident() -> Option<&'static str> { + Some("Box") + } + + fn crate_name() -> Option<&'static str> { + Some(module_path!().split(':').next().unwrap()) + } + + fn module_path() -> Option<&'static str> { + Some(module_path!()) + } +} + +impl GetTypeRegistration for Box { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for Box { + fn from_reflect(reflect: &dyn Reflect) -> Option { + Some(reflect.as_any().downcast_ref::()?.clone()) + } +} + +impl<'a> Serialize for dyn UserInput + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Check that `UserInput` has `erased_serde::Serialize` as a super trait, + // preventing infinite recursion at runtime. + const fn __check_erased_serialize_super_trait() { + require_erased_serialize_impl::(); + } + serialize_trait_object(serializer, self.reflect_short_type_path(), self) + } +} + +impl<'de> Deserialize<'de> for Box { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let registry = unsafe { INPUT_REGISTRY.read().unwrap() }; + registry.deserialize_trait_object(deserializer) + } +} + +/// Registry of deserializers for [`UserInput`]s. +static mut INPUT_REGISTRY: Lazy>> = + Lazy::new(|| RwLock::new(MapRegistry::new("UserInput"))); + +/// A trait for registering a specific [`UserInput`]. +pub trait RegisterUserInput { + /// Registers the specified [`UserInput`]. + fn register_user_input<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn UserInput> + GetTypeRegistration; +} + +impl RegisterUserInput for App { + fn register_user_input<'de, T>(&mut self) -> &mut Self + where + T: RegisterTypeTag<'de, dyn UserInput> + GetTypeRegistration, + { + let mut registry = unsafe { INPUT_REGISTRY.write().unwrap() }; + T::register_typetag(&mut registry); + self.register_type::(); + self } } diff --git a/src/user_inputs/mouse.rs b/src/user_inputs/mouse.rs new file mode 100644 index 00000000..0bc095c6 --- /dev/null +++ b/src/user_inputs/mouse.rs @@ -0,0 +1,43 @@ +//! Mouse inputs + +use bevy::prelude::{Reflect, Vec2}; +use leafwing_input_manager_macros::serde_typetag; +use serde::{Deserialize, Serialize}; + +use crate::input_processing::*; +use crate::input_streams::InputStreams; +use crate::user_inputs::UserInput; + +/// Represents mouse motion input, combining individual axis movements into a single value. +/// +/// It uses an internal [`DualAxisProcessor`] to process raw mouse movement data +/// from multiple mouse motion events into a combined representation. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +pub struct MouseMotionInput(DualAxisProcessor); + +#[serde_typetag] +impl UserInput for MouseMotionInput { + /// Checks if there is active mouse motion. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + self.axis_pair(input_streams).unwrap() != Vec2::ZERO + } + + /// Retrieves the magnitude of the mouse motion, representing the overall amount of movement. + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + self.axis_pair(input_streams).unwrap().length() + } + + /// Retrieves the accumulated mouse displacement. + #[inline] + fn axis_pair(&self, input_streams: &InputStreams) -> Option { + let value = input_streams + .mouse_motion + .iter() + .map(|event| event.delta) + .sum(); + let value = self.0.process(value); + Some(value) + } +} diff --git a/src/user_inputs/mouse_inputs.rs b/src/user_inputs/mouse_inputs.rs deleted file mode 100644 index 2fd8ace3..00000000 --- a/src/user_inputs/mouse_inputs.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Utilities for handling keyboard inputs. - -use bevy::prelude::Reflect; -use serde::{Deserialize, Serialize}; - -use crate::input_streams::InputStreams; -use crate::user_inputs::axislike_settings::SingleAxisSettings; -use crate::user_inputs::UserInput; - -/// Vertical mouse wheel input with settings. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub struct MouseWheelVertical(pub SingleAxisSettings); - -impl UserInput<'_> for MouseWheelVertical { - /// Retrieves the magnitude of vertical mouse wheel movements. - /// - /// Returns `None` if mouse input tracking is unavailable. - /// Returns `Some(0.0)` if the tracked mouse wheel isn't scrolling. - /// Returns `Some(magnitude)` of the tracked mouse wheel along the y-axis. - fn value(&self, input_query: InputStreams) -> Option { - let mouse_wheel_input = input_query.mouse_wheel; - mouse_wheel_input.map(|mouse_wheel| { - let movements = mouse_wheel.iter().map(|wheel| wheel.y).sum(); - self.0.apply_settings(movements) - }) - } - - /// Checks if the mouse wheel started scrolling vertically during the current tick. - fn started(&self, _input_query: InputStreams) -> bool { - // Unable to accurately determine this here; - // it should be checked during the update of the `ActionState`. - false - } - - /// Checks if the mouse wheel stopped scrolling vertically during the current tick. - fn finished(&self, _input_query: InputStreams) -> bool { - // Unable to accurately determine this here; - // it should be checked during the update of the `ActionState`. - false - } -} diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index 85805c82..831e90ef 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -3,7 +3,7 @@ use bevy::input::gamepad::{ }; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DeadZoneShape, DualAxisData}; +use leafwing_input_manager::axislike::{AxisType, DualAxisData}; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] @@ -53,7 +53,7 @@ fn raw_gamepad_axis_events() { let mut app = test_app(); app.insert_resource(InputMap::new([( ButtonlikeTestAction::Up, - SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1), + SingleAxis::new(GamepadAxisType::RightStickX).with_processor(AxisDeadZone::default()), )])); let mut events = app.world.resource_mut::>(); @@ -78,10 +78,7 @@ fn game_pad_single_axis_mocking() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - sensitivity: 1.0, - inverted: false, + processor: AxisProcessor::None, }; app.send_input(input); @@ -97,23 +94,10 @@ fn game_pad_dual_axis_mocking() { assert_eq!(events.drain().count(), 0); let input = DualAxis { - x: SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - sensitivity: 1.0, - inverted: false, - }, - y: SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(0.), - positive_low: 0.0, - negative_low: 0.0, - sensitivity: 1.0, - inverted: false, - }, - deadzone: DualAxis::DEFAULT_DEADZONE_SHAPE, + x_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), + y_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), + processor: CircleDeadZone::default().into(), + value: Some(Vec2::X), }; app.send_input(input); let mut events = app.world.resource_mut::>(); @@ -127,11 +111,11 @@ fn game_pad_single_axis() { app.insert_resource(InputMap::new([ ( AxislikeTestAction::X, - SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1), + SingleAxis::new(GamepadAxisType::LeftStickX).with_processor(AxisDeadZone::default()), ), ( AxislikeTestAction::Y, - SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1), + SingleAxis::new(GamepadAxisType::LeftStickY).with_processor(AxisDeadZone::default()), ), ])); @@ -139,10 +123,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -153,10 +134,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -167,10 +145,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -181,10 +156,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -192,14 +164,11 @@ fn game_pad_single_axis() { assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 + // Usually a small deadzone threshold will be set let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: Some(0.0), - // Usually a small deadzone threshold will be set - positive_low: 0.1, - negative_low: 0.1, - inverted: false, - sensitivity: 1.0, + processor: AxisDeadZone::default().into(), }; app.send_input(input); app.update(); @@ -210,10 +179,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: None, - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -224,10 +190,7 @@ fn game_pad_single_axis() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(0.2), - positive_low: 0.1, - negative_low: 0.1, - inverted: false, - sensitivity: 1.0, + processor: AxisDeadZone::default().into(), }; app.send_input(input); app.update(); @@ -242,11 +205,15 @@ fn game_pad_single_axis_inverted() { app.insert_resource(InputMap::new([ ( AxislikeTestAction::X, - SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1).inverted(), + SingleAxis::new(GamepadAxisType::LeftStickX) + .with_processor(AxisDeadZone::default()) + .with_processor(AxisProcessor::Inverted), ), ( AxislikeTestAction::Y, - SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1).inverted(), + SingleAxis::new(GamepadAxisType::LeftStickY) + .with_processor(AxisDeadZone::default()) + .with_processor(AxisProcessor::Inverted), ), ])); @@ -254,12 +221,8 @@ fn game_pad_single_axis_inverted() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: true, - sensitivity: -1.0, - } - .inverted(); + processor: AxisProcessor::Inverted, + }; app.send_input(input); app.update(); let action_state = app.world.resource::>(); @@ -270,10 +233,7 @@ fn game_pad_single_axis_inverted() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: true, - sensitivity: -1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -285,10 +245,7 @@ fn game_pad_single_axis_inverted() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: true, - sensitivity: -1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -300,10 +257,7 @@ fn game_pad_single_axis_inverted() { let input = SingleAxis { axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: true, - sensitivity: -1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -313,17 +267,14 @@ fn game_pad_single_axis_inverted() { } #[test] -fn game_pad_dual_axis_cross() { +fn game_pad_dual_axis_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().with_deadzone(DeadZoneShape::Cross { - horizontal_width: 0.1, - vertical_width: 0.1, - }), + DualAxis::left_stick().replace_processor(DualAxisDeadZone::default()), )])); - // Test that an input inside the cross deadzone is filtered out. + // Test that an input inside the dual-axis deadzone is filtered out. app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, @@ -341,7 +292,7 @@ fn game_pad_dual_axis_cross() { DualAxisData::new(0.0, 0.0) ); - // Test that an input outside the cross deadzone is not filtered out. + // Test that an input outside the dual-axis deadzone is not filtered out. app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, @@ -359,7 +310,7 @@ fn game_pad_dual_axis_cross() { DualAxisData::new(1.0, 0.11111112) ); - // Test that each axis of the cross deadzone is filtered independently. + // Test that each axis of the dual-axis deadzone is filtered independently. app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, @@ -379,17 +330,14 @@ fn game_pad_dual_axis_cross() { } #[test] -fn game_pad_dual_axis_ellipse() { +fn game_pad_circle_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().with_deadzone(DeadZoneShape::Ellipse { - radius_x: 0.1, - radius_y: 0.1, - }), + DualAxis::left_stick().replace_processor(CircleDeadZone::default()), )])); - // Test that an input inside the ellipse deadzone is filtered out, assuming values of 0.1 + // Test that an input inside the circle deadzone is filtered out, assuming values of 0.1 app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, @@ -407,7 +355,7 @@ fn game_pad_dual_axis_ellipse() { DualAxisData::new(0.0, 0.0) ); - // Test that an input outside the ellipse deadzone is not filtered out, assuming values of 0.1 + // Test that an input outside the circle deadzone is not filtered out, assuming values of 0.1 app.send_input(DualAxis::from_value( GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY, @@ -427,14 +375,11 @@ fn game_pad_dual_axis_ellipse() { } #[test] -fn test_zero_cross() { +fn test_zero_dual_axis_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().with_deadzone(DeadZoneShape::Cross { - horizontal_width: 0.0, - vertical_width: 0.0, - }), + DualAxis::left_stick().replace_processor(DualAxisDeadZone::ZERO), )])); // Test that an input of zero will be `None` even with no deadzone. @@ -457,14 +402,11 @@ fn test_zero_cross() { } #[test] -fn test_zero_ellipse() { +fn test_zero_circle_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().with_deadzone(DeadZoneShape::Ellipse { - radius_x: 0.0, - radius_y: 0.0, - }), + DualAxis::left_stick().replace_processor(CircleDeadZone::ZERO), )])); // Test that an input of zero will be `None` even with no deadzone. @@ -488,7 +430,7 @@ fn test_zero_ellipse() { #[test] #[ignore = "Input mocking is subtly broken: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/407"] -fn game_pad_virtualdpad() { +fn game_pad_virtual_dpad() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 5643d04b..1638d8ce 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -4,7 +4,6 @@ use bevy::prelude::*; use leafwing_input_manager::axislike::{AxisType, DualAxisData, MouseMotionAxisType}; use leafwing_input_manager::buttonlike::MouseMotionDirection; use leafwing_input_manager::prelude::*; -use leafwing_input_manager::user_input::InputKind; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] enum ButtonlikeTestAction { @@ -83,10 +82,7 @@ fn mouse_motion_single_axis_mocking() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); @@ -101,23 +97,10 @@ fn mouse_motion_dual_axis_mocking() { assert_eq!(events.drain().count(), 0); let input = DualAxis { - x: SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, - }, - y: SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - value: Some(0.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, - }, - deadzone: DualAxis::ZERO_DEADZONE_SHAPE, + x_axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), + y_axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), + processor: DualAxisProcessor::None, + value: Some(Vec2::X), }; app.send_input(input); let mut events = app.world.resource_mut::>(); @@ -182,10 +165,7 @@ fn mouse_motion_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -196,10 +176,7 @@ fn mouse_motion_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -210,10 +187,7 @@ fn mouse_motion_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -224,10 +198,7 @@ fn mouse_motion_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -235,14 +206,11 @@ fn mouse_motion_single_axis() { assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 + // Usually a small deadzone threshold will be set let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), value: Some(0.0), - // Usually a small deadzone threshold will be set - positive_low: 0.1, - negative_low: 0.1, - inverted: false, - sensitivity: 1.0, + processor: AxisDeadZone::default().into(), }; app.send_input(input); app.update(); @@ -253,10 +221,7 @@ fn mouse_motion_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), value: None, - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -292,7 +257,7 @@ fn mouse_motion_dual_axis() { } #[test] -fn mouse_motion_virtualdpad() { +fn mouse_motion_virtual_dpad() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index 478d341e..03d243e8 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -1,7 +1,7 @@ use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData, MouseWheelAxisType}; +use leafwing_input_manager::axislike::{AxisType, DualAxisData}; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] @@ -79,10 +79,7 @@ fn mouse_wheel_single_axis_mocking() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); @@ -97,23 +94,10 @@ fn mouse_wheel_dual_axis_mocking() { assert_eq!(events.drain().count(), 0); let input = DualAxis { - x: SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, - }, - y: SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - value: Some(0.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, - }, - deadzone: DualAxis::ZERO_DEADZONE_SHAPE, + x_axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), + y_axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), + processor: DualAxisProcessor::None, + value: Some(Vec2::X), }; app.send_input(input); let mut events = app.world.resource_mut::>(); @@ -178,10 +162,7 @@ fn mouse_wheel_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -192,10 +173,7 @@ fn mouse_wheel_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -206,10 +184,7 @@ fn mouse_wheel_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), value: Some(1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -220,10 +195,7 @@ fn mouse_wheel_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), value: Some(-1.), - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); @@ -231,14 +203,11 @@ fn mouse_wheel_single_axis() { assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 + // Usually a small deadzone threshold will be set let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), value: Some(0.0), - // Usually a small deadzone threshold will be set - positive_low: 0.1, - negative_low: 0.1, - inverted: false, - sensitivity: 1.0, + processor: AxisDeadZone::default().into(), }; app.send_input(input); app.update(); @@ -249,10 +218,7 @@ fn mouse_wheel_single_axis() { let input = SingleAxis { axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), value: None, - positive_low: 0.0, - negative_low: 0.0, - inverted: false, - sensitivity: 1.0, + processor: AxisProcessor::None, }; app.send_input(input); app.update(); diff --git a/tests/multiple_gamepads.rs b/tests/multiple_gamepads.rs new file mode 100644 index 00000000..8c5cb5df --- /dev/null +++ b/tests/multiple_gamepads.rs @@ -0,0 +1,123 @@ +use bevy::input::gamepad::{GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo}; +use bevy::input::InputPlugin; +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; + +#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] +enum MyAction { + Jump, +} + +fn create_test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins).add_plugins(InputPlugin); + app.add_plugins(InputManagerPlugin::::default()); + + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { + // Must be consistent with mocked events + gamepad: Gamepad { id: 1 }, + connection: GamepadConnection::Connected(GamepadInfo { + name: "FirstController".into(), + }), + })); + gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { + // Must be consistent with mocked events + gamepad: Gamepad { id: 2 }, + connection: GamepadConnection::Connected(GamepadInfo { + name: "SecondController".into(), + }), + })); + + // Ensure the gamepads are picked up + app.update(); + // Flush the gamepad connection events + app.update(); + + app +} + +fn jump_button_press_event(gamepad: Gamepad) -> GamepadEvent { + use bevy::input::gamepad::GamepadButtonChangedEvent; + + GamepadEvent::Button(GamepadButtonChangedEvent::new( + gamepad, + GamepadButtonType::South, + 1.0, + )) +} + +#[test] +fn default_accepts_any() { + let mut app = create_test_app(); + + const FIRST_GAMEPAD: Gamepad = Gamepad { id: 1 }; + const SECOND_GAMEPAD: Gamepad = Gamepad { id: 2 }; + + let input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]); + app.insert_resource(input_map); + app.init_resource::>(); + + // When we press the Jump button... + let mut events = app.world.resource_mut::>(); + events.send(jump_button_press_event(FIRST_GAMEPAD)); + app.update(); + + // ... We should receive a Jump action! + let mut action_state = app.world.resource_mut::>(); + assert!(action_state.pressed(&MyAction::Jump)); + + action_state.release(&MyAction::Jump); + app.update(); + + // This is maintained for any gamepad. + let mut events = app.world.resource_mut::>(); + events.send(jump_button_press_event(SECOND_GAMEPAD)); + app.update(); + + let action_state = app.world.resource_mut::>(); + assert!(action_state.pressed(&MyAction::Jump)); +} + +#[test] +fn accepts_preferred_gamepad() { + let mut app = create_test_app(); + + const PREFERRED_GAMEPAD: Gamepad = Gamepad { id: 2 }; + + let mut input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]); + input_map.set_gamepad(PREFERRED_GAMEPAD); + app.insert_resource(input_map); + app.init_resource::>(); + + // When we press the Jump button... + let mut events = app.world.resource_mut::>(); + events.send(jump_button_press_event(PREFERRED_GAMEPAD)); + app.update(); + + // ... We should receive a Jump action! + let action_state = app.world.resource_mut::>(); + assert!(action_state.pressed(&MyAction::Jump)); +} + +#[test] +fn filters_out_other_gamepads() { + let mut app = create_test_app(); + + const PREFERRED_GAMEPAD: Gamepad = Gamepad { id: 2 }; + const OTHER_GAMEPAD: Gamepad = Gamepad { id: 1 }; + + let mut input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]); + input_map.set_gamepad(PREFERRED_GAMEPAD); + app.insert_resource(input_map); + app.init_resource::>(); + + // When we press the Jump button... + let mut events = app.world.resource_mut::>(); + events.send(jump_button_press_event(OTHER_GAMEPAD)); + app.update(); + + // ... We should receive a Jump action! + let action_state = app.world.resource_mut::>(); + assert!(action_state.released(&MyAction::Jump)); +} From 29195ea392ea5be79c6270f0cefbd0f2c3f4ae7f Mon Sep 17 00:00:00 2001 From: Shute052 Date: Mon, 29 Apr 2024 07:02:10 +0800 Subject: [PATCH 04/20] Implement mouse inputs --- src/axislike.rs | 116 ------- src/input_streams.rs | 5 +- src/user_inputs/axislike.rs | 244 +++++++++++++++ src/user_inputs/chord.rs | 9 +- src/user_inputs/keyboard.rs | 39 +-- src/user_inputs/mod.rs | 28 +- src/user_inputs/mouse.rs | 594 ++++++++++++++++++++++++++++++++++-- 7 files changed, 856 insertions(+), 179 deletions(-) create mode 100644 src/user_inputs/axislike.rs diff --git a/src/axislike.rs b/src/axislike.rs index 2d9d8f07..b827ea4b 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -615,119 +615,3 @@ impl TryFrom for MouseMotionAxisType { /// An [`AxisType`] could not be converted into a more specialized variant #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct AxisConversionError; - -/// A wrapped [`Vec2`] that represents the combination of two input axes. -/// -/// The neutral origin is always at 0, 0. -/// When working with gamepad axes, both `x` and `y` values are bounded by [-1.0, 1.0]. -/// For other input axes (such as mousewheel data), this may not be true! -/// -/// This struct should store the processed form of your raw inputs in a device-agnostic fashion. -/// Any deadzone correction, rescaling or drift-correction should be done at an earlier level. -#[derive(Debug, Copy, Clone, PartialEq, Default, Deserialize, Serialize, Reflect)] -pub struct DualAxisData { - xy: Vec2, -} - -// Constructors -impl DualAxisData { - /// Creates a new [`DualAxisData`] from the provided (x,y) coordinates - pub fn new(x: f32, y: f32) -> DualAxisData { - DualAxisData { - xy: Vec2::new(x, y), - } - } - - /// Creates a new [`DualAxisData`] directly from a [`Vec2`] - pub fn from_xy(xy: Vec2) -> DualAxisData { - DualAxisData { xy } - } - - /// Merge the state of this [`DualAxisData`] with another. - /// - /// This is useful if you have multiple sticks bound to the same game action, - /// and you want to get their combined position. - /// - /// # Warning - /// - /// This method can result in values with a greater maximum magnitude than expected! - /// Use [`DualAxisData::clamp_length`] to limit the resulting direction. - pub fn merged_with(&self, other: DualAxisData) -> DualAxisData { - DualAxisData::from_xy(self.xy() + other.xy()) - } -} - -// Methods -impl DualAxisData { - /// The value along the x-axis, typically ranging from -1 to 1 - #[must_use] - #[inline] - pub fn x(&self) -> f32 { - self.xy.x - } - - /// The value along the y-axis, typically ranging from -1 to 1 - #[must_use] - #[inline] - pub fn y(&self) -> f32 { - self.xy.y - } - - /// The (x, y) values, each typically ranging from -1 to 1 - #[must_use] - #[inline] - pub fn xy(&self) -> Vec2 { - self.xy - } - - /// The [`Direction2d`] that this axis is pointing towards, if any - /// - /// If the axis is neutral (x,y) = (0,0), a (0, 0) `None` will be returned - #[must_use] - #[inline] - pub fn direction(&self) -> Option { - Direction2d::new(self.xy).ok() - } - - /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any - /// - /// If the axis is neutral (x,y) = (0,0), this will be `None` - #[must_use] - #[inline] - pub fn rotation(&self) -> Option { - Rotation::from_xy(self.xy).ok() - } - - /// How far from the origin is this axis's position? - /// - /// Typically bounded by 0 and 1. - /// - /// If you only need to compare relative magnitudes, use `magnitude_squared` instead for faster computation. - #[must_use] - #[inline] - pub fn length(&self) -> f32 { - self.xy.length() - } - - /// The square of the axis' magnitude - /// - /// Typically bounded by 0 and 1. - /// - /// This is faster than `magnitude`, as it avoids a square root, but will generally have less natural behavior. - #[must_use] - #[inline] - pub fn length_squared(&self) -> f32 { - self.xy.length_squared() - } - - /// Clamps the magnitude of the axis - pub fn clamp_length(&mut self, max: f32) { - self.xy = self.xy.clamp_length_max(max); - } -} - -impl From for Vec2 { - fn from(data: DualAxisData) -> Vec2 { - data.xy - } -} diff --git a/src/input_streams.rs b/src/input_streams.rs index a63a895a..1cff7797 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -11,12 +11,11 @@ use bevy::input::{ use bevy::math::Vec2; use bevy::utils::HashSet; -use crate::axislike::{ - AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, -}; +use crate::axislike::{AxisType, MouseMotionAxisType, MouseWheelAxisType, SingleAxis}; use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; use crate::prelude::DualAxis; use crate::user_input::{InputKind, UserInput}; +use crate::user_inputs::axislike::DualAxisData; /// A collection of [`ButtonInput`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). /// diff --git a/src/user_inputs/axislike.rs b/src/user_inputs/axislike.rs new file mode 100644 index 00000000..7e16adfd --- /dev/null +++ b/src/user_inputs/axislike.rs @@ -0,0 +1,244 @@ +use crate::input_processing::{AxisProcessor, DualAxisProcessor}; +use crate::input_streams::InputStreams; +use crate::orientation::Rotation; +use bevy::math::Vec2; +use bevy::prelude::{Direction2d, Reflect}; +use bevy::utils::petgraph::matrix_graph::Zero; +use serde::{Deserialize, Serialize}; + +/// Different ways that user input is represented on an axis. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Reflect)] +#[must_use] +pub enum AxisInputMode { + /// Continuous input values, typically range from a negative maximum (e.g., `-1.0`) + /// to a positive maximum (e.g., `1.0`), allowing for smooth and precise control. + Analog, + + /// Discrete input values, using three distinct values to represent the states: + /// `-1.0` for active in negative direction, `0.0` for inactive, and `1.0` for active in positive direction. + Digital, +} + +impl AxisInputMode { + /// Converts the given `f32` value based on the current [`AxisInputMode`]. + /// + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0`, leaving others as is. + #[must_use] + #[inline] + pub fn axis_value(&self, value: f32) -> f32 { + match self { + Self::Analog => value, + Self::Digital => { + if value < 0.0 { + -1.0 + } else if value > 0.0 { + 1.0 + } else { + value + } + } + } + } + + /// Converts the given [`Vec2`] value based on the current [`AxisInputMode`]. + /// + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0` along each axis, leaving others as is. + #[must_use] + #[inline] + pub fn dual_axis_value(&self, value: Vec2) -> Vec2 { + match self { + Self::Analog => value, + Self::Digital => Vec2::new(self.axis_value(value.x), self.axis_value(value.y)), + } + } + + /// Computes the magnitude of given [`Vec2`] value based on the current [`AxisInputMode`]. + /// + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: `1.0` for non-zero values, `0.0` for others. + #[must_use] + #[inline] + pub fn dual_axis_magnitude(&self, value: Vec2) -> f32 { + match self { + Self::Analog => value.length(), + Self::Digital => f32::from(value != Vec2::ZERO), + } + } +} + +/// The axes for dual-axis inputs. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum DualAxis { + /// The X-axis (typically horizontal movement). + X, + + /// The Y-axis (typically vertical movement). + Y, +} + +impl DualAxis { + /// Gets the component on the specified axis. + #[must_use] + #[inline] + pub fn value(&self, value: Vec2) -> f32 { + match self { + Self::X => value.x, + Self::Y => value.y, + } + } +} + +/// The directions for dual-axis inputs. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum DualAxisDirection { + /// Upward direction. + Up, + + /// Downward direction. + Down, + + /// Leftward direction. + Left, + + /// Rightward direction. + Right, +} + +impl DualAxisDirection { + /// Checks if the given `value` is active in the specified direction. + #[must_use] + #[inline] + pub fn is_active(&self, value: Vec2) -> bool { + match self { + Self::Up => value.y > 0.0, + Self::Down => value.y < 0.0, + Self::Left => value.x < 0.0, + Self::Right => value.x > 0.0, + } + } +} + +/// A combined input data from two axes (X and Y). +/// +/// This struct stores the X and Y values as a [`Vec2`] in a device-agnostic way, +/// meaning it works consistently regardless of the specific input device (gamepad, joystick, etc.). +/// It assumes any calibration (deadzone correction, rescaling, drift correction, etc.) +/// has already been applied at an earlier stage of processing. +/// +/// The neutral origin of this input data is always at `(0, 0)`. +/// When working with gamepad axes, both X and Y values are typically bounded by `[-1.0, 1.0]`. +/// However, this range may not apply to other input types, such as mousewheel data which can have a wider range. +#[derive(Default, Debug, Copy, Clone, PartialEq, Deserialize, Serialize, Reflect)] +#[must_use] +pub struct DualAxisData(Vec2); + +impl DualAxisData { + /// Creates a [`DualAxisData`] with the given values. + pub const fn new(x: f32, y: f32) -> Self { + Self(Vec2::new(x, y)) + } + + /// Creates a [`DualAxisData`] directly from the given [`Vec2`]. + pub const fn from_xy(xy: Vec2) -> Self { + Self(xy) + } + + /// Combines the directional input from this [`DualAxisData`] with another. + /// + /// This is useful if you have multiple sticks bound to the same game action, + /// and you want to get their combined position. + /// + /// # Warning + /// + /// This method performs vector addition on the X and Y components. + /// While the direction is preserved, the combined magnitude might exceed the expected + /// range for certain input devices (e.g., gamepads typically have a maximum magnitude of `1.0`). + /// + /// To ensure the combined input stays within the expected range, + /// consider using [`Self::clamp_length`] on the returned value. + pub fn merged_with(&self, other: Self) -> Self { + Self::analog(self.0 + other.0) + } + + /// The value along the X-axis, typically ranging from `-1.0` to `1.0`. + #[must_use] + #[inline] + pub const fn x(&self) -> f32 { + self.0.x + } + + /// The value along the Y-axis, typically ranging from `-1.0` to `1.0`. + #[must_use] + #[inline] + pub const fn y(&self) -> f32 { + self.0.y + } + + /// The values along each axis, each typically ranging from `-1.0` to `1.0`. + #[must_use] + #[inline] + pub const fn xy(&self) -> Vec2 { + self.0 + } + + /// The [`Direction2d`] that this axis is pointing towards, if any + /// + /// If the axis is neutral (x, y) = (0, 0), this will be `None`. + #[must_use] + #[inline] + pub fn direction(&self) -> Option { + Direction2d::new(self.0).ok() + } + + /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any + /// + /// If the axis is neutral (x, y) = (0, 0), this will be `None`. + #[must_use] + #[inline] + pub fn rotation(&self) -> Option { + Rotation::from_xy(self.0).ok() + } + + /// How far from the origin is this axis's position? + /// + /// Typically bounded by 0 and 1. + /// + /// If you only need to compare relative magnitudes, use [`Self::length_squared`] instead for faster computation. + #[must_use] + #[inline] + pub fn length(&self) -> f32 { + self.0.length() + } + + /// The square of the axis' magnitude + /// + /// Typically bounded by 0 and 1. + /// + /// This is faster than [`Self::length`], as it avoids a square root, but will generally have less natural behavior. + #[must_use] + #[inline] + pub fn length_squared(&self) -> f32 { + self.0.length_squared() + } + + /// Clamps the magnitude of the axis + pub fn clamp_length(&mut self, max: f32) { + self.0 = self.0.clamp_length_max(max); + } +} + +impl From for Vec2 { + fn from(data: DualAxisData) -> Vec2 { + data.0 + } +} diff --git a/src/user_inputs/chord.rs b/src/user_inputs/chord.rs index f9527c91..ec9fbfe7 100644 --- a/src/user_inputs/chord.rs +++ b/src/user_inputs/chord.rs @@ -15,7 +15,14 @@ use crate::user_inputs::UserInput; /// When it treated as a single-axis input, it uses the sum of values from all inner single-axis inputs. /// When it treated as a dual-axis input, it only uses the value of the first inner dual-axis input. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub struct ChordInput(Vec>); +pub struct ChordInput( + // Note: we cannot use a HashSet here because of https://users.rust-lang.org/t/hash-not-implemented-why-cant-it-be-derived/92416/8 + // We cannot use a BTreeSet because the underlying types don't impl Ord + // We don't want to use a PetitSet here because of memory bloat + // So a vec it is! + // RIP your uniqueness guarantees + Vec>, +); // #[serde_typetag] impl UserInput for ChordInput { diff --git a/src/user_inputs/keyboard.rs b/src/user_inputs/keyboard.rs index 999f2d10..22236a9e 100644 --- a/src/user_inputs/keyboard.rs +++ b/src/user_inputs/keyboard.rs @@ -4,10 +4,12 @@ use bevy::prelude::{KeyCode, Reflect, Vec2}; use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; +use crate as leafwing_input_manager; use crate::input_streams::InputStreams; +use crate::user_inputs::axislike::DualAxisData; use crate::user_inputs::UserInput; -// Built-in support for KeyCode +// Built-in support for Bevy's KeyCode #[serde_typetag] impl UserInput for KeyCode { /// Checks if the specified [`KeyCode`] is currently pressed down. @@ -18,25 +20,15 @@ impl UserInput for KeyCode { .is_some_and(|keys| keys.pressed(*self)) } - /// Retrieves the strength of the key press for the specified [`KeyCode`]. - /// - /// # Returns - /// - /// - `1.0`: The key is currently pressed down, indicating an active input. - /// - `0.0`: The key is not pressed, signifying no input. + /// Retrieves the strength of the key press for the specified [`KeyCode`], + /// returning `0.0` for no press and `1.0` for a currently pressed key. #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { f32::from(self.is_active(input_streams)) } - - /// Always returns [`None`] as [`KeyCode`]s don't represent dual-axis input. - #[inline] - fn axis_pair(&self, _input_streams: &InputStreams) -> Option { - None - } } -/// Defines different keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). +/// Keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). /// /// Each variant represents a pair of [`KeyCode`]s, the left and right version of the modifier key, /// allowing for handling modifiers regardless of which side is pressed. @@ -86,11 +78,6 @@ impl ModifierKey { #[serde_typetag] impl UserInput for ModifierKey { /// Checks if the specified [`ModifierKey`] is currently pressed down. - /// - /// # Returns - /// - /// - `true`: The key is currently pressed down, indicating an active input. - /// - `false`: The key is not pressed, signifying no input. #[inline] fn is_active(&self, input_streams: &InputStreams) -> bool { let modifiers = self.keys(); @@ -99,20 +86,10 @@ impl UserInput for ModifierKey { .is_some_and(|keys| keys.pressed(modifiers[0]) | keys.pressed(modifiers[1])) } - /// Gets the strength of the key press for the specified [`ModifierKey`]. - /// - /// # Returns - /// - /// - `1.0`: The key is currently pressed down, indicating an active input. - /// - `0.0`: The key is not pressed, signifying no input. + /// Gets the strength of the key press for the specified [`ModifierKey`], + /// returning `0.0` for no press and `1.0` for a currently pressed key. #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { f32::from(self.is_active(input_streams)) } - - /// Always returns [`None`] as [`ModifierKey`]s don't represent dual-axis input. - #[inline] - fn axis_pair(&self, _input_streams: &InputStreams) -> Option { - None - } } diff --git a/src/user_inputs/mod.rs b/src/user_inputs/mod.rs index 1a501d01..f08131a7 100644 --- a/src/user_inputs/mod.rs +++ b/src/user_inputs/mod.rs @@ -47,16 +47,24 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_flexitos::ser::require_erased_serialize_impl; use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; +use self::axislike::DualAxisData; use crate::input_streams::InputStreams; +use crate::orientation::Rotation; use crate::typetag::RegisterTypeTag; +pub use self::chord::*; +pub use self::gamepad::*; +pub use self::keyboard::*; +pub use self::mouse::*; + +pub mod axislike; pub mod chord; pub mod gamepad; pub mod keyboard; pub mod mouse; #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] -pub enum InputKind { +pub enum UserInputDataType { Button, Axis, DualAxis, @@ -64,19 +72,25 @@ pub enum InputKind { /// A trait for defining the behavior expected from different user input sources. /// -/// Implementers of this trait should provide methods for accessing and -/// processing user input data. +/// Implementers of this trait should provide methods for accessing and processing user input data. pub trait UserInput: Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize { - /// Checks if the user input is currently active. + /// Checks if the input is currently active. fn is_active(&self, input_streams: &InputStreams) -> bool; - /// Retrieves the current value of the user input. + /// Retrieves the current value of the input. fn value(&self, input_streams: &InputStreams) -> f32; - /// Retrieves the current dual-axis value of the user input if available. - fn axis_pair(&self, input_streams: &InputStreams) -> Option; + /// Attempts to retrieve the current [`DualAxisData`] of the input if applicable. + /// + /// This method is intended for inputs that represent movement on two axes. + /// However, some input types (e.g., buttons, mouse scroll) don't inherently provide separate X and Y information. + /// + /// For implementers that don't represent dual-axis input, this method should always return [`None`]. + fn axis_pair(&self, _input_streams: &InputStreams) -> Option { + None + } } dyn_clone::clone_trait_object!(UserInput); diff --git a/src/user_inputs/mouse.rs b/src/user_inputs/mouse.rs index 0bc095c6..1ca71ee7 100644 --- a/src/user_inputs/mouse.rs +++ b/src/user_inputs/mouse.rs @@ -1,43 +1,595 @@ //! Mouse inputs -use bevy::prelude::{Reflect, Vec2}; +use bevy::prelude::{MouseButton, Reflect, Vec2}; use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; +use crate as leafwing_input_manager; use crate::input_processing::*; use crate::input_streams::InputStreams; +use crate::user_inputs::axislike::{AxisInputMode, DualAxis, DualAxisData, DualAxisDirection}; use crate::user_inputs::UserInput; -/// Represents mouse motion input, combining individual axis movements into a single value. -/// -/// It uses an internal [`DualAxisProcessor`] to process raw mouse movement data -/// from multiple mouse motion events into a combined representation. -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub struct MouseMotionInput(DualAxisProcessor); +// Built-in support for Bevy's MouseButton +impl UserInput for MouseButton { + /// Checks if the specified [`MouseButton`] is currently pressed down. + fn is_active(&self, input_streams: &InputStreams) -> bool { + input_streams + .mouse_buttons + .is_some_and(|buttons| buttons.pressed(*self)) + } + + /// Retrieves the strength of the button press for the specified [`MouseButton`]. + /// + /// # Returns + /// + /// - `1.0` if the button is currently pressed down, indicating an active input. + /// - `0.0` if the button is not pressed, signifying no input. + fn value(&self, input_streams: &InputStreams) -> f32 { + f32::from(self.is_active(input_streams)) + } + + /// Always returns [`None`] as [`MouseButton`]s don't represent dual-axis input. + fn axis_pair(&self, _input_streams: &InputStreams) -> Option { + None + } +} + +/// Retrieves the total mouse displacement. +#[must_use] +#[inline] +fn accumulate_mouse_movement(input_streams: &InputStreams) -> Vec2 { + // PERF: this summing is computed for every individual input + // This should probably be computed once, and then cached / read + // Fix upstream! + input_streams + .mouse_motion + .iter() + .map(|event| event.delta) + .sum() +} + +/// Input associated with mouse movement on both X and Y axes. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseMove { + /// The [`AxisInputMode`] for both axes. + pub(crate) input_mode: AxisInputMode, + + /// The [`DualAxisProcessor`] used to handle input values. + pub(crate) processor: DualAxisProcessor, +} + +impl MouseMove { + /// Creates a [`MouseMove`] for continuous movement on X and Y axes without any processing applied. + pub const fn analog() -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: DualAxisProcessor::None, + } + } + + /// Creates a [`MouseMove`] for discrete movement on X and Y axes without any processing applied. + pub const fn digital() -> Self { + Self { + input_mode: AxisInputMode::Digital, + processor: DualAxisProcessor::None, + } + } + + /// Creates a [`MouseMove`] for continuous movement on X and Y axes with the specified [`DualAxisProcessor`]. + pub fn analog_using(processor: impl Into) -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseMove`] for discrete movement on X and Y axes with the specified [`DualAxisProcessor`]. + pub fn digital_using(processor: impl Into) -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Appends the given [`DualAxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self + } + + /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); + self + } + + /// Removes the current used [`DualAxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = DualAxisProcessor::None; + self + } +} + +#[serde_typetag] +impl UserInput for MouseMove { + /// Checks if there is any recent mouse movement. + #[must_use] + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let movement = accumulate_mouse_movement(input_streams); + self.processor.process(movement) != Vec2::ZERO + } + + /// Retrieves the amount of the mouse movement + /// after processing by the associated [`DualAxisProcessor`]. + #[must_use] + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + let movement = accumulate_mouse_movement(input_streams); + let value = self.processor.process(movement); + self.input_mode.dual_axis_magnitude(value) + } + + /// Retrieves the mouse displacement + /// after processing by the associated [`DualAxisProcessor`]. + #[must_use] + #[inline] + fn axis_pair(&self, input_streams: &InputStreams) -> Option { + let movement = accumulate_mouse_movement(input_streams); + let value = self.input_mode.dual_axis_value(movement); + let value = self.processor.process(value); + Some(DualAxisData::from_xy(value)) + } +} + +/// Input associated with mouse movement on a specific axis. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseMoveAxis { + /// The axis that this input tracks. + pub(crate) axis: DualAxis, + + /// The [`AxisInputMode`] for the axis. + pub(crate) input_mode: AxisInputMode, + + /// The [`AxisProcessor`] used to handle input values. + pub(crate) processor: AxisProcessor, +} + +impl MouseMoveAxis { + /// Creates a [`MouseMoveAxis`] for continuous movement on the X-axis without any processing applied. + pub const fn analog_x() -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseMoveAxis`] for continuous movement on the Y-axis without any processing applied. + pub const fn analog_y() -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseMoveAxis`] for discrete movement on the X-axis without any processing applied. + pub const fn digital_x() -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Digital, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseMoveAxis`] for discrete movement on the Y-axis without any processing applied. + pub const fn digital_y() -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Digital, + processor: AxisProcessor::None, + } + } + /// Creates a [`MouseMoveAxis`] for continuous movement on the X-axis with the specified [`AxisProcessor`]. + pub fn analog_x_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseMoveAxis`] for continuous movement on the Y-axis with the specified [`AxisProcessor`]. + pub fn analog_y_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseMoveAxis`] for discrete movement on the X-axis with the specified [`AxisProcessor`]. + pub fn digital_x_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseMoveAxis`] for discrete movement on the Y-axis with the specified [`AxisProcessor`]. + pub fn digital_y_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Appends the given [`AxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self + } + + /// Replaces the current [`AxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); + self + } + + /// Removes the current used [`AxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = AxisProcessor::None; + self + } +} + +#[serde_typetag] +impl UserInput for MouseMoveAxis { + /// Checks if there is any recent mouse movement along the specified axis. + #[must_use] + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let movement = accumulate_mouse_movement(input_streams); + let value = self.axis.value(movement); + self.processor.process(value) != 0.0 + } + + /// Retrieves the amount of the mouse movement along the specified axis + /// after processing by the associated [`AxisProcessor`]. + #[must_use] + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + let movement = accumulate_mouse_movement(input_streams); + let value = self.axis.value(movement); + let value = self.processor.process(value); + self.input_mode.axis_value(value) + } +} + +/// Input associated with mouse movement on a specific direction, treated as a button press. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseMoveDirection(DualAxisDirection); + +impl MouseMoveDirection { + /// Movement in the upward direction. + const UP: Self = Self(DualAxisDirection::Up); + + /// Movement in the downward direction. + const DOWN: Self = Self(DualAxisDirection::Down); + + /// Movement in the leftward direction. + const LEFT: Self = Self(DualAxisDirection::Left); + + /// Movement in the rightward direction. + const RIGHT: Self = Self(DualAxisDirection::Right); +} #[serde_typetag] -impl UserInput for MouseMotionInput { - /// Checks if there is active mouse motion. +impl UserInput for MouseMoveDirection { + /// Checks if there is any recent mouse movement along the specified direction. + #[must_use] #[inline] fn is_active(&self, input_streams: &InputStreams) -> bool { - self.axis_pair(input_streams).unwrap() != Vec2::ZERO + let movement = accumulate_mouse_movement(input_streams); + self.0.is_active(movement) } - /// Retrieves the magnitude of the mouse motion, representing the overall amount of movement. + /// Retrieves the amount of the mouse movement along the specified direction, + /// returning `0.0` for no movement and `1.0` for a currently active direction. + #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { - self.axis_pair(input_streams).unwrap().length() + f32::from(self.is_active(input_streams)) + } +} + +/// Accumulates the mouse wheel movement. +#[must_use] +#[inline] +fn accumulate_wheel_movement(input_streams: &InputStreams) -> Vec2 { + let Some(wheel) = &input_streams.mouse_wheel else { + return Vec2::ZERO; + }; + + wheel.iter().map(|event| Vec2::new(event.x, event.y)).sum() +} + +/// Input associated with mouse wheel movement on both X and Y axes. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseScroll { + /// The [`AxisInputMode`] for both axes. + pub(crate) input_mode: AxisInputMode, + + /// The [`DualAxisProcessor`] used to handle input values. + pub(crate) processor: DualAxisProcessor, +} + +impl MouseScroll { + /// Creates a [`MouseScroll`] for continuous movement on X and Y axes without any processing applied. + pub const fn analog() -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: DualAxisProcessor::None, + } } - /// Retrieves the accumulated mouse displacement. + /// Creates a [`MouseScroll`] for discrete movement on X and Y axes without any processing applied. + pub const fn digital() -> Self { + Self { + input_mode: AxisInputMode::Digital, + processor: DualAxisProcessor::None, + } + } + + /// Creates a [`MouseScroll`] for continuous movement on X and Y axes with the specified [`DualAxisProcessor`]. + pub fn analog_using(processor: impl Into) -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseScroll`] for discrete movement on X and Y axes with the specified [`DualAxisProcessor`]. + pub fn digital_using(processor: impl Into) -> Self { + Self { + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Appends the given [`DualAxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self + } + + /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. #[inline] - fn axis_pair(&self, input_streams: &InputStreams) -> Option { - let value = input_streams - .mouse_motion - .iter() - .map(|event| event.delta) - .sum(); - let value = self.0.process(value); - Some(value) + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); + self + } + + /// Removes the current used [`DualAxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = DualAxisProcessor::None; + self + } +} + +#[serde_typetag] +impl UserInput for MouseScroll { + /// Checks if there is any recent mouse wheel movement. + #[must_use] + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let movement = accumulate_wheel_movement(input_streams); + self.processor.process(movement) != Vec2::ZERO + } + + /// Retrieves the amount of the mouse wheel movement on both axes + /// after processing by the associated [`DualAxisProcessor`]. + #[must_use] + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + let movement = accumulate_wheel_movement(input_streams); + let value = self.processor.process(movement); + self.input_mode.dual_axis_magnitude(value) + } + + /// Retrieves the mouse scroll movement on both axes + /// after processing by the associated [`DualAxisProcessor`]. + #[must_use] + #[inline] + fn axis_pair(&self, input_streams: &InputStreams) -> Option { + let movement = accumulate_wheel_movement(input_streams); + let value = self.input_mode.dual_axis_value(movement); + let value = self.processor.process(value); + Some(DualAxisData::from_xy(value)) + } +} + +/// Input associated with mouse wheel movement on a specific axis. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseScrollAxis { + /// The axis that this input tracks. + pub(crate) axis: DualAxis, + + /// The [`AxisInputMode`] for the axis. + pub(crate) input_mode: AxisInputMode, + + /// The [`AxisProcessor`] used to handle input values. + pub(crate) processor: AxisProcessor, +} + +impl MouseScrollAxis { + /// Creates a [`MouseScrollAxis`] for continuous movement on the X-axis without any processing applied. + pub const fn analog_x() -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseScrollAxis`] for continuous movement on the Y-axis without any processing applied. + pub const fn analog_y() -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseScrollAxis`] for discrete movement on the X-axis without any processing applied. + pub const fn digital_x() -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Digital, + processor: AxisProcessor::None, + } + } + + /// Creates a [`MouseScrollAxis`] for discrete movement on the Y-axis without any processing applied. + pub const fn digital_y() -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Digital, + processor: AxisProcessor::None, + } + } + /// Creates a [`MouseScrollAxis`] for continuous movement on the X-axis with the specified [`AxisProcessor`]. + pub fn analog_x_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseScrollAxis`] for continuous movement on the Y-axis with the specified [`AxisProcessor`]. + pub fn analog_y_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseScrollAxis`] for discrete movement on the X-axis with the specified [`AxisProcessor`]. + pub fn digital_x_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::X, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Creates a [`MouseScrollAxis`] for discrete movement on the Y-axis with the specified [`AxisProcessor`]. + pub fn digital_y_using(processor: impl Into) -> Self { + Self { + axis: DualAxis::Y, + input_mode: AxisInputMode::Analog, + processor: processor.into(), + } + } + + /// Appends the given [`AxisProcessor`] as the next processing step. + #[inline] + pub fn with_processor(mut self, processor: impl Into) -> Self { + self.processor = self.processor.with_processor(processor); + self + } + + /// Replaces the current [`AxisProcessor`] with the specified `processor`. + #[inline] + pub fn replace_processor(mut self, processor: impl Into) -> Self { + self.processor = processor.into(); + self + } + + /// Removes the current used [`AxisProcessor`]. + #[inline] + pub fn no_processor(mut self) -> Self { + self.processor = AxisProcessor::None; + self + } +} + +#[serde_typetag] +impl UserInput for MouseScrollAxis { + /// Checks if there is any recent mouse wheel movement along the specified axis. + #[must_use] + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let movement = accumulate_wheel_movement(input_streams); + let value = self.axis.value(movement); + self.processor.process(value) != 0.0 + } + + /// Retrieves the amount of the mouse wheel movement along the specified axis + /// after processing by the associated [`AxisProcessor`]. + #[must_use] + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + let movement = accumulate_wheel_movement(input_streams); + let value = self.axis.value(movement); + let value = self.input_mode.axis_value(value); + self.processor.process(value) + } +} + +/// Input associated with mouse wheel movement on a specific direction, treated as a button press. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub struct MouseScrollDirection(DualAxisDirection); + +impl MouseScrollDirection { + /// Movement in the upward direction. + const UP: Self = Self(DualAxisDirection::Up); + + /// Movement in the downward direction. + const DOWN: Self = Self(DualAxisDirection::Down); + + /// Movement in the leftward direction. + const LEFT: Self = Self(DualAxisDirection::Left); + + /// Movement in the rightward direction. + const RIGHT: Self = Self(DualAxisDirection::Right); +} + +#[serde_typetag] +impl UserInput for MouseMoveDirection { + /// Checks if there is any recent mouse wheel movement along the specified direction. + #[must_use] + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let movement = accumulate_wheel_movement(input_streams); + self.0.is_active(movement) + } + + /// Retrieves the magnitude of the mouse wheel movement along the specified direction, + /// returning `0.0` for no movement and `1.0` for a currently active direction. + #[must_use] + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + f32::from(self.is_active(input_streams)) } } From f18ff69c87b0320a43fc8f8df2431eb5b8542cdc Mon Sep 17 00:00:00 2001 From: Shute052 Date: Mon, 29 Apr 2024 08:11:49 +0800 Subject: [PATCH 05/20] Prefer `FromIter<*AxisProcessor>` over `From>` --- examples/input_processing.rs | 2 +- src/input_processing/dual_axis/mod.rs | 14 +++++++------- src/input_processing/single_axis/mod.rs | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/input_processing.rs b/examples/input_processing.rs index d1c6407b..e4ba17d0 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -46,7 +46,7 @@ fn spawn_player(mut commands: Commands) { .insert( Action::LookAround, // You can also use a sequence of processors as the processing pipeline. - DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from(vec![ + DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from_iter([ // The first processor is a circular deadzone. CircleDeadZone::new(0.1).into(), // The next processor doubles inputs normalized by the deadzone. diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index 8cb83b21..ff52a9d0 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -72,7 +72,7 @@ pub enum DualAxisProcessor { /// /// assert_eq!( /// expected, - /// DualAxisProcessor::from(vec![ + /// DualAxisProcessor::from_iter([ /// DualAxisInverted::ALL.into(), /// DualAxisSensitivity::all(2.0).into(), /// ]) @@ -136,9 +136,9 @@ impl DualAxisProcessor { } } -impl From> for DualAxisProcessor { - fn from(value: Vec) -> Self { - Self::Pipeline(value.into_iter().map(Arc::new).collect()) +impl FromIterator for DualAxisProcessor { + fn from_iter>(iter: T) -> Self { + Self::Pipeline(iter.into_iter().map(Arc::new).collect()) } } @@ -315,17 +315,17 @@ mod tests { #[test] fn test_dual_axis_processor_from_list() { assert_eq!( - DualAxisProcessor::from(vec![]), + DualAxisProcessor::from_iter([]), DualAxisProcessor::Pipeline(vec![]) ); assert_eq!( - DualAxisProcessor::from(vec![DualAxisInverted::ALL.into()]), + DualAxisProcessor::from_iter([DualAxisInverted::ALL.into()]), DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) ); assert_eq!( - DualAxisProcessor::from(vec![ + DualAxisProcessor::from_iter([ DualAxisInverted::ALL.into(), DualAxisSensitivity::all(2.0).into(), ]), diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs index 79ef51b8..eb4adbab 100644 --- a/src/input_processing/single_axis/mod.rs +++ b/src/input_processing/single_axis/mod.rs @@ -79,7 +79,7 @@ pub enum AxisProcessor { /// /// assert_eq!( /// expected, - /// AxisProcessor::from(vec![ + /// AxisProcessor::from_iter([ /// AxisProcessor::Inverted, /// AxisProcessor::Sensitivity(2.0), /// ]) @@ -141,9 +141,9 @@ impl AxisProcessor { } } -impl From> for AxisProcessor { - fn from(value: Vec) -> Self { - Self::Pipeline(value.into_iter().map(Arc::new).collect()) +impl FromIterator for AxisProcessor { + fn from_iter>(iter: T) -> Self { + Self::Pipeline(iter.into_iter().map(Arc::new).collect()) } } @@ -209,18 +209,18 @@ mod tests { #[test] fn test_axis_processor_from_list() { - assert_eq!(AxisProcessor::from(vec![]), AxisProcessor::Pipeline(vec![])); + assert_eq!( + AxisProcessor::from_iter([]), + AxisProcessor::Pipeline(vec![]) + ); assert_eq!( - AxisProcessor::from(vec![AxisProcessor::Inverted]), + AxisProcessor::from_iter([AxisProcessor::Inverted]), AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), ); assert_eq!( - AxisProcessor::from(vec![ - AxisProcessor::Inverted, - AxisProcessor::Sensitivity(2.0), - ]), + AxisProcessor::from_iter([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0),]), AxisProcessor::Pipeline(vec![ Arc::new(AxisProcessor::Inverted), Arc::new(AxisProcessor::Sensitivity(2.0)), From a1c166cffdc56e2a16549bf0379ded8bbd726627 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Mon, 29 Apr 2024 08:17:30 +0800 Subject: [PATCH 06/20] Implement `GamepadButtonType` and `GamepadAxisType` --- src/user_inputs/gamepad.rs | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/user_inputs/gamepad.rs b/src/user_inputs/gamepad.rs index 8f01f48d..6522051f 100644 --- a/src/user_inputs/gamepad.rs +++ b/src/user_inputs/gamepad.rs @@ -1 +1,95 @@ //! Gamepad inputs + +use bevy::prelude::{Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType}; +use leafwing_input_manager_macros::serde_typetag; + +use crate as leafwing_input_manager; +use crate::input_streams::InputStreams; +use crate::user_inputs::UserInput; + +// Built-in support for Bevy's GamepadButtonType. +#[serde_typetag] +impl UserInput for GamepadButtonType { + /// Checks if the specified [`GamepadButtonType`] is currently pressed down. + /// + /// When a [`Gamepad`] is specified, only checks if the button is pressed on the gamepad. + /// Otherwise, checks if the button is pressed on any connected gamepads. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + let gamepad_pressed_self = |gamepad: Gamepad| -> bool { + let button = GamepadButton::new(gamepad, *self); + input_streams.gamepad_buttons.pressed(button) + }; + + if let Some(gamepad) = input_streams.associated_gamepad { + gamepad_pressed_self(gamepad) + } else { + input_streams.gamepads.iter().any(gamepad_pressed_self) + } + } + + /// Retrieves the strength of the button press for the specified [`GamepadButtonType`]. + /// + /// When a [`Gamepad`] is specified, only retrieves the value of the button on the gamepad. + /// Otherwise, retrieves the value on any connected gamepads. + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + // This implementation differs from `is_active()` because the upstream bevy::input + // still waffles about whether triggers are buttons or axes. + // So, we consider the axes for consistency with other gamepad axes (e.g., thumbs ticks). + + let gamepad_value_self = |gamepad: Gamepad| -> Option { + let button = GamepadButton::new(gamepad, *self); + input_streams.gamepad_button_axes.get(button) + }; + + if let Some(gamepad) = input_streams.associated_gamepad { + gamepad_value_self(gamepad).unwrap_or_else(|| f32::from(self.is_active(input_streams))) + } else { + input_streams + .gamepads + .iter() + .map(gamepad_value_self) + .flatten() + .find(|value| *value != 0.0) + .unwrap_or_default() + } + } +} + +// Built-in support for Bevy's GamepadAxisType. +// #[serde_typetag] +impl UserInput for GamepadAxisType { + /// Checks if the specified [`GamepadAxisType`] is currently active. + /// + /// When a [`Gamepad`] is specified, only checks if the axis is triggered on the gamepad. + /// Otherwise, checks if the axis is triggered on any connected gamepads. + #[inline] + fn is_active(&self, input_streams: &InputStreams) -> bool { + self.value(input_streams) != 0.0 + } + + /// Retrieves the strength of the specified [`GamepadAxisType`]. + /// + /// When a [`Gamepad`] is specified, only retrieves the value of the axis on the gamepad. + /// Otherwise, retrieves the axis on any connected gamepads. + #[inline] + fn value(&self, input_streams: &InputStreams) -> f32 { + let gamepad_value_self = |gamepad: Gamepad| -> Option { + let axis = GamepadAxis::new(gamepad, *self); + input_streams.gamepad_axes.get(axis) + }; + + if let Some(gamepad) = input_streams.associated_gamepad { + gamepad_value_self(gamepad).unwrap_or_else(|| f32::from(self.is_active(input_streams))) + } else { + input_streams + .gamepads + .iter() + .map(gamepad_value_self) + .flatten() + .find(|value| *value != 0.0) + .unwrap_or_default() + } + } +} From 1f1a87893395c122dedcb9bf99cc5339e0cf0751 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 12:03:53 +0800 Subject: [PATCH 07/20] Add a `pipeline` method to create input processing pipelines --- RELEASES.md | 4 ++-- examples/input_processing.rs | 2 +- src/input_processing/dual_axis/mod.rs | 32 ++++++++++++++++++++++--- src/input_processing/mod.rs | 4 ++-- src/input_processing/single_axis/mod.rs | 27 ++++++++++++++++++--- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index e6bee84b..75ac970c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -37,8 +37,8 @@ Input processors allow you to create custom logic for axis-like input manipulati - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. - you can also create them by these methods: - - `AxisProcessor::with_processor` or `FromIterator::from_iter` for `AxisProcessor::Pipeline`. - - `DualAxisProcessor::with_processor` or `FromIterator::from_iter` for `DualAxisProcessor::Pipeline`. + - `AxisProcessor::pipeline` or `AxisProcessor::with_processor` for `AxisProcessor::Pipeline`. + - `DualAxisProcessor::pipeline` or `DualAxisProcessor::with_processor` for `DualAxisProcessor::Pipeline`. - Inversion: Reverses control (positive becomes negative, etc.) - `AxisProcessor::Inverted`: Single-axis inversion. - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. diff --git a/examples/input_processing.rs b/examples/input_processing.rs index 6607d574..fecd7bb3 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -45,7 +45,7 @@ fn spawn_player(mut commands: Commands) { .with( Action::LookAround, // You can also use a sequence of processors as the processing pipeline. - DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from_iter([ + DualAxis::mouse_motion().replace_processor(DualAxisProcessor::pipeline([ // The first processor is a circular deadzone. CircleDeadZone::new(0.1).into(), // The next processor doubles inputs normalized by the deadzone. diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index 7ba44375..c8a8c17f 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -53,7 +53,7 @@ pub enum DualAxisProcessor { /// one for the current step and the other for the next step. /// /// For a straightforward creation of a [`DualAxisProcessor::Pipeline`], - /// you can use [`DualAxisProcessor::with_processor`] or [`FromIterator::from_iter`] methods. + /// you can use [`DualAxisProcessor::pipeline`] or [`DualAxisProcessor::with_processor`] methods. /// /// ```rust /// use std::sync::Arc; @@ -83,6 +83,12 @@ pub enum DualAxisProcessor { } impl DualAxisProcessor { + /// Creates a [`DualAxisProcessor::Pipeline`] from the given `processors`. + #[inline] + pub fn pipeline(processors: impl IntoIterator) -> Self { + Self::from_iter(processors) + } + /// Computes the result by processing the `input_value`. #[must_use] #[inline] @@ -522,7 +528,7 @@ mod tests { use super::*; #[test] - fn test_axis_processor_pipeline() { + fn test_axis_processing_pipeline() { let pipeline = DualAxisProcessor::Pipeline(vec![ Arc::new(DualAxisInverted::ALL.into()), Arc::new(DualAxisSensitivity::all(2.0).into()), @@ -540,17 +546,37 @@ mod tests { } #[test] - fn test_dual_axis_processor_from_iter() { + fn test_dual_axis_processing_pipeline_creation() { + assert_eq!( + DualAxisProcessor::pipeline([]), + DualAxisProcessor::Pipeline(vec![]) + ); assert_eq!( DualAxisProcessor::from_iter([]), DualAxisProcessor::Pipeline(vec![]) ); + assert_eq!( + DualAxisProcessor::pipeline([DualAxisInverted::ALL.into()]), + DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) + ); + assert_eq!( DualAxisProcessor::from_iter([DualAxisInverted::ALL.into()]), DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) ); + assert_eq!( + DualAxisProcessor::pipeline([ + DualAxisInverted::ALL.into(), + DualAxisSensitivity::all(2.0).into(), + ]), + DualAxisProcessor::Pipeline(vec![ + Arc::new(DualAxisInverted::ALL.into()), + Arc::new(DualAxisSensitivity::all(2.0).into()), + ]) + ); + assert_eq!( DualAxisProcessor::from_iter([ DualAxisInverted::ALL.into(), diff --git a/src/input_processing/mod.rs b/src/input_processing/mod.rs index 22d055ba..c04bff79 100644 --- a/src/input_processing/mod.rs +++ b/src/input_processing/mod.rs @@ -26,8 +26,8 @@ //! //! You can also use these methods to create a pipeline. //! -//! - [`AxisProcessor::with_processor`] or [`FromIterator::from_iter`] for [`AxisProcessor::Pipeline`]. -//! - [`DualAxisProcessor::with_processor`] or [`FromIterator::from_iter`] for [`DualAxisProcessor::Pipeline`]. +//! - [`AxisProcessor::pipeline`] or [`AxisProcessor::with_processor`] for [`AxisProcessor::Pipeline`]. +//! - [`DualAxisProcessor::pipeline`] or [`DualAxisProcessor::with_processor`] for [`DualAxisProcessor::Pipeline`]. //! //! ## Inversion //! diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs index 85139a8e..4d1ad4bd 100644 --- a/src/input_processing/single_axis/mod.rs +++ b/src/input_processing/single_axis/mod.rs @@ -61,7 +61,7 @@ pub enum AxisProcessor { /// Processes input values sequentially through a sequence of [`AxisProcessor`]s. /// /// For a straightforward creation of a [`AxisProcessor::Pipeline`], - /// you can use [`AxisProcessor::with_processor`] or [`FromIterator::from_iter`] methods. + /// you can use [`AxisProcessor::pipeline`] or [`AxisProcessor::with_processor`] methods. /// /// ```rust /// use std::sync::Arc; @@ -92,6 +92,12 @@ pub enum AxisProcessor { } impl AxisProcessor { + /// Creates an [`AxisProcessor::Pipeline`] from the given `processors`. + #[inline] + pub fn pipeline(processors: impl IntoIterator) -> Self { + Self::from_iter(processors) + } + /// Computes the result by processing the `input_value`. #[must_use] #[inline] @@ -270,7 +276,7 @@ mod tests { } #[test] - fn test_axis_processor_pipeline() { + fn test_axis_processing_pipeline() { let pipeline = AxisProcessor::Pipeline(vec![ Arc::new(AxisProcessor::Inverted), Arc::new(AxisProcessor::Sensitivity(2.0)), @@ -284,17 +290,32 @@ mod tests { } #[test] - fn test_axis_processor_from_iter() { + fn test_axis_processing_pipeline_creation() { + assert_eq!(AxisProcessor::pipeline([]), AxisProcessor::Pipeline(vec![])); + assert_eq!( AxisProcessor::from_iter([]), AxisProcessor::Pipeline(vec![]) ); + assert_eq!( + AxisProcessor::pipeline([AxisProcessor::Inverted]), + AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), + ); + assert_eq!( AxisProcessor::from_iter([AxisProcessor::Inverted]), AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), ); + assert_eq!( + AxisProcessor::pipeline([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0)]), + AxisProcessor::Pipeline(vec![ + Arc::new(AxisProcessor::Inverted), + Arc::new(AxisProcessor::Sensitivity(2.0)), + ]) + ); + assert_eq!( AxisProcessor::from_iter([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0)]), AxisProcessor::Pipeline(vec![ From 6872e2d8f5be7c9a2b642e2a978dac2dfa0bc821 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 12:14:48 +0800 Subject: [PATCH 08/20] Typo --- src/input_processing/dual_axis/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index c8a8c17f..2bdd9603 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -528,7 +528,7 @@ mod tests { use super::*; #[test] - fn test_axis_processing_pipeline() { + fn test_dual_axis_processing_pipeline() { let pipeline = DualAxisProcessor::Pipeline(vec![ Arc::new(DualAxisInverted::ALL.into()), Arc::new(DualAxisSensitivity::all(2.0).into()), From 72adc5bda35569d38208cb4a8dee632c053ca3e7 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 13:45:12 +0800 Subject: [PATCH 09/20] Replace old `UserInput` and `InputKind` with the new ones --- .github/workflows/ci.yml | 8 +- README.md | 6 +- RELEASE-CHECKLIST.md | 2 +- RELEASES.md | 104 +-- benches/input_map.rs | 27 +- examples/action_state_resource.rs | 10 +- examples/arpg_indirection.rs | 5 +- examples/axis_inputs.rs | 14 +- examples/clash_handling.rs | 15 +- examples/default_controls.rs | 4 +- examples/input_processing.rs | 27 +- examples/mouse_motion.rs | 4 +- examples/mouse_wheel.rs | 21 +- examples/multiplayer.rs | 6 +- examples/register_gamepads.rs | 3 +- examples/send_actions_over_network.rs | 7 +- examples/twin_stick_controller.rs | 8 +- examples/virtual_dpad.rs | 11 +- src/action_state.rs | 45 +- src/axislike.rs | 746 +++++++-------------- src/buttonlike.rs | 32 +- src/clashing_inputs.rs | 263 +++----- src/display_impl.rs | 50 -- src/input_map.rs | 611 +++++++++-------- src/input_mocking.rs | 558 +++++++++------ src/input_processing/dual_axis/circle.rs | 3 +- src/input_processing/dual_axis/custom.rs | 4 +- src/input_processing/dual_axis/mod.rs | 263 +++++++- src/input_processing/dual_axis/range.rs | 106 +-- src/input_processing/mod.rs | 8 +- src/input_processing/single_axis/custom.rs | 4 +- src/input_processing/single_axis/mod.rs | 105 ++- src/input_processing/single_axis/range.rs | 18 +- src/input_streams.rs | 313 --------- src/lib.rs | 6 +- src/plugin.rs | 39 +- src/systems.rs | 2 +- src/user_input.rs | 621 ----------------- src/user_inputs/axislike.rs | 244 ------- src/user_inputs/chord.rs | 55 -- src/user_inputs/gamepad.rs | 95 --- src/user_inputs/keyboard.rs | 95 --- src/user_inputs/mod.rs | 284 -------- src/user_inputs/mouse.rs | 595 ---------------- tests/clashes.rs | 46 +- tests/gamepad_axis.rs | 217 ++---- tests/integration.rs | 10 +- tests/mouse_motion.rs | 171 ++--- tests/mouse_wheel.rs | 150 ++--- 49 files changed, 1837 insertions(+), 4204 deletions(-) delete mode 100644 src/display_impl.rs delete mode 100644 src/user_input.rs delete mode 100644 src/user_inputs/axislike.rs delete mode 100644 src/user_inputs/chord.rs delete mode 100644 src/user_inputs/gamepad.rs delete mode 100644 src/user_inputs/keyboard.rs delete mode 100644 src/user_inputs/mod.rs delete mode 100644 src/user_inputs/mouse.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f32b75da..f4bbaa39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: toolchain: stable components: rustfmt, clippy - name: Cache Cargo build files - uses: Leafwing-Studios/cargo-cache@v1.1.0 + uses: Leafwing-Studios/cargo-cache@v1.2.0 - name: Install alsa and udev run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev - name: CI job @@ -34,7 +34,7 @@ jobs: with: toolchain: stable - name: Cache Cargo build files - uses: Leafwing-Studios/cargo-cache@v1.1.0 + uses: Leafwing-Studios/cargo-cache@v1.2.0 - name: Install alsa and udev run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: Build & run tests @@ -51,7 +51,7 @@ jobs: with: toolchain: stable - name: Cache Cargo build files - uses: Leafwing-Studios/cargo-cache@v1.1.0 + uses: Leafwing-Studios/cargo-cache@v1.2.0 - name: Install alsa and udev run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: Check Compile @@ -66,7 +66,7 @@ jobs: with: toolchain: stable - name: Cache Cargo build files - uses: Leafwing-Studios/cargo-cache@v1.1.0 + uses: Leafwing-Studios/cargo-cache@v1.2.0 - name: Install alsa and udev run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev if: runner.os == 'linux' diff --git a/README.md b/README.md index 5dcea207..2280b8cb 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ and a single input can result in multiple actions being triggered, which can be - Full keyboard, mouse and joystick support for button-like and axis inputs - Dual axis support for analog inputs from gamepads and joysticks -- Bind arbitrary button inputs into virtual DPads +- Bind arbitrary button inputs into virtual D-Pads - Effortlessly wire UI buttons to game state with one simple component! - When clicked, your button will press the appropriate action on the corresponding entity - Store all your input mappings in a single `InputMap` component @@ -36,13 +36,13 @@ and a single input can result in multiple actions being triggered, which can be - Ergonomic insertion API that seamlessly blends multiple input types for you - Can't decide between `input_map.insert(Action::Jump, KeyCode::Space)` and `input_map.insert(Action::Jump, GamepadButtonType::South)`? Have both! - Full support for arbitrary button combinations: chord your heart out. - - `input_map.insert_chord(Action::Console, [KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC])` + - `input_map.insert(Action::Console, InputChord::multiple([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))` - Sophisticated input disambiguation with the `ClashStrategy` enum: stop triggering individual buttons when you meant to press a chord! - Create an arbitrary number of strongly typed disjoint action sets by adding multiple copies of this plugin: decouple your camera and player state - Local multiplayer support: freely bind keys to distinct entities, rather than worrying about singular global state - Networked multiplayer support: serializable structs, and a space-conscious `ActionDiff` representation to send on the wire - Powerful and easy-to-use input mocking API for integration testing your Bevy applications - - `app.send_input(KeyCode::KeyB)` or `world.send_input(UserInput::chord([KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY])` + - `app.press_input(KeyCode::KeyB)` or `world.press_input(UserInput::chord([KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY])` - Control which state this plugin is active in: stop wandering around while in a menu! - Leafwing Studio's trademark `#![forbid(missing_docs)]` diff --git a/RELEASE-CHECKLIST.md b/RELEASE-CHECKLIST.md index 9096f8bc..40a93525 100644 --- a/RELEASE-CHECKLIST.md +++ b/RELEASE-CHECKLIST.md @@ -4,7 +4,7 @@ 1. Ensure that `reset_inputs` for `MutableInputStreams` is resetting all relevant fields. 2. Ensure that `RawInputs` struct has fields that cover all necessary input types. -3. Ensure that `send_input` and `release_input` check all possible fields on `RawInputs`. +3. Ensure that `press_input`, `send_axis_values` and `release_input` check all possible fields on `RawInputs`. ## Before release diff --git a/RELEASES.md b/RELEASES.md index 35e3a0da..75ac970c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,47 +5,75 @@ ### Breaking Changes - removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. -- added input processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad` to refine input values: - - added processor enums: - - `AxisProcessor`: Handles single-axis values. - - `DualAxisProcessor`: Handles dual-axis values. - - added processor traits for defining custom processors: - - `CustomAxisProcessor`: Handles single-axis values. - - `CustomDualAxisProcessor`: Handles dual-axis values. - - added built-in processor variants (no variant versions implemented `Into`): - - Pipelines: Handle input values sequentially through a sequence of processors. - - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. - - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. - - you can also create them by these methods: - - `AxisProcessor::with_processor` or `From>::from` for `AxisProcessor::Pipeline`. - - `DualAxisProcessor::with_processor` or `From>::from` for `DualAxisProcessor::Pipeline`. - - Inversion: Reverses control (positive becomes negative, etc.) - - `AxisProcessor::Inverted`: Single-axis inversion. - - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. - - Sensitivity: Adjusts control responsiveness (doubling, halving, etc.). - - `AxisProcessor::Sensitivity`: Single-axis scaling. - - `DualAxisSensitivity`: Dual-axis scaling, implemented `Into`. - - Value Bounds: Define the boundaries for constraining input values. - - `AxisBounds`: Restricts single-axis values to a range, implemented `Into` and `Into`. - - `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into`. - - `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into`. - - Deadzones: Ignores near-zero values, treating them as zero. - - Unscaled versions: - - `AxisExclusion`: Excludes small single-axis values, implemented `Into` and `Into`. - - `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into`. - - `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into`. - - Scaled versions: - - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. - - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. - - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. - - removed `DeadZoneShape`. - - removed functions for inverting, adjusting sensitivity, and creating deadzones from `SingleAxis` and `DualAxis`. - - added `with_processor`, `replace_processor`, and `no_processor` to manage processors for `SingleAxis`, `DualAxis`, `VirtualAxis`, and `VirtualDpad`. - - added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. - - added `serde_typetag` procedural macro attribute for processor type tagging. +- replaced axis-like input handling with new input processors (see 'Enhancements: Input Processors' for details). + - removed `DeadZoneShape` in favor of new dead zone processors. - made the dependency on bevy's `bevy_gilrs` feature optional. - it is still enabled by leafwing-input-manager's default features. - if you're using leafwing-input-manager with `default_features = false`, you can readd it by adding `bevy/bevy_gilrs` as a dependency. +- removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). +- renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. +- split `MockInput::send_input` method to two methods: + - `fn press_input(&self, input: UserInput)` for focusing on simulating button and key presses. + - `fn send_axis_values(&self, input: UserInput, values: impl IntoIterator)` for sending value changed events to each axis of the input. +- removed the hacky `value` field and `from_value` method from `SingleAxis` and `DualAxis`, in favor of new input mocking. + +### Enhancements + +#### Input Processors + +Input processors allow you to create custom logic for axis-like input manipulation. + +- added processor enums: + - `AxisProcessor`: Handles single-axis values. + - `DualAxisProcessor`: Handles dual-axis values. +- added processor traits for defining custom processors: + - `CustomAxisProcessor`: Handles single-axis values. + - `CustomDualAxisProcessor`: Handles dual-axis values. +- implemented `WithAxisProcessorExt` to manage processors for `SingleAxis` and `VirtualAxis`. +- implemented `WithDualAxisProcessorExt` to manage processors for `DualAxis` and `VirtualDpad`. +- added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. +- added built-in processors (variants of processor enums and `Into` implementors): + - Pipelines: Handle input values sequentially through a sequence of processors. + - `AxisProcessor::Pipeline`: Pipeline for single-axis inputs. + - `DualAxisProcessor::Pipeline`: Pipeline for dual-axis inputs. + - you can also create them by these methods: + - `AxisProcessor::pipeline` or `AxisProcessor::with_processor` for `AxisProcessor::Pipeline`. + - `DualAxisProcessor::pipeline` or `DualAxisProcessor::with_processor` for `DualAxisProcessor::Pipeline`. + - Inversion: Reverses control (positive becomes negative, etc.) + - `AxisProcessor::Inverted`: Single-axis inversion. + - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. + - Sensitivity: Adjusts control responsiveness (doubling, halving, etc.). + - `AxisProcessor::Sensitivity`: Single-axis scaling. + - `DualAxisSensitivity`: Dual-axis scaling, implemented `Into`. + - Value Bounds: Define the boundaries for constraining input values. + - `AxisBounds`: Restricts single-axis values to a range, implemented `Into` and `Into`. + - `DualAxisBounds`: Restricts single-axis values to a range along each axis, implemented `Into`. + - `CircleBounds`: Limits dual-axis values to a maximum magnitude, implemented `Into`. + - Deadzones: Ignores near-zero values, treating them as zero. + - Unscaled versions: + - `AxisExclusion`: Excludes small single-axis values, implemented `Into` and `Into`. + - `DualAxisExclusion`: Excludes small dual-axis values along each axis, implemented `Into`. + - `CircleExclusion`: Excludes dual-axis values below a specified magnitude threshold, implemented `Into`. + - Scaled versions: + - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. + - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. + - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. + +### Usability + +#### InputMap + +Introduce new fluent builders for creating a new `InputMap` with short configurations: + +- `fn with(mut self, action: A, input: impl Into)`. +- `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. +- `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. +- `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`. + +Introduce new iterators over `InputMap`: + +- `bindings(&self) -> impl Iterator` for iterating over all registered action-input bindings. +- `actions(&self) -> impl Iterator` for iterating over all registered actions. ### Bugs diff --git a/benches/input_map.rs b/benches/input_map.rs index 2960822c..73a687e4 100644 --- a/benches/input_map.rs +++ b/benches/input_map.rs @@ -44,17 +44,16 @@ fn construct_input_map_from_iter() -> InputMap { fn construct_input_map_from_chained_calls() -> InputMap { black_box( InputMap::default() - .insert(TestAction::A, KeyCode::KeyA) - .insert(TestAction::B, KeyCode::KeyB) - .insert(TestAction::C, KeyCode::KeyC) - .insert(TestAction::D, KeyCode::KeyD) - .insert(TestAction::E, KeyCode::KeyE) - .insert(TestAction::F, KeyCode::KeyF) - .insert(TestAction::G, KeyCode::KeyG) - .insert(TestAction::H, KeyCode::KeyH) - .insert(TestAction::I, KeyCode::KeyI) - .insert(TestAction::J, KeyCode::KeyJ) - .build(), + .with(TestAction::A, KeyCode::KeyA) + .with(TestAction::B, KeyCode::KeyB) + .with(TestAction::C, KeyCode::KeyC) + .with(TestAction::D, KeyCode::KeyD) + .with(TestAction::E, KeyCode::KeyE) + .with(TestAction::F, KeyCode::KeyF) + .with(TestAction::G, KeyCode::KeyG) + .with(TestAction::H, KeyCode::KeyH) + .with(TestAction::I, KeyCode::KeyI) + .with(TestAction::J, KeyCode::KeyJ), ) } @@ -63,7 +62,7 @@ fn which_pressed( clash_strategy: ClashStrategy, ) -> HashMap { let input_map = construct_input_map_from_iter(); - input_map.which_pressed(input_streams, clash_strategy) + input_map.process_actions(input_streams, clash_strategy) } pub fn criterion_benchmark(c: &mut Criterion) { @@ -78,8 +77,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { // Constructing our test app / input stream outside the timed benchmark let mut app = App::new(); app.add_plugins(InputPlugin); - app.send_input(KeyCode::KeyA); - app.send_input(KeyCode::KeyB); + app.press_input(KeyCode::KeyA); + app.press_input(KeyCode::KeyB); app.update(); let input_streams = InputStreams::from_world(&app.world, None); diff --git a/examples/action_state_resource.rs b/examples/action_state_resource.rs index 3f4c5633..d7f3edab 100644 --- a/examples/action_state_resource.rs +++ b/examples/action_state_resource.rs @@ -25,14 +25,10 @@ pub enum PlayerAction { Jump, } -// Exhaustively match `PlayerAction` and define the default binding to the input +// Exhaustively match `PlayerAction` and define the default bindings to the input impl PlayerAction { - fn mkb_input_map() -> InputMap { - use KeyCode::*; - InputMap::new([ - (Self::Jump, UserInput::Single(InputKind::PhysicalKey(Space))), - (Self::Move, UserInput::VirtualDPad(VirtualDPad::wasd())), - ]) + fn mkb_input_map() -> InputMap { + InputMap::new([(Self::Jump, KeyCode::Space)]).with(Self::Move, KeyboardVirtualDPad::WASD) } } diff --git a/examples/arpg_indirection.rs b/examples/arpg_indirection.rs index 8651275c..1fec7912 100644 --- a/examples/arpg_indirection.rs +++ b/examples/arpg_indirection.rs @@ -101,9 +101,8 @@ fn spawn_player(mut commands: Commands) { (Slot::Ability3, KeyE), (Slot::Ability4, KeyR), ]) - .insert(Slot::Primary, MouseButton::Left) - .insert(Slot::Secondary, MouseButton::Right) - .build(), + .with(Slot::Primary, MouseButton::Left) + .with(Slot::Secondary, MouseButton::Right), slot_action_state: ActionState::default(), ability_action_state: ActionState::default(), ability_slot_map, diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs index c175b0dd..fd81f42d 100644 --- a/examples/axis_inputs.rs +++ b/examples/axis_inputs.rs @@ -27,19 +27,17 @@ struct Player; fn spawn_player(mut commands: Commands) { // Describes how to convert from player inputs into those actions let input_map = InputMap::default() - // Configure the left stick as a dual-axis - .insert(Action::Move, DualAxis::left_stick()) + // Use the left stick for the move action + .with(Action::Move, GamepadStick::LEFT) // Let's bind the right gamepad trigger to the throttle action - .insert(Action::Throttle, GamepadButtonType::RightTrigger2) + .with(Action::Throttle, GamepadButtonType::RightTrigger2) // And we'll use the right stick's x-axis as a rudder control - .insert( + .with( // Add an AxisDeadzone to process horizontal values of the right stick. // This will trigger if the axis is moved 10% or more in either direction. Action::Rudder, - SingleAxis::new(GamepadAxisType::RightStickX) - .with_processor(AxisDeadZone::magnitude(0.1)), - ) - .build(); + GamepadControlAxis::RIGHT_X.with_deadzone_symmetric(0.1), + ); commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player); diff --git a/examples/clash_handling.rs b/examples/clash_handling.rs index 58b10649..a1dc4782 100644 --- a/examples/clash_handling.rs +++ b/examples/clash_handling.rs @@ -32,16 +32,17 @@ fn spawn_input_map(mut commands: Commands) { use KeyCode::*; use TestAction::*; - let mut input_map = InputMap::default(); - // Setting up input mappings in the obvious way - input_map.insert_multiple([(One, Digit1), (Two, Digit2), (Three, Digit3)]); + let mut input_map = InputMap::new([(One, Digit1), (Two, Digit2), (Three, Digit3)]); - input_map.insert_chord(OneAndTwo, [Digit1, Digit2]); - input_map.insert_chord(OneAndThree, [Digit1, Digit3]); - input_map.insert_chord(TwoAndThree, [Digit2, Digit3]); + input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); + input_map.insert(OneAndThree, InputChord::from_multiple([Digit1, Digit3])); + input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); - input_map.insert_chord(OneAndTwoAndThree, [Digit1, Digit2, Digit3]); + input_map.insert( + OneAndTwoAndThree, + InputChord::from_multiple([Digit1, Digit2, Digit3]), + ); commands.spawn(InputManagerBundle::with_map(input_map)); } diff --git a/examples/default_controls.rs b/examples/default_controls.rs index e43e14e3..bc8d75b1 100644 --- a/examples/default_controls.rs +++ b/examples/default_controls.rs @@ -25,12 +25,12 @@ impl PlayerAction { let mut input_map = InputMap::default(); // Default gamepad input bindings - input_map.insert(Self::Run, DualAxis::left_stick()); + input_map.insert(Self::Run, GamepadStick::LEFT); input_map.insert(Self::Jump, GamepadButtonType::South); input_map.insert(Self::UseItem, GamepadButtonType::RightTrigger2); // Default kbm input bindings - input_map.insert(Self::Run, VirtualDPad::wasd()); + input_map.insert(Self::Run, KeyboardVirtualDPad::WASD); input_map.insert(Self::Jump, KeyCode::Space); input_map.insert(Self::UseItem, MouseButton::Left); diff --git a/examples/input_processing.rs b/examples/input_processing.rs index e4ba17d0..f542e14b 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -20,33 +20,32 @@ enum Action { struct Player; fn spawn_player(mut commands: Commands) { - let mut input_map = InputMap::default(); - input_map - .insert( + let input_map = InputMap::default() + .with( Action::Move, - VirtualDPad::wasd() - // You can add a processor to handle axis-like user inputs by using the `with_processor`. + KeyboardVirtualDPad::WASD + // You can configure a processing pipeline to handle axis-like user inputs. // - // This processor is a circular deadzone that normalizes input values + // This step adds a circular deadzone that normalizes input values // by clamping their magnitude to a maximum of 1.0, // excluding those with a magnitude less than 0.1, // and scaling other values linearly in between. - .with_processor(CircleDeadZone::new(0.1)) + .with_circle_deadzone(0.1) // Followed by appending Y-axis inversion for the next processing step. - .with_processor(DualAxisInverted::ONLY_Y), + .inverted_y(), ) - .insert( + .with( Action::Move, - DualAxis::left_stick() - // You can replace the currently used processor with another processor. + GamepadStick::LEFT + // You can replace the currently used pipeline with another processor. .replace_processor(CircleDeadZone::default()) - // Or remove the processor directly, leaving no processor applied. + // Or remove the pipeline directly, leaving no any processing applied. .no_processor(), ) - .insert( + .with( Action::LookAround, // You can also use a sequence of processors as the processing pipeline. - DualAxis::mouse_motion().replace_processor(DualAxisProcessor::from_iter([ + MouseMove::RAW.with_processor(DualAxisProcessor::pipeline([ // The first processor is a circular deadzone. CircleDeadZone::new(0.1).into(), // The next processor doubles inputs normalized by the deadzone. diff --git a/examples/mouse_motion.rs b/examples/mouse_motion.rs index 29ff6326..2468efc2 100644 --- a/examples/mouse_motion.rs +++ b/examples/mouse_motion.rs @@ -19,8 +19,8 @@ fn setup(mut commands: Commands) { let input_map = InputMap::new([ // This will capture the total continuous value, for direct use. // Note that you can also use discrete gesture-like motion, - // via the `MouseMotionDirection` enum. - (CameraMovement::Pan, DualAxis::mouse_motion()), + // via the `MouseMoveDirection` enum. + (CameraMovement::Pan, MouseMove::RAW), ]); commands .spawn(Camera2dBundle::default()) diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index 7d6b12f5..1eee77ef 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -14,6 +14,7 @@ fn main() { #[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)] enum CameraMovement { Zoom, + Pan, PanLeft, PanRight, } @@ -21,16 +22,14 @@ enum CameraMovement { fn setup(mut commands: Commands) { let input_map = InputMap::default() // This will capture the total continuous value, for direct use. - .insert(CameraMovement::Zoom, SingleAxis::mouse_wheel_y()) - // This will return a binary button-like output. - .insert(CameraMovement::PanLeft, MouseWheelDirection::Left) - .insert(CameraMovement::PanRight, MouseWheelDirection::Right) - // Alternatively, you could model this as a virtual Dpad. - // It's extremely useful for modeling 4-directional button-like inputs with the mouse wheel - // .insert(VirtualDpad::mouse_wheel(), Pan) - // Or even a continuous `DualAxis`! - // .insert(DualAxis::mouse_wheel(), Pan) - .build(); + .with(CameraMovement::Zoom, MouseScrollAxis::Y) + // These will return a binary button-like output. + .with(CameraMovement::PanLeft, MouseScrollDirection::LEFT) + .with(CameraMovement::PanRight, MouseScrollDirection::RIGHT) + // Alternatively, you could model them as a continuous dual-axis input + .with(CameraMovement::Pan, MouseScroll::RAW) + // Or even a digital dual-axis input! + .with(CameraMovement::Pan, MouseScroll::DIGITAL); commands .spawn(Camera2dBundle::default()) .insert(InputManagerBundle::with_map(input_map)); @@ -62,7 +61,7 @@ fn pan_camera(mut query: Query<(&mut Transform, &ActionState), W let (mut camera_transform, action_state) = query.single_mut(); - // When using the `MouseWheelDirection` type, mouse wheel inputs can be treated like simple buttons + // When using the `MouseScrollDirection` type, mouse wheel inputs can be treated like simple buttons if action_state.pressed(&CameraMovement::PanLeft) { camera_transform.translation.x -= CAMERA_PAN_RATE; } diff --git a/examples/multiplayer.rs b/examples/multiplayer.rs index 406c402f..1bda0f52 100644 --- a/examples/multiplayer.rs +++ b/examples/multiplayer.rs @@ -42,15 +42,13 @@ impl PlayerBundle { // and gracefully handle disconnects // Note that this step is not required: // if it is skipped, all input maps will read from all connected gamepads - .set_gamepad(Gamepad { id: 0 }) - .build(), + .with_gamepad(Gamepad { id: 0 }), Player::Two => InputMap::new([ (Action::Left, KeyCode::ArrowLeft), (Action::Right, KeyCode::ArrowRight), (Action::Jump, KeyCode::ArrowUp), ]) - .set_gamepad(Gamepad { id: 1 }) - .build(), + .with_gamepad(Gamepad { id: 1 }), }; // Each player will use the same gamepad controls, but on separate gamepads. diff --git a/examples/register_gamepads.rs b/examples/register_gamepads.rs index 38aaea9b..677e865b 100644 --- a/examples/register_gamepads.rs +++ b/examples/register_gamepads.rs @@ -49,8 +49,7 @@ fn join( (Action::Disconnect, GamepadButtonType::Select), ]) // Make sure to set the gamepad or all gamepads will be used! - .set_gamepad(gamepad) - .build(); + .with_gamepad(gamepad); let player = commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player { gamepad }) diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs index 4763a00e..cb7fb9a9 100644 --- a/examples/send_actions_over_network.rs +++ b/examples/send_actions_over_network.rs @@ -69,8 +69,8 @@ fn main() { client_app.update(); // Sending inputs to the client - client_app.send_input(KeyCode::Space); - client_app.send_input(MouseButton::Left); + client_app.press_input(KeyCode::Space); + client_app.press_input(MouseButton::Left); // These are converted into actions when the client_app's `Schedule` runs client_app.update(); @@ -122,8 +122,7 @@ fn spawn_player(mut commands: Commands) { use KeyCode::*; let input_map = InputMap::new([(MoveLeft, KeyW), (MoveRight, KeyD), (Jump, Space)]) - .insert(Shoot, MouseButton::Left) - .build(); + .with(Shoot, MouseButton::Left); commands .spawn(InputManagerBundle::with_map(input_map)) .insert(Player); diff --git a/examples/twin_stick_controller.rs b/examples/twin_stick_controller.rs index dc45d2fa..67540083 100644 --- a/examples/twin_stick_controller.rs +++ b/examples/twin_stick_controller.rs @@ -44,13 +44,13 @@ impl PlayerAction { let mut input_map = InputMap::default(); // Default gamepad input bindings - input_map.insert(Self::Move, DualAxis::left_stick()); - input_map.insert(Self::Look, DualAxis::right_stick()); + input_map.insert(Self::Move, GamepadStick::LEFT); + input_map.insert(Self::Look, GamepadStick::RIGHT); input_map.insert(Self::Shoot, GamepadButtonType::RightTrigger); // Default kbm input bindings - input_map.insert(Self::Move, VirtualDPad::wasd()); - input_map.insert(Self::Look, VirtualDPad::arrow_keys()); + input_map.insert(Self::Move, KeyboardVirtualDPad::WASD); + input_map.insert(Self::Look, KeyboardVirtualDPad::ARROW_KEYS); input_map.insert(Self::Shoot, MouseButton::Left); input_map diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs index d30f9d20..e791915b 100644 --- a/examples/virtual_dpad.rs +++ b/examples/virtual_dpad.rs @@ -24,16 +24,11 @@ struct Player; fn spawn_player(mut commands: Commands) { // Stores "which actions are currently activated" - // Map some arbitrary keys into a virtual direction pad that triggers our move action let input_map = InputMap::new([( Action::Move, - VirtualDPad { - up: KeyCode::KeyW.into(), - down: KeyCode::KeyS.into(), - left: KeyCode::KeyA.into(), - right: KeyCode::KeyD.into(), - processor: DualAxisProcessor::None, - }, + // Define a virtual D-pad using four arbitrary keys. + // You can also use GamepadVirtualDPad to create similar ones using gamepad buttons. + KeyboardVirtualDPad::new(KeyCode::KeyW, KeyCode::KeyS, KeyCode::KeyA, KeyCode::KeyD), )]); commands .spawn(InputManagerBundle::with_map(input_map)) diff --git a/src/action_state.rs b/src/action_state.rs index e07153ab..5a6d3cd2 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -251,9 +251,9 @@ impl ActionState { /// /// - Binary buttons will have a value of `0.0` when the button is not pressed, and a value of /// `1.0` when the button is pressed. - /// - Some axes, such as an analog stick, will have a value in the range `-1.0..=1.0`. - /// - Some axes, such as a variable trigger, will have a value in the range `0.0..=1.0`. - /// - Some buttons will also return a value in the range `0.0..=1.0`, such as analog gamepad + /// - Some axes, such as an analog stick, will have a value in the range `[-1.0, 1.0]`. + /// - Some axes, such as a variable trigger, will have a value in the range `[0.0, 1.0]`. + /// - Some buttons will also return a value in the range `[0.0, 1.0]`, such as analog gamepad /// triggers which may be tracked as buttons or axes. Examples of these include the Xbox LT/RT /// triggers and the Playstation L2/R2 triggers. See also the `axis_inputs` example in the /// repository. @@ -289,11 +289,8 @@ impl ActionState { /// Get the [`DualAxisData`] from the binding that triggered the corresponding `action`. /// - /// Only certain events such as [`VirtualDPad`][crate::axislike::VirtualDPad] and - /// [`DualAxis`][crate::axislike::DualAxis] provide an [`DualAxisData`], and this - /// will return [`None`] for other events. - /// - /// Chord inputs will return the [`DualAxisData`] of it's first input. + /// Only events that represent dual-axis control provide an [`DualAxisData`], + /// and this will return [`None`] for other events. /// /// If multiple inputs with an axis pair trigger the same game action at the same time, the /// value of each axis pair will be added together. @@ -621,6 +618,7 @@ mod tests { use crate::input_map::InputMap; use crate::input_mocking::MockInput; use crate::input_streams::InputStreams; + use crate::prelude::InputChord; use bevy::input::InputPlugin; use bevy::prelude::*; use bevy::utils::{Duration, Instant}; @@ -647,7 +645,7 @@ mod tests { // Starting state let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -655,12 +653,12 @@ mod tests { assert!(!action_state.just_released(&Action::Run)); // Pressing - app.send_input(KeyCode::KeyR); + app.press_input(KeyCode::KeyR); // Process the input events into Input data app.update(); let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(action_state.pressed(&Action::Run)); assert!(action_state.just_pressed(&Action::Run)); @@ -669,7 +667,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -681,7 +679,7 @@ mod tests { app.update(); let input_streams = InputStreams::from_world(&app.world, None); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -690,7 +688,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); - action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll)); + action_state.update(input_map.process_actions(&input_streams, ClashStrategy::PressAll)); assert!(!action_state.pressed(&Action::Run)); assert!(!action_state.just_pressed(&Action::Run)); @@ -712,7 +710,10 @@ mod tests { let mut input_map = InputMap::default(); input_map.insert(Action::One, Digit1); input_map.insert(Action::Two, Digit2); - input_map.insert_chord(Action::OneAndTwo, [Digit1, Digit2]); + input_map.insert( + Action::OneAndTwo, + InputChord::from_multiple([Digit1, Digit2]), + ); let mut app = App::new(); app.add_plugins(InputPlugin); @@ -723,18 +724,18 @@ mod tests { // Starting state let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.released(&Action::One)); assert!(action_state.released(&Action::Two)); assert!(action_state.released(&Action::OneAndTwo)); // Pressing One - app.send_input(Digit1); + app.press_input(Digit1); app.update(); let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.pressed(&Action::One)); assert!(action_state.released(&Action::Two)); @@ -743,19 +744,19 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.pressed(&Action::One)); assert!(action_state.released(&Action::Two)); assert!(action_state.released(&Action::OneAndTwo)); // Pressing Two - app.send_input(Digit2); + app.press_input(Digit2); app.update(); let input_streams = InputStreams::from_world(&app.world, None); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); // Now only the longest OneAndTwo has been pressed, // while both One and Two have been released @@ -766,7 +767,7 @@ mod tests { // Waiting action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); action_state - .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + .update(input_map.process_actions(&input_streams, ClashStrategy::PrioritizeLongest)); assert!(action_state.released(&Action::One)); assert!(action_state.released(&Action::Two)); diff --git a/src/axislike.rs b/src/axislike.rs index b827ea4b..d7f03e29 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -1,617 +1,343 @@ //! Tools for working with directional axis-like user inputs (game sticks, D-Pads and emulated equivalents) -use bevy::prelude::{Direction2d, GamepadAxisType, GamepadButtonType, KeyCode, Reflect, Vec2}; +use bevy::prelude::{Direction2d, Reflect, Vec2}; use serde::{Deserialize, Serialize}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::input_processing::*; use crate::orientation::Rotation; -use crate::user_input::InputKind; -/// A single directional axis with a configurable trigger zone. -/// -/// These can be stored in a [`InputKind`] to create a virtual button. -#[derive(Debug, Clone, Serialize, Deserialize, Reflect)] -pub struct SingleAxis { - /// The axis that is being checked. - pub axis_type: AxisType, - - /// The processor used to handle input values. - pub processor: AxisProcessor, - - /// The target value for this input, used for input mocking. - /// - /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) - pub value: Option, +/// Different ways that user input is represented on an axis. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Reflect)] +#[must_use] +pub enum AxisInputMode { + /// Continuous input values, typically range from a negative maximum (e.g., `-1.0`) + /// to a positive maximum (e.g., `1.0`), allowing for smooth and precise control. + #[default] + Analog, + + /// Discrete input values, using three distinct values to represent the states: + /// `-1.0` for active in negative direction, `0.0` for inactive, and `1.0` for active in positive direction. + Digital, } -impl SingleAxis { - /// Creates a [`SingleAxis`] with the specified axis type. - #[must_use] - pub fn new(axis_type: impl Into) -> Self { - Self { - axis_type: axis_type.into(), - processor: AxisProcessor::None, - value: None, - } - } - - /// Creates a [`SingleAxis`] with the specified axis type and `value`. +impl AxisInputMode { + /// Converts the given `f32` value based on the current [`AxisInputMode`]. /// - /// Primarily useful for [input mocking](crate::input_mocking). - #[must_use] - pub fn from_value(axis_type: impl Into, value: f32) -> Self { - Self { - axis_type: axis_type.into(), - processor: AxisProcessor::None, - value: Some(value), - } - } - - /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseWheel`](bevy::input::mouse::MouseWheel) movement - #[must_use] - pub const fn mouse_wheel_x() -> Self { - Self { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - processor: AxisProcessor::None, - value: None, - } - } - - /// Creates a [`SingleAxis`] corresponding to vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement - #[must_use] - pub const fn mouse_wheel_y() -> Self { - Self { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - processor: AxisProcessor::None, - value: None, - } - } - - /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseMotion`](bevy::input::mouse::MouseMotion) movement + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0`, leaving others as is. #[must_use] - pub const fn mouse_motion_x() -> Self { - Self { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - processor: AxisProcessor::None, - value: None, + #[inline] + pub fn axis_value(&self, value: f32) -> f32 { + match self { + Self::Analog => value, + Self::Digital => { + if value < 0.0 { + -1.0 + } else if value > 0.0 { + 1.0 + } else { + value + } + } } } - /// Creates a [`SingleAxis`] corresponding to vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement + /// Converts the given [`Vec2`] value based on the current [`AxisInputMode`]. + /// + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0` along each axis, leaving others as is. #[must_use] - pub const fn mouse_motion_y() -> Self { - Self { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - processor: AxisProcessor::None, - value: None, - } - } - - /// Appends the given [`AxisProcessor`] as the next processing step. - #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self - } - - /// Replaces the current [`AxisProcessor`] with the specified `processor`. - #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self - } - - /// Remove the current used [`AxisProcessor`]. #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = AxisProcessor::None; - self + pub fn dual_axis_value(&self, value: Vec2) -> Vec2 { + match self { + Self::Analog => value, + Self::Digital => Vec2::new(self.axis_value(value.x), self.axis_value(value.y)), + } } - /// Get the "value" of this axis. - /// If a processor is set, it will compute and return the processed value. - /// Otherwise, pass the `input_value` through unchanged. + /// Computes the magnitude of given [`Vec2`] value based on the current [`AxisInputMode`]. + /// + /// # Returns + /// + /// - [`AxisInputMode::Analog`]: Leaves values as is. + /// - [`AxisInputMode::Digital`]: `1.0` for non-zero values, `0.0` for others. #[must_use] #[inline] - pub fn input_value(&self, input_value: f32) -> f32 { - self.processor.process(input_value) - } -} - -impl PartialEq for SingleAxis { - fn eq(&self, other: &Self) -> bool { - self.axis_type == other.axis_type && self.processor == other.processor - } -} - -impl Eq for SingleAxis {} - -impl std::hash::Hash for SingleAxis { - fn hash(&self, state: &mut H) { - self.axis_type.hash(state); - self.processor.hash(state); + pub fn dual_axis_magnitude(&self, value: Vec2) -> f32 { + match self { + Self::Analog => value.length(), + Self::Digital => f32::from(value != Vec2::ZERO), + } } } -/// Two directional axes combined as one input. -/// -/// These can be stored in a [`VirtualDPad`], which is itself stored in an [`InputKind`] for consumption. -/// -/// This input will generate a [`DualAxis`] which can be read with -/// [`ActionState::axis_pair`][crate::action_state::ActionState::axis_pair]. -#[derive(Debug, Clone, Serialize, Deserialize, Reflect)] -pub struct DualAxis { - /// The horizontal axis that is being checked. - pub x_axis_type: AxisType, - - /// The vertical axis that is being checked. - pub y_axis_type: AxisType, - - /// The processor used to handle input values. - pub processor: DualAxisProcessor, +/// The directions for single-axis inputs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum AxisDirection { + /// Negative direction. + Negative, - /// The target value for this input, used for input mocking. - /// - /// WARNING: this field is ignored for the sake of [`Eq`] and [`Hash`](std::hash::Hash) - pub value: Option, + /// Positive direction. + Positive, } -impl DualAxis { - /// Creates a [`DualAxis`] with the specified axis types. - #[must_use] - pub fn new(x_axis_type: impl Into, y_axis_type: impl Into) -> Self { - Self { - x_axis_type: x_axis_type.into(), - y_axis_type: y_axis_type.into(), - processor: DualAxisProcessor::None, - value: None, - } - } - - /// Creates a [`DualAxis`] with the specified axis types and `value`. - /// - /// Primarily useful for [input mocking](crate::input_mocking). +impl AxisDirection { + /// Returns the full active value along an axis. #[must_use] - pub fn from_value( - x_axis_type: impl Into, - y_axis_type: impl Into, - x_value: f32, - y_value: f32, - ) -> Self { - Self { - x_axis_type: x_axis_type.into(), - y_axis_type: y_axis_type.into(), - processor: DualAxisProcessor::None, - value: Some(Vec2::new(x_value, y_value)), + #[inline] + pub fn full_active_value(&self) -> f32 { + match self { + Self::Negative => -1.0, + Self::Positive => 1.0, } } - /// Creates a [`DualAxis`] for the left analogue stick of the gamepad. + /// Checks if the given `value` represents an active input in this direction. #[must_use] - pub fn left_stick() -> Self { - Self { - x_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - y_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - processor: CircleDeadZone::default().into(), - value: None, + #[inline] + pub fn is_active(&self, value: f32) -> bool { + match self { + Self::Negative => value < 0.0, + Self::Positive => value > 0.0, } } +} - /// Creates a [`DualAxis`] for the right analogue stick of the gamepad. - #[must_use] - pub fn right_stick() -> Self { - Self { - x_axis_type: AxisType::Gamepad(GamepadAxisType::RightStickX), - y_axis_type: AxisType::Gamepad(GamepadAxisType::RightStickY), - processor: CircleDeadZone::default().into(), - value: None, - } - } +/// An axis for dual-axis inputs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum DualAxisType { + /// The X-axis (typically horizontal movement). + X, - /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement - pub const fn mouse_wheel() -> Self { - Self { - x_axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - y_axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - processor: DualAxisProcessor::None, - value: None, - } - } + /// The Y-axis (typically vertical movement). + Y, +} - /// Creates a [`DualAxis`] corresponding to horizontal and vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement - pub const fn mouse_motion() -> Self { - Self { - x_axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - y_axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - processor: DualAxisProcessor::None, - value: None, - } +impl DualAxisType { + /// Returns both X and Y axes. + #[inline] + pub const fn axes() -> [Self; 2] { + [Self::X, Self::Y] } - /// Appends the given [`DualAxisProcessor`] as the next processing step. + /// Returns the positive and negative [`DualAxisDirection`]s for the current axis. #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self + pub const fn directions(&self) -> [DualAxisDirection; 2] { + [self.negative(), self.positive()] } - /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. + /// Returns the negative [`DualAxisDirection`] for the current axis. #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self + pub const fn negative(&self) -> DualAxisDirection { + match self { + Self::X => DualAxisDirection::Left, + Self::Y => DualAxisDirection::Down, + } } - /// Remove the current used [`DualAxisProcessor`]. + /// Returns the positive [`DualAxisDirection`] for the current axis. #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = DualAxisProcessor::None; - self + pub const fn positive(&self) -> DualAxisDirection { + match self { + Self::X => DualAxisDirection::Right, + Self::Y => DualAxisDirection::Up, + } } - /// Get the "value" of these axes. - /// If a processor is set, it will compute and return the processed value. - /// Otherwise, pass the `input_value` through unchanged. + /// Returns the value along the current axis. #[must_use] #[inline] - pub fn input_value(&self, input_value: Vec2) -> Vec2 { - self.processor.process(input_value) - } -} - -impl PartialEq for DualAxis { - fn eq(&self, other: &Self) -> bool { - self.x_axis_type == other.x_axis_type - && self.y_axis_type == other.y_axis_type - && self.processor == other.processor - } -} - -impl Eq for DualAxis {} - -impl std::hash::Hash for DualAxis { - fn hash(&self, state: &mut H) { - self.x_axis_type.hash(state); - self.y_axis_type.hash(state); - self.processor.hash(state); - } -} - -#[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an unquoted item -/// A virtual DPad that you can get an [`DualAxis`] from. -/// -/// Typically, you don't want to store a [`DualAxis`] in this type, -/// even though it can be stored as an [`InputKind`]. -/// -/// Instead, use it directly as [`InputKind::DualAxis`]! -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub struct VirtualDPad { - /// The input that represents the up direction in this virtual DPad - pub up: InputKind, - /// The input that represents the down direction in this virtual DPad - pub down: InputKind, - /// The input that represents the left direction in this virtual DPad - pub left: InputKind, - /// The input that represents the right direction in this virtual DPad - pub right: InputKind, - /// The processor used to handle input values. - pub processor: DualAxisProcessor, -} - -impl VirtualDPad { - /// Generates a [`VirtualDPad`] corresponding to the arrow keyboard keycodes - pub fn arrow_keys() -> VirtualDPad { - VirtualDPad { - up: InputKind::PhysicalKey(KeyCode::ArrowUp), - down: InputKind::PhysicalKey(KeyCode::ArrowDown), - left: InputKind::PhysicalKey(KeyCode::ArrowLeft), - right: InputKind::PhysicalKey(KeyCode::ArrowRight), - processor: CircleDeadZone::default().into(), + pub const fn get_value(&self, value: Vec2) -> f32 { + match self { + Self::X => value.x, + Self::Y => value.y, } } - /// Generates a [`VirtualDPad`] corresponding to the `WASD` keys on the standard US QWERTY layout. - /// - /// Note that on other keyboard layouts, different keys need to be pressed. - /// The _location_ of the keys is the same on all keyboard layouts. - /// This ensures that the classic triangular shape is retained on all layouts, - /// which enables comfortable movement controls. - pub fn wasd() -> VirtualDPad { - VirtualDPad { - up: InputKind::PhysicalKey(KeyCode::KeyW), - down: InputKind::PhysicalKey(KeyCode::KeyS), - left: InputKind::PhysicalKey(KeyCode::KeyA), - right: InputKind::PhysicalKey(KeyCode::KeyD), - processor: CircleDeadZone::default().into(), + /// Creates a [`Vec2`] with the specified `value` on this axis and `0.0` on the other. + #[must_use] + #[inline] + pub const fn dual_axis_value(&self, value: f32) -> Vec2 { + match self { + Self::X => Vec2::new(value, 0.0), + Self::Y => Vec2::new(0.0, value), } } +} - #[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an unquoted item - /// Generates a [`VirtualDPad`] corresponding to the DPad on a gamepad - pub fn dpad() -> VirtualDPad { - VirtualDPad { - up: InputKind::GamepadButton(GamepadButtonType::DPadUp), - down: InputKind::GamepadButton(GamepadButtonType::DPadDown), - left: InputKind::GamepadButton(GamepadButtonType::DPadLeft), - right: InputKind::GamepadButton(GamepadButtonType::DPadRight), - processor: CircleDeadZone::default().into(), - } - } +/// The directions for dual-axis inputs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[must_use] +pub enum DualAxisDirection { + /// Upward direction. + Up, - /// Generates a [`VirtualDPad`] corresponding to the face buttons on a gamepad - /// - /// North corresponds to up, west corresponds to left, - /// east corresponds to right, and south corresponds to down - pub fn gamepad_face_buttons() -> VirtualDPad { - VirtualDPad { - up: InputKind::GamepadButton(GamepadButtonType::North), - down: InputKind::GamepadButton(GamepadButtonType::South), - left: InputKind::GamepadButton(GamepadButtonType::West), - right: InputKind::GamepadButton(GamepadButtonType::East), - processor: CircleDeadZone::default().into(), - } - } + /// Downward direction. + Down, - /// Generates a [`VirtualDPad`] corresponding to discretized mousewheel movements - pub fn mouse_wheel() -> VirtualDPad { - VirtualDPad { - up: InputKind::MouseWheel(MouseWheelDirection::Up), - down: InputKind::MouseWheel(MouseWheelDirection::Down), - left: InputKind::MouseWheel(MouseWheelDirection::Left), - right: InputKind::MouseWheel(MouseWheelDirection::Right), - processor: DualAxisProcessor::None, - } - } + /// Leftward direction. + Left, - /// Generates a [`VirtualDPad`] corresponding to discretized mouse motions - pub fn mouse_motion() -> VirtualDPad { - VirtualDPad { - up: InputKind::MouseMotion(MouseMotionDirection::Up), - down: InputKind::MouseMotion(MouseMotionDirection::Down), - left: InputKind::MouseMotion(MouseMotionDirection::Left), - right: InputKind::MouseMotion(MouseMotionDirection::Right), - processor: DualAxisProcessor::None, - } - } + /// Rightward direction. + Right, +} - /// Appends the given [`DualAxisProcessor`] as the next processing step. +impl DualAxisDirection { + /// Returns the [`DualAxisType`] associated with this direction. #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self + pub fn axis(&self) -> DualAxisType { + match self { + Self::Up => DualAxisType::Y, + Self::Down => DualAxisType::Y, + Self::Left => DualAxisType::X, + Self::Right => DualAxisType::X, + } } - /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. + /// Returns the [`AxisDirection`] (positive or negative) on the axis. #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self + pub fn axis_direction(&self) -> AxisDirection { + match self { + Self::Up => AxisDirection::Positive, + Self::Down => AxisDirection::Negative, + Self::Left => AxisDirection::Negative, + Self::Right => AxisDirection::Positive, + } } - /// Remove the current used [`DualAxisProcessor`]. + /// Returns the full active value along both axes. + #[must_use] #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = DualAxisProcessor::None; - self + pub fn full_active_value(&self) -> Vec2 { + match self { + Self::Up => Vec2::Y, + Self::Down => Vec2::NEG_Y, + Self::Left => Vec2::NEG_X, + Self::Right => Vec2::X, + } } - /// Get the "value" of these axes. - /// If a processor is set, it will compute and return the processed value. - /// Otherwise, pass the `input_value` through unchanged. + /// Checks if the given `value` represents an active input in this direction. #[must_use] #[inline] - pub fn input_value(&self, input_value: Vec2) -> Vec2 { - self.processor.process(input_value) + pub fn is_active(&self, value: Vec2) -> bool { + let component_along_axis = self.axis().get_value(value); + self.axis_direction().is_active(component_along_axis) } } -/// A virtual Axis that you can get a value between -1 and 1 from. +/// A combined input data from two axes (X and Y). /// -/// Typically, you don't want to store a [`SingleAxis`] in this type, -/// even though it can be stored as an [`InputKind`]. +/// This struct stores the X and Y values as a [`Vec2`] in a device-agnostic way, +/// meaning it works consistently regardless of the specific input device (gamepad, joystick, etc.). +/// It assumes any calibration (deadzone correction, rescaling, drift correction, etc.) +/// has already been applied at an earlier stage of processing. /// -/// Instead, use it directly as [`InputKind::SingleAxis`]! -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub struct VirtualAxis { - /// The input that represents the negative direction of this virtual axis - pub negative: InputKind, - /// The input that represents the positive direction of this virtual axis - pub positive: InputKind, - /// The processor used to handle input values. - pub processor: AxisProcessor, -} - -impl VirtualAxis { - /// Helper function for generating a [`VirtualAxis`] from arbitrary keycodes, shorthand for - /// wrapping each key in [`InputKind::PhysicalKey`] - pub const fn from_keys(negative: KeyCode, positive: KeyCode) -> VirtualAxis { - VirtualAxis { - negative: InputKind::PhysicalKey(negative), - positive: InputKind::PhysicalKey(positive), - processor: AxisProcessor::None, - } - } - - /// Generates a [`VirtualAxis`] corresponding to the horizontal arrow keyboard keycodes - pub const fn horizontal_arrow_keys() -> VirtualAxis { - VirtualAxis::from_keys(KeyCode::ArrowLeft, KeyCode::ArrowRight) - } - - /// Generates a [`VirtualAxis`] corresponding to the horizontal arrow keyboard keycodes - pub const fn vertical_arrow_keys() -> VirtualAxis { - VirtualAxis::from_keys(KeyCode::ArrowDown, KeyCode::ArrowUp) - } - - /// Generates a [`VirtualAxis`] corresponding to the `AD` keyboard keycodes. - pub const fn ad() -> VirtualAxis { - VirtualAxis::from_keys(KeyCode::KeyA, KeyCode::KeyD) - } +/// The neutral origin of this input data is always at `(0, 0)`. +/// When working with gamepad axes, both X and Y values are typically bounded by `[-1.0, 1.0]`. +/// However, this range may not apply to other input types, such as mousewheel data which can have a wider range. +#[derive(Default, Debug, Copy, Clone, PartialEq, Deserialize, Serialize, Reflect)] +#[must_use] +pub struct DualAxisData(Vec2); - /// Generates a [`VirtualAxis`] corresponding to the `WS` keyboard keycodes. - pub const fn ws() -> VirtualAxis { - VirtualAxis::from_keys(KeyCode::KeyS, KeyCode::KeyW) - } +impl DualAxisData { + /// All zeros. + pub const ZERO: Self = Self(Vec2::ZERO); - #[allow(clippy::doc_markdown)] - /// Generates a [`VirtualAxis`] corresponding to the horizontal DPad buttons on a gamepad. - pub const fn horizontal_dpad() -> VirtualAxis { - VirtualAxis { - negative: InputKind::GamepadButton(GamepadButtonType::DPadLeft), - positive: InputKind::GamepadButton(GamepadButtonType::DPadRight), - processor: AxisProcessor::None, - } + /// Creates a [`DualAxisData`] with the given values. + pub const fn new(x: f32, y: f32) -> Self { + Self(Vec2::new(x, y)) } - #[allow(clippy::doc_markdown)] - /// Generates a [`VirtualAxis`] corresponding to the vertical DPad buttons on a gamepad. - pub const fn vertical_dpad() -> VirtualAxis { - VirtualAxis { - negative: InputKind::GamepadButton(GamepadButtonType::DPadDown), - positive: InputKind::GamepadButton(GamepadButtonType::DPadUp), - processor: AxisProcessor::None, - } + /// Creates a [`DualAxisData`] directly from the given [`Vec2`]. + pub const fn from_xy(xy: Vec2) -> Self { + Self(xy) } - /// Generates a [`VirtualAxis`] corresponding to the horizontal gamepad face buttons. - pub const fn horizontal_gamepad_face_buttons() -> VirtualAxis { - VirtualAxis { - negative: InputKind::GamepadButton(GamepadButtonType::West), - positive: InputKind::GamepadButton(GamepadButtonType::East), - processor: AxisProcessor::None, - } + /// Combines the directional input from this [`DualAxisData`] with another. + /// + /// This is useful if you have multiple sticks bound to the same game action, + /// and you want to get their combined position. + /// + /// # Warning + /// + /// This method performs vector addition on the X and Y components. + /// While the direction is preserved, the combined magnitude might exceed the expected + /// range for certain input devices (e.g., gamepads typically have a maximum magnitude of `1.0`). + /// + /// To ensure the combined input stays within the expected range, + /// consider using [`Self::clamp_length`] on the returned value. + pub fn merged_with(&self, other: Self) -> Self { + Self(self.0 + other.0) } - /// Generates a [`VirtualAxis`] corresponding to the vertical gamepad face buttons. - pub const fn vertical_gamepad_face_buttons() -> VirtualAxis { - VirtualAxis { - negative: InputKind::GamepadButton(GamepadButtonType::South), - positive: InputKind::GamepadButton(GamepadButtonType::North), - processor: AxisProcessor::None, - } + /// The value along the X-axis, typically ranging from `-1.0` to `1.0`. + #[must_use] + #[inline] + pub const fn x(&self) -> f32 { + self.0.x } - /// Appends the given [`AxisProcessor`] as the next processing step. + /// The value along the Y-axis, typically ranging from `-1.0` to `1.0`. + #[must_use] #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self + pub const fn y(&self) -> f32 { + self.0.y } - /// Replaces the current [`AxisProcessor`] with the specified `processor`. + /// The values along each axis, each typically ranging from `-1.0` to `1.0`. + #[must_use] #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self + pub const fn xy(&self) -> Vec2 { + self.0 } - /// Removes the current used [`AxisProcessor`]. + /// The [`Direction2d`] that this axis is pointing towards, if not neutral. + #[must_use] #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = AxisProcessor::None; - self + pub fn direction(&self) -> Option { + Direction2d::new(self.0).ok() } - /// Get the "value" of the axis. - /// If a processor is set, it will compute and return the processed value. - /// Otherwise, pass the `input_value` through unchanged. + /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if not neutral. #[must_use] #[inline] - pub fn input_value(&self, input_value: f32) -> f32 { - self.processor.process(input_value) + pub fn rotation(&self) -> Option { + Rotation::from_xy(self.0).ok() } -} -/// The type of axis used by a [`UserInput`](crate::user_input::UserInput). -/// -/// This is stored in either a [`SingleAxis`] or [`DualAxis`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub enum AxisType { - /// Input associated with a gamepad, such as the triggers or one axis of an analog stick. - Gamepad(GamepadAxisType), - /// Input associated with a mouse wheel. - MouseWheel(MouseWheelAxisType), - /// Input associated with movement of the mouse - MouseMotion(MouseMotionAxisType), -} - -/// The motion direction of the mouse wheel. -/// -/// Stored in the [`AxisType`] enum. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub enum MouseWheelAxisType { - /// Horizontal movement. - /// - /// This is much less common than the `Y` variant, and is only supported on some devices. - X, - /// Vertical movement. + /// Computes the magnitude of the value (distance from the origin), typically bounded by `[0, 1]`. /// - /// This is the standard behavior for a mouse wheel, used to scroll up and down pages. - Y, -} - -/// The motion direction of the mouse. -/// -/// Stored in the [`AxisType`] enum. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub enum MouseMotionAxisType { - /// Horizontal movement. - X, - /// Vertical movement. - Y, -} - -impl From for AxisType { - fn from(axis_type: GamepadAxisType) -> Self { - AxisType::Gamepad(axis_type) - } -} - -impl From for AxisType { - fn from(axis_type: MouseWheelAxisType) -> Self { - AxisType::MouseWheel(axis_type) - } -} - -impl From for AxisType { - fn from(axis_type: MouseMotionAxisType) -> Self { - AxisType::MouseMotion(axis_type) + /// If you only need to compare relative magnitudes, use [`Self::length_squared`] instead for faster computation. + #[must_use] + #[inline] + pub fn length(&self) -> f32 { + self.0.length() } -} -impl TryFrom for GamepadAxisType { - type Error = AxisConversionError; - - fn try_from(axis_type: AxisType) -> Result { - match axis_type { - AxisType::Gamepad(inner) => Ok(inner), - _ => Err(AxisConversionError), - } + /// Computes the squared magnitude, typically bounded by `[0, 1]`. + /// + /// This is faster than [`Self::length`], as it avoids a square root, but will generally have less natural behavior. + #[must_use] + #[inline] + pub fn length_squared(&self) -> f32 { + self.0.length_squared() } -} - -impl TryFrom for MouseWheelAxisType { - type Error = AxisConversionError; - fn try_from(axis_type: AxisType) -> Result { - match axis_type { - AxisType::MouseWheel(inner) => Ok(inner), - _ => Err(AxisConversionError), - } + /// Clamps the value to a maximum magnitude. + #[inline] + pub fn clamp_length(&mut self, max: f32) { + self.0 = self.0.clamp_length_max(max); } } -impl TryFrom for MouseMotionAxisType { - type Error = AxisConversionError; - - fn try_from(axis_type: AxisType) -> Result { - match axis_type { - AxisType::MouseMotion(inner) => Ok(inner), - _ => Err(AxisConversionError), - } +impl From for Vec2 { + fn from(data: DualAxisData) -> Vec2 { + data.0 } } - -/// An [`AxisType`] could not be converted into a more specialized variant -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AxisConversionError; diff --git a/src/buttonlike.rs b/src/buttonlike.rs index e4712f28..a30b764f 100644 --- a/src/buttonlike.rs +++ b/src/buttonlike.rs @@ -1,5 +1,5 @@ //! Tools for working with button-like user inputs (mouse clicks, gamepad button, keyboard inputs and so on) -//! + use bevy::reflect::Reflect; use serde::{Deserialize, Serialize}; @@ -83,33 +83,3 @@ impl ButtonState { *self == ButtonState::JustReleased } } - -/// A buttonlike-input triggered by [`MouseWheel`](bevy::input::mouse::MouseWheel) events -/// -/// These will be considered pressed if non-zero net movement in the correct direction is detected. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub enum MouseWheelDirection { - /// Corresponds to `+y` - Up, - /// Corresponds to `-y` - Down, - /// Corresponds to `+x` - Right, - /// Corresponds to `-x` - Left, -} - -/// A buttonlike-input triggered by [`MouseMotion`](bevy::input::mouse::MouseMotion) events -/// -/// These will be considered pressed if non-zero net movement in the correct direction is detected. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)] -pub enum MouseMotionDirection { - /// Corresponds to `+y` - Up, - /// Corresponds to `-y` - Down, - /// Corresponds to `+x` - Right, - /// Corresponds to `-x` - Left, -} diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index b92cdfc1..70fee141 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -1,17 +1,16 @@ //! Handles clashing inputs into a [`InputMap`] in a configurable fashion. -use crate::action_state::ActionData; -use crate::axislike::{VirtualAxis, VirtualDPad}; -use crate::input_map::InputMap; -use crate::input_streams::InputStreams; -use crate::user_input::{InputKind, UserInput}; -use crate::Actionlike; +use std::cmp::Ordering; use bevy::prelude::Resource; use bevy::utils::HashMap; -use itertools::Itertools; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; + +use crate::action_state::ActionData; +use crate::input_map::InputMap; +use crate::input_streams::InputStreams; +use crate::user_input::UserInput; +use crate::Actionlike; /// How should clashing inputs by handled by an [`InputMap`]? /// @@ -25,7 +24,7 @@ use std::cmp::Ordering; /// - `ControlLeft + S`, `AltLeft + S` and `ControlLeft + AltLeft + S`: clashes /// /// This strategy is only used when assessing the actions and input holistically, -/// in [`InputMap::which_pressed`], using [`InputMap::handle_clashes`]. +/// in [`InputMap::process_actions`], using [`InputMap::handle_clashes`]. #[non_exhaustive] #[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)] pub enum ClashStrategy { @@ -47,38 +46,21 @@ impl ClashStrategy { } } -impl UserInput { - /// Does `self` clash with `other`? - #[must_use] - fn clashes(&self, other: &UserInput) -> bool { - use UserInput::*; - - match (self, other) { - (Single(_), Single(_)) => false, - (Chord(self_chord), Chord(other_chord)) => chord_chord_clash(self_chord, other_chord), - (Single(self_button), Chord(other_chord)) => { - button_chord_clash(self_button, other_chord) - } - (Chord(self_chord), Single(other_button)) => { - button_chord_clash(other_button, self_chord) - } - (VirtualDPad(self_dpad), Chord(other_chord)) => { - dpad_chord_clash(self_dpad, other_chord) - } - (Chord(self_chord), VirtualDPad(other_dpad)) => { - dpad_chord_clash(other_dpad, self_chord) - } - (VirtualAxis(self_axis), Chord(other_chord)) => { - virtual_axis_chord_clash(self_axis, other_chord) - } - (Chord(self_chord), VirtualAxis(other_axis)) => { - virtual_axis_chord_clash(other_axis, self_chord) - } - _ => self - .iter() - .any(|self_input| other.iter().contains(&self_input)), - } +/// Checks if the given two [`UserInput`]s clash with each other. +#[must_use] +#[inline] +fn check_clashed(lhs: &dyn UserInput, rhs: &dyn UserInput) -> bool { + #[inline] + fn clash(list_a: &[Box], list_b: &[Box]) -> bool { + // Checks if the length is greater than one, + // as simple, atomic input are able to trigger multiple actions. + list_a.len() > 1 && list_b.iter().all(|a| list_a.contains(a)) } + + let lhs_inners = lhs.destructure(); + let rhs_inners = rhs.destructure(); + + clash(&lhs_inners, &rhs_inners) || clash(&rhs_inners, &lhs_inners) } impl InputMap { @@ -151,7 +133,7 @@ impl InputMap { for input_a in self.get(action_a)? { for input_b in self.get(action_b)? { - if input_a.clashes(input_b) { + if check_clashed(input_a.as_ref(), input_b.as_ref()) { clash.inputs_a.push(input_a.clone()); clash.inputs_b.push(input_b.clone()); } @@ -169,8 +151,8 @@ impl InputMap { pub(crate) struct Clash { action_a: A, action_b: A, - inputs_a: Vec, - inputs_b: Vec, + inputs_a: Vec>, + inputs_b: Vec>, } impl Clash { @@ -186,40 +168,6 @@ impl Clash { } } -// Does the `button` clash with the `chord`? -#[must_use] -fn button_chord_clash(button: &InputKind, chord: &[InputKind]) -> bool { - chord.len() > 1 && chord.contains(button) -} - -// Does the `dpad` clash with the `chord`? -#[must_use] -fn dpad_chord_clash(dpad: &VirtualDPad, chord: &[InputKind]) -> bool { - chord.len() > 1 - && chord - .iter() - .any(|button| [&dpad.up, &dpad.down, &dpad.left, &dpad.right].contains(&button)) -} - -#[must_use] -fn virtual_axis_chord_clash(axis: &VirtualAxis, chord: &[InputKind]) -> bool { - chord.len() > 1 - && chord - .iter() - .any(|button| *button == axis.negative || *button == axis.positive) -} - -/// Does the `chord_a` clash with `chord_b`? -#[must_use] -fn chord_chord_clash(chord_a: &Vec, chord_b: &Vec) -> bool { - fn is_subset(slice_a: &[InputKind], slice_b: &[InputKind]) -> bool { - slice_a.iter().all(|a| slice_b.contains(a)) - } - - let prerequisite = chord_a.len() > 1 && chord_b.len() > 1 && chord_a != chord_b; - prerequisite && (is_subset(chord_a, chord_b) || is_subset(chord_b, chord_a)) -} - /// Given the `input_streams`, does the provided clash actually occur? /// /// Returns `Some(clash)` if they are clashing, and `None` if they are not. @@ -231,16 +179,16 @@ fn check_clash(clash: &Clash, input_streams: &InputStreams) -> for input_a in clash .inputs_a .iter() - .filter(|&input| input_streams.input_pressed(input)) + .filter(|&input| input.pressed(input_streams)) { // For all inputs actually pressed that match action B for input_b in clash .inputs_b .iter() - .filter(|&input| input_streams.input_pressed(input)) + .filter(|&input| input.pressed(input_streams)) { // If a clash was detected, - if input_a.clashes(input_b) { + if check_clashed(input_a.as_ref(), input_b.as_ref()) { actual_clash.inputs_a.push(input_a.clone()); actual_clash.inputs_b.push(input_b.clone()); } @@ -259,16 +207,18 @@ fn resolve_clash( input_streams: &InputStreams, ) -> Option { // Figure out why the actions are pressed - let reasons_a_is_pressed: Vec<&UserInput> = clash + let reasons_a_is_pressed: Vec> = clash .inputs_a .iter() - .filter(|&input| input_streams.input_pressed(input)) + .filter(|&input| input.pressed(input_streams)) + .cloned() .collect(); - let reasons_b_is_pressed: Vec<&UserInput> = clash + let reasons_b_is_pressed: Vec> = clash .inputs_b .iter() - .filter(|&input| input_streams.input_pressed(input)) + .filter(|&input| input.pressed(input_streams)) + .cloned() .collect(); // Clashes are spurious if the actions are pressed for any non-clashing reason @@ -276,7 +226,7 @@ fn resolve_clash( for reason_b in reasons_b_is_pressed.iter() { // If there is at least one non-clashing reason why these buttons should both be pressed, // we can avoid resolving the clash completely - if !reason_a.clashes(reason_b) { + if !check_clashed(reason_a.as_ref(), reason_b.as_ref()) { return None; } } @@ -290,13 +240,13 @@ fn resolve_clash( ClashStrategy::PrioritizeLongest => { let longest_a: usize = reasons_a_is_pressed .iter() - .map(|input| input.len()) + .map(|input| input.destructure().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); let longest_b: usize = reasons_b_is_pressed .iter() - .map(|input| input.len()) + .map(|input| input.destructure().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); @@ -311,14 +261,16 @@ fn resolve_clash( #[cfg(test)] mod tests { - use super::*; - use crate as leafwing_input_manager; - use crate::input_processing::DualAxisProcessor; use bevy::app::App; use bevy::input::keyboard::KeyCode::*; use bevy::prelude::Reflect; use leafwing_input_manager_macros::Actionlike; + use super::*; + use crate as leafwing_input_manager; + use crate::prelude::KeyboardVirtualDPad; + use crate::user_input::InputChord; + #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)] enum Action { One, @@ -340,27 +292,30 @@ mod tests { input_map.insert(One, Digit1); input_map.insert(Two, Digit2); - input_map.insert_chord(OneAndTwo, [Digit1, Digit2]); - input_map.insert_chord(TwoAndThree, [Digit2, Digit3]); - input_map.insert_chord(OneAndTwoAndThree, [Digit1, Digit2, Digit3]); - input_map.insert_chord(CtrlOne, [ControlLeft, Digit1]); - input_map.insert_chord(AltOne, [AltLeft, Digit1]); - input_map.insert_chord(CtrlAltOne, [ControlLeft, AltLeft, Digit1]); + input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); + input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); + input_map.insert( + OneAndTwoAndThree, + InputChord::from_multiple([Digit1, Digit2, Digit3]), + ); + input_map.insert(CtrlOne, InputChord::from_multiple([ControlLeft, Digit1])); + input_map.insert(AltOne, InputChord::from_multiple([AltLeft, Digit1])); input_map.insert( - MoveDPad, - VirtualDPad { - up: ArrowUp.into(), - down: ArrowDown.into(), - left: ArrowLeft.into(), - right: ArrowRight.into(), - processor: DualAxisProcessor::None, - }, + CtrlAltOne, + InputChord::from_multiple([ControlLeft, AltLeft, Digit1]), ); - input_map.insert_chord(CtrlUp, [ControlLeft, ArrowUp]); + input_map.insert(MoveDPad, KeyboardVirtualDPad::ARROW_KEYS); + input_map.insert(CtrlUp, InputChord::from_multiple([ControlLeft, ArrowUp])); input_map } + fn test_input_clash(input_a: A, input_b: B) -> bool { + let input_a: Box = Box::new(input_a); + let input_b: Box = Box::new(input_b); + check_clashed(input_a.as_ref(), input_b.as_ref()) + } + mod basic_functionality { use crate::input_mocking::MockInput; use bevy::input::InputPlugin; @@ -370,49 +325,28 @@ mod tests { #[test] fn clash_detection() { - let a: UserInput = KeyA.into(); - let b: UserInput = KeyB.into(); - let c: UserInput = KeyC.into(); - let ab = UserInput::chord([KeyA, KeyB]); - let bc = UserInput::chord([KeyB, KeyC]); - let abc = UserInput::chord([KeyA, KeyB, KeyC]); - let axyz_dpad: UserInput = VirtualDPad { - up: KeyA.into(), - down: KeyX.into(), - left: KeyY.into(), - right: KeyZ.into(), - processor: DualAxisProcessor::None, - } - .into(); - let abcd_dpad: UserInput = VirtualDPad { - up: KeyA.into(), - down: KeyB.into(), - left: KeyC.into(), - right: KeyD.into(), - processor: DualAxisProcessor::None, - } - .into(); - - let ctrl_up: UserInput = UserInput::chord([ArrowUp, ControlLeft]); - let directions_dpad: UserInput = VirtualDPad { - up: ArrowUp.into(), - down: ArrowDown.into(), - left: ArrowLeft.into(), - right: ArrowRight.into(), - processor: DualAxisProcessor::None, - } - .into(); - - assert!(!a.clashes(&b)); - assert!(a.clashes(&ab)); - assert!(!c.clashes(&ab)); - assert!(!ab.clashes(&bc)); - assert!(ab.clashes(&abc)); - assert!(axyz_dpad.clashes(&a)); - assert!(axyz_dpad.clashes(&ab)); - assert!(!axyz_dpad.clashes(&bc)); - assert!(axyz_dpad.clashes(&abcd_dpad)); - assert!(ctrl_up.clashes(&directions_dpad)); + let a = KeyA; + let b = KeyB; + let c = KeyC; + let ab = InputChord::from_multiple([KeyA, KeyB]); + let bc = InputChord::from_multiple([KeyB, KeyC]); + let abc = InputChord::from_multiple([KeyA, KeyB, KeyC]); + let axyz_dpad = KeyboardVirtualDPad::new(KeyA, KeyX, KeyY, KeyZ); + let abcd_dpad = KeyboardVirtualDPad::WASD; + + let ctrl_up = InputChord::from_multiple([ArrowUp, ControlLeft]); + let directions_dpad = KeyboardVirtualDPad::ARROW_KEYS; + + assert!(!test_input_clash(a, b)); + assert!(test_input_clash(a, ab.clone())); + assert!(!test_input_clash(c, ab.clone())); + assert!(!test_input_clash(ab.clone(), bc.clone())); + assert!(test_input_clash(ab.clone(), abc.clone())); + assert!(test_input_clash(axyz_dpad.clone(), a)); + assert!(test_input_clash(axyz_dpad.clone(), ab.clone())); + assert!(!test_input_clash(axyz_dpad.clone(), bc.clone())); + assert!(test_input_clash(axyz_dpad.clone(), abcd_dpad.clone())); + assert!(test_input_clash(ctrl_up.clone(), directions_dpad.clone())); } #[test] @@ -420,11 +354,12 @@ mod tests { let input_map = test_input_map(); let observed_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap(); + let correct_clash = Clash { action_a: One, action_b: OneAndTwo, - inputs_a: vec![Digit1.into()], - inputs_b: vec![UserInput::chord([Digit1, Digit2])], + inputs_a: vec![Box::new(Digit1)], + inputs_b: vec![Box::new(InputChord::from_multiple([Digit1, Digit2]))], }; assert_eq!(observed_clash, correct_clash); @@ -440,8 +375,10 @@ mod tests { let correct_clash = Clash { action_a: OneAndTwoAndThree, action_b: OneAndTwo, - inputs_a: vec![UserInput::chord([Digit1, Digit2, Digit3])], - inputs_b: vec![UserInput::chord([Digit1, Digit2])], + inputs_a: vec![Box::new(InputChord::from_multiple([ + Digit1, Digit2, Digit3, + ]))], + inputs_b: vec![Box::new(InputChord::from_multiple([Digit1, Digit2]))], }; assert_eq!(observed_clash, correct_clash); @@ -467,8 +404,8 @@ mod tests { let input_map = test_input_map(); let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap(); - app.send_input(Digit1); - app.send_input(Digit2); + app.press_input(Digit1); + app.press_input(Digit2); app.update(); let input_streams = InputStreams::from_world(&app.world, None); @@ -495,7 +432,7 @@ mod tests { let chord_clash = input_map .possible_clash(&OneAndTwo, &OneAndTwoAndThree) .unwrap(); - app.send_input(Digit3); + app.press_input(Digit3); app.update(); let input_streams = InputStreams::from_world(&app.world, None); @@ -516,8 +453,8 @@ mod tests { app.add_plugins(InputPlugin); let input_map = test_input_map(); - app.send_input(Digit1); - app.send_input(Digit2); + app.press_input(Digit1); + app.press_input(Digit2); app.update(); let mut action_data = HashMap::new(); @@ -547,8 +484,8 @@ mod tests { app.add_plugins(InputPlugin); let input_map = test_input_map(); - app.send_input(ControlLeft); - app.send_input(ArrowUp); + app.press_input(ControlLeft); + app.press_input(ArrowUp); app.update(); let mut action_data = HashMap::new(); @@ -575,12 +512,12 @@ mod tests { app.add_plugins(InputPlugin); let input_map = test_input_map(); - app.send_input(Digit1); - app.send_input(Digit2); - app.send_input(ControlLeft); + app.press_input(Digit1); + app.press_input(Digit2); + app.press_input(ControlLeft); app.update(); - let action_data = input_map.which_pressed( + let action_data = input_map.process_actions( &InputStreams::from_world(&app.world, None), ClashStrategy::PrioritizeLongest, ); diff --git a/src/display_impl.rs b/src/display_impl.rs deleted file mode 100644 index 5c01b880..00000000 --- a/src/display_impl.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Containment module for boring implementations of the [`Display`] trait - -use crate::axislike::{VirtualAxis, VirtualDPad}; -use crate::user_input::{InputKind, UserInput}; -use itertools::Itertools; -use std::fmt::Display; - -impl Display for UserInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - // The representation of the button - UserInput::Single(button) => write!(f, "{button}"), - // The representation of each button, separated by "+" - UserInput::Chord(button_set) => f.write_str(&button_set.iter().join("+")), - UserInput::VirtualDPad(VirtualDPad { - up, - down, - left, - right, - .. - }) => { - write!( - f, - "VirtualDPad(up: {up}, down: {down}, left: {left}, right: {right})" - ) - } - UserInput::VirtualAxis(VirtualAxis { - negative, positive, .. - }) => { - write!(f, "VirtualDPad(negative: {negative}, positive: {positive})") - } - } - } -} - -impl Display for InputKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InputKind::SingleAxis(axis) => write!(f, "{axis:?}"), - InputKind::DualAxis(axis) => write!(f, "{axis:?}"), - InputKind::GamepadButton(button) => write!(f, "{button:?}"), - InputKind::Mouse(button) => write!(f, "{button:?}"), - InputKind::MouseWheel(button) => write!(f, "{button:?}"), - InputKind::MouseMotion(button) => write!(f, "{button:?}"), - // TODO: We probably want to display the key on the currently active layout - InputKind::PhysicalKey(key_code) => write!(f, "{key_code:?}"), - InputKind::Modifier(button) => write!(f, "{button:?}"), - } - } -} diff --git a/src/input_map.rs b/src/input_map.rs index 9622422d..d4c02b48 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -1,82 +1,94 @@ //! This module contains [`InputMap`] and its supporting methods and impls. -use crate::action_state::ActionData; -use crate::buttonlike::ButtonState; -use crate::clashing_inputs::ClashStrategy; -use crate::input_streams::InputStreams; -use crate::user_input::*; -use crate::Actionlike; +use std::fmt::Debug; #[cfg(feature = "asset")] use bevy::asset::Asset; -use bevy::ecs::component::Component; -use bevy::ecs::system::Resource; -use bevy::input::gamepad::Gamepad; -use bevy::reflect::Reflect; +use bevy::prelude::{Component, Gamepad, Reflect, Resource}; use bevy::utils::HashMap; +use itertools::Itertools; use serde::{Deserialize, Serialize}; -use core::fmt::Debug; - -/** -Maps from raw inputs to an input-method agnostic representation - -Multiple inputs can be mapped to the same action, -and each input can be mapped to multiple actions. - -The provided input types must be able to be converted into a [`UserInput`]. - -By default, if two actions are triggered by a combination of buttons, -and one combination is a strict subset of the other, only the larger input is registered. -For example, pressing both `S` and `Ctrl + S` in your text editor app would save your file, -but not enter the letters `s`. -Set the [`ClashStrategy`] resource -to configure this behavior. - -# Example -```rust -use bevy::prelude::*; -use leafwing_input_manager::prelude::*; -use leafwing_input_manager::user_input::InputKind; - -// You can Run! -// But you can't Hide :( -#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] -enum Action { - Run, - Hide, -} - -// Construction -let mut input_map = InputMap::new([ - // Note that the type of your iterators must be homogeneous; - // you can use `InputKind` or `UserInput` if needed - // as unifying types - (Action::Run, GamepadButtonType::South), - (Action::Hide, GamepadButtonType::LeftTrigger), - (Action::Hide, GamepadButtonType::RightTrigger), -]); +use crate::action_state::ActionData; +use crate::buttonlike::ButtonState; +use crate::clashing_inputs::ClashStrategy; +use crate::input_streams::InputStreams; +use crate::user_input::UserInput; +use crate::Actionlike; -// Insertion -input_map.insert(Action::Run, MouseButton::Left) -.insert(Action::Run, KeyCode::ShiftLeft) -// Chords -.insert_modified(Action::Run, Modifier::Control, KeyCode::KeyR) -.insert_chord(Action::Run, - [InputKind::PhysicalKey(KeyCode::KeyH), - InputKind::GamepadButton(GamepadButtonType::South), - InputKind::Mouse(MouseButton::Middle)], - ); - -// Removal -input_map.clear_action(&Action::Hide); -``` - **/ +/// A Multi-Map that allows you to map actions to multiple [`UserInput`]s. +/// +/// # Many-to-One Mapping +/// +/// You can associate multiple [`UserInput`]s (e.g., keyboard keys, mouse buttons, gamepad buttons) +/// with a single action, simplifying handling complex input combinations for the same action. +/// Duplicate associations are ignored. +/// +/// # One-to-Many Mapping +/// +/// A single [`UserInput`] can be mapped to multiple actions simultaneously. +/// This allows flexibility in defining alternative ways to trigger an action. +/// +/// # Clash Resolution +/// +/// By default, the [`InputMap`] prioritizes larger [`UserInput`] combinations to trigger actions. +/// This means if two actions share some inputs, and one action requires all the inputs +/// of the other plus additional ones; only the larger combination will be registered. +/// +/// This avoids unintended actions from being triggered by more specific input combinations. +/// For example, pressing both `S` and `Ctrl + S` in your text editor app +/// would only save your file (the larger combination), and not enter the letter `s`. +/// +/// This behavior can be customized using the [`ClashStrategy`] resource. +/// +/// # Examples +/// +/// ```rust +/// use bevy::prelude::*; +/// use leafwing_input_manager::prelude::*; +/// use leafwing_input_manager::user_input::InputKind; +/// +/// // Define your actions. +/// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +/// enum Action { +/// Move, +/// Run, +/// Jump, +/// } +/// +/// // Create an InputMap from an iterable, +/// // allowing for multiple input types per action. +/// let mut input_map = InputMap::new([ +/// // Multiple inputs can be bound to the same action. +/// // Note that the type of your iterators must be homogeneous. +/// (Action::Run, KeyCode::ShiftLeft), +/// (Action::Run, KeyCode::ShiftRight), +/// // Note that duplicate associations are ignored. +/// (Action::Run, KeyCode::ShiftRight), +/// (Action::Jump, KeyCode::Space), +/// ]) +/// // Associate actions with other input types. +/// .with(Action::Move, KeyboardVirtualDPad::WASD) +/// .with(Action::Move, GamepadStick::LEFT) +/// // Associate an action with multiple inputs at once. +/// .with_one_to_many(Action::Jump, [KeyCode::KeyJ, KeyCode::KeyU]); +/// +/// // You can also use methods like a normal MultiMap. +/// input_map.insert(Action::Jump, KeyCode::KeyM); +/// +/// // Remove all bindings to a specific action. +/// input_map.clear_action(&Action::Jump); +/// +/// // Remove all bindings. +/// input_map.clear(); +/// ``` #[derive(Resource, Component, Debug, Clone, PartialEq, Eq, Reflect, Serialize, Deserialize)] #[cfg_attr(feature = "asset", derive(Asset))] pub struct InputMap { - /// The usize stored here is the index of the input in the Actionlike iterator - map: HashMap>, + /// The underlying map that stores action-input mappings. + map: HashMap>>, + + /// The specified [`Gamepad`] from which this map exclusively accepts input. associated_gamepad: Option, } @@ -91,157 +103,146 @@ impl Default for InputMap { // Constructors impl InputMap { - /// Creates a new [`InputMap`] from an iterator of `(user_input, action)` pairs - /// - /// To create an empty input map, use the [`Default::default`] method instead. - /// - /// # Example - /// ```rust - /// use leafwing_input_manager::input_map::InputMap; - /// use leafwing_input_manager::Actionlike; - /// use bevy::input::keyboard::KeyCode; - /// use bevy::prelude::Reflect; - /// - /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] - /// enum Action { - /// Run, - /// Jump, - /// } - /// - /// let input_map = InputMap::new([ - /// (Action::Run, KeyCode::ShiftLeft), - /// (Action::Jump, KeyCode::Space), - /// ]); + /// Creates an [`InputMap`] from an iterator over action-input bindings. + /// Note that all elements within the iterator must be of the same type (homogeneous). /// - /// assert_eq!(input_map.len(), 2); - /// ``` - #[must_use] - pub fn new(bindings: impl IntoIterator)>) -> Self { - let mut input_map = InputMap::default(); - input_map.insert_multiple(bindings); - - input_map + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn new(bindings: impl IntoIterator) -> Self { + bindings + .into_iter() + .fold(Self::default(), |map, (action, input)| { + map.with(action, input) + }) } - /// Constructs a new [`InputMap`] from a `&mut InputMap`, allowing you to insert or otherwise use it + /// Associates an `action` with a specific `input`. + /// Multiple inputs can be bound to the same action. /// - /// This is helpful when constructing input maps using the "builder pattern": - /// 1. Create a new [`InputMap`] struct using [`InputMap::default`] or [`InputMap::new`]. - /// 2. Add bindings and configure the struct using a chain of method calls directly on this struct. - /// 3. Finish building your struct by calling `.build()`, receiving a concrete struct you can insert as a component. - /// - /// Note that this is not the *original* input map, as we do not have ownership of the struct. - /// Under the hood, this is just a more-readable call to `.clone()`. - /// - /// # Example - /// ```rust - /// use leafwing_input_manager::prelude::*; + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with(mut self, action: A, input: impl UserInput) -> Self { + self.insert(action, input); + self + } - /// use bevy::input::keyboard::KeyCode; - /// use bevy::prelude::Reflect; + /// Associates an `action` with multiple `inputs` provided by an iterator. + /// Note that all elements within the iterator must be of the same type (homogeneous). /// - /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] - /// enum Action { - /// Run, - /// Jump, - /// } + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with_one_to_many( + mut self, + action: A, + inputs: impl IntoIterator, + ) -> Self { + self.insert_one_to_many(action, inputs); + self + } + + /// Adds multiple action-input bindings provided by an iterator. + /// Note that all elements within the iterator must be of the same type (homogeneous). /// - /// let input_map: InputMap = InputMap::default() - /// .insert(Action::Jump, KeyCode::Space).build(); - /// ``` - #[inline] - #[must_use] - pub fn build(&mut self) -> Self { - self.clone() + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn with_multiple( + mut self, + bindings: impl IntoIterator, + ) -> Self { + self.insert_multiple(bindings); + self } } // Insertion impl InputMap { - /// Insert a mapping between `input` and `action` - pub fn insert(&mut self, action: A, input: impl Into) -> &mut Self { - let input = input.into(); - - // Check for existing copies of the input: insertion should be idempotent - if !matches!(self.map.get(&action), Some(vec) if vec.contains(&input)) { - self.map.entry(action).or_default().push(input); - } - - self - } - - /// Insert a mapping between many `input`'s and one `action` + /// Inserts a binding between an `action` and a specific boxed dyn [`UserInput`]. + /// Multiple inputs can be bound to the same action. + /// + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn insert_one_to_many( - &mut self, - action: A, - input: impl IntoIterator>, - ) -> &mut Self { - for input in input { - self.insert(action.clone(), input); + fn insert_boxed(&mut self, action: A, input: Box) -> &mut Self { + if let Some(bindings) = self.map.get_mut(&action) { + if !bindings.contains(&input) { + bindings.push(input); + } + } else { + self.map.insert(action, vec![input]); } + self } - /// Insert a mapping between the provided `input_action_pairs` + /// Inserts a binding between an `action` and a specific `input`. + /// Multiple inputs can be bound to the same action. /// - /// This method creates multiple distinct bindings. - /// If you want to require multiple buttons to be pressed at once, use [`insert_chord`](Self::insert_chord). - /// Any iterator convertible into a [`UserInput`] can be supplied. - pub fn insert_multiple( - &mut self, - input_action_pairs: impl IntoIterator)>, - ) -> &mut Self { - for (action, input) in input_action_pairs { - self.insert(action, input); - } - + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn insert(&mut self, action: A, input: impl UserInput) -> &mut Self { + self.insert_boxed(action, Box::new(input)); self } - /// Insert a mapping between the simultaneous combination of `buttons` and the `action` provided + /// Inserts bindings between the same `action` and multiple `inputs` provided by an iterator. + /// Note that all elements within the iterator must be of the same type (homogeneous). /// - /// Any iterator convertible into a [`InputKind`] can be supplied, - /// but will be converted into a [`HashSet`](bevy::utils::HashSet) for storage and use. - /// Chords can also be added with the [insert](Self::insert) method if the [`UserInput::Chord`] variant is constructed explicitly. - /// - /// When working with keyboard modifier keys, consider using the `insert_modified` method instead. - pub fn insert_chord( + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn insert_one_to_many( &mut self, action: A, - buttons: impl IntoIterator>, + inputs: impl IntoIterator, ) -> &mut Self { - self.insert(action, UserInput::chord(buttons)); + let inputs = inputs + .into_iter() + .map(|input| Box::new(input) as Box); + if let Some(bindings) = self.map.get_mut(&action) { + for input in inputs { + if !bindings.contains(&input) { + bindings.push(input); + } + } + } else { + self.map.insert(action, inputs.unique().collect()); + } self } - /// Inserts a mapping between the simultaneous combination of the [`Modifier`] plus the `input` and the `action` provided. + /// Inserts multiple action-input bindings provided by an iterator. + /// Note that all elements within the iterator must be of the same type (homogeneous). /// - /// When working with keyboard modifiers, should be preferred over `insert_chord`. - pub fn insert_modified( + /// This method ensures idempotence, meaning that adding the same input + /// for the same action multiple times will only result in a single binding being created. + #[inline(always)] + pub fn insert_multiple( &mut self, - action: A, - modifier: Modifier, - input: impl Into, + bindings: impl IntoIterator, ) -> &mut Self { - self.insert(action, UserInput::modified(modifier, input)); + for (action, input) in bindings.into_iter() { + self.insert(action, input); + } self } - /// Merges the provided [`InputMap`] into the [`InputMap`] this method was called on + /// Merges the provided [`InputMap`] into this `map`, combining their bindings, + /// avoiding duplicates. /// - /// This adds both of their bindings to the resulting [`InputMap`]. - /// Like usual, any duplicate bindings are ignored. - /// - /// If the associated gamepads do not match, the resulting associated gamepad will be set to `None`. + /// If the associated gamepads do not match, the association will be removed. pub fn merge(&mut self, other: &InputMap) -> &mut Self { if self.associated_gamepad != other.associated_gamepad { - self.associated_gamepad = None; + self.clear_gamepad(); } - for other_action in other.map.iter() { - for input in other_action.1.iter() { - self.insert(other_action.0.clone(), input.clone()); + for (other_action, other_inputs) in other.map.iter() { + for other_input in other_inputs.iter().cloned() { + self.insert_boxed(other_action.clone(), other_input); } } @@ -251,18 +252,19 @@ impl InputMap { // Configuration impl InputMap { - /// Fetches the [Gamepad] associated with the entity controlled by this entity map + /// Fetches the [`Gamepad`] associated with the entity controlled by this input map. /// /// If this is [`None`], input from any connected gamepad will be used. #[must_use] - pub fn gamepad(&self) -> Option { + #[inline] + pub const fn gamepad(&self) -> Option { self.associated_gamepad } - /// Assigns a particular [`Gamepad`] to the entity controlled by this input map + /// Assigns a particular [`Gamepad`] to the entity controlled by this input map. /// - /// Use this when an [`InputMap`] should exclusively accept input from a - /// particular gamepad. + /// Use this when an [`InputMap`] should exclusively accept input + /// from a particular gamepad. /// /// If this is not called, input from any connected gamepad will be used. /// The first matching non-zero input will be accepted, @@ -270,24 +272,42 @@ impl InputMap { /// /// Because of this robust fallback behavior, /// this method can typically be ignored when writing single-player games. + #[inline] + pub fn with_gamepad(mut self, gamepad: Gamepad) -> Self { + self.set_gamepad(gamepad); + self + } + + /// Assigns a particular [`Gamepad`] to the entity controlled by this input map. + /// + /// Use this when an [`InputMap`] should exclusively accept input + /// from a particular gamepad. + /// + /// If this is not called, input from any connected gamepad will be used. + /// The first matching non-zero input will be accepted, + /// as determined by gamepad registration order. + /// + /// Because of this robust fallback behavior, + /// this method can typically be ignored when writing single-player games. + #[inline] pub fn set_gamepad(&mut self, gamepad: Gamepad) -> &mut Self { self.associated_gamepad = Some(gamepad); self } - /// Clears any [Gamepad] associated with the entity controlled by this input map + /// Clears any [`Gamepad`] associated with the entity controlled by this input map. + #[inline] pub fn clear_gamepad(&mut self) -> &mut Self { self.associated_gamepad = None; self } } -// Check whether buttons are pressed +// Check whether actions are pressed impl InputMap { - /// Is at least one of the corresponding inputs for `action` found in the provided `input` streams? + /// Checks if the `action` are currently pressed by any of the associated [`UserInput`]s. /// - /// Accounts for clashing inputs according to the [`ClashStrategy`]. - /// If you need to inspect many inputs at once, prefer [`InputMap::which_pressed`] instead. + /// Accounts for clashing inputs according to the [`ClashStrategy`] and remove conflicting actions. #[must_use] pub fn pressed( &self, @@ -295,17 +315,17 @@ impl InputMap { input_streams: &InputStreams, clash_strategy: ClashStrategy, ) -> bool { - self.which_pressed(input_streams, clash_strategy) + self.process_actions(input_streams, clash_strategy) .get(action) .map(|datum| datum.state.pressed()) .unwrap_or_default() } - /// Returns the actions that are currently pressed, and the responsible [`UserInput`] for each action + /// Processes [`UserInput`] bindings for each action and generates corresponding [`ActionData`]. /// - /// Accounts for clashing inputs according to the [`ClashStrategy`]. + /// Accounts for clashing inputs according to the [`ClashStrategy`] and remove conflicting actions. #[must_use] - pub fn which_pressed( + pub fn process_actions( &self, input_streams: &InputStreams, clash_strategy: ClashStrategy, @@ -313,12 +333,12 @@ impl InputMap { let mut action_data = HashMap::new(); // Generate the raw action presses - for (action, input_vec) in self.iter() { + for (action, input_bindings) in self.iter() { let mut action_datum = ActionData::default(); - for input in input_vec { + for input in input_bindings { // Merge the axis pair into action datum - if let Some(axis_pair) = input_streams.input_axis_pair(input) { + if let Some(axis_pair) = input.axis_pair(input_streams) { action_datum.axis_pair = action_datum .axis_pair .map_or(Some(axis_pair), |current_axis_pair| { @@ -326,9 +346,9 @@ impl InputMap { }); } - if input_streams.input_pressed(input) { + if input.pressed(input_streams) { action_datum.state = ButtonState::JustPressed; - action_datum.value += input_streams.input_value(input); + action_datum.value += input.value(input_streams); } } @@ -344,40 +364,49 @@ impl InputMap { // Utilities impl InputMap { - /// Returns an iterator over actions with their inputs - pub fn iter(&self) -> impl Iterator)> { + /// Returns an iterator over all registered actions with their input bindings. + pub fn iter(&self) -> impl Iterator>)> { self.map.iter() } - /// Returns an iterator over actions - pub(crate) fn actions(&self) -> impl Iterator { + + /// Returns an iterator over all registered action-input bindings. + pub fn bindings(&self) -> impl Iterator { + self.map + .iter() + .flat_map(|(action, inputs)| inputs.iter().map(move |input| (action, input.as_ref()))) + } + + /// Returns an iterator over all registered actions. + pub fn actions(&self) -> impl Iterator { self.map.keys() } - /// Returns a reference to the inputs mapped to `action` + + /// Returns a reference to the inputs associated with the given `action`. #[must_use] - pub fn get(&self, action: &A) -> Option<&Vec> { + pub fn get(&self, action: &A) -> Option<&Vec>> { self.map.get(action) } /// Returns a mutable reference to the inputs mapped to `action` #[must_use] - pub fn get_mut(&mut self, action: &A) -> Option<&mut Vec> { + pub fn get_mut(&mut self, action: &A) -> Option<&mut Vec>> { self.map.get_mut(action) } - /// How many input bindings are registered total? + /// Count the total number of registered input bindings. #[must_use] pub fn len(&self) -> usize { self.map.values().map(|inputs| inputs.len()).sum() } - /// Are any input bindings registered at all? + /// Returns `true` if the map contains no action-input bindings. #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Clears the map, removing all action-inputs pairs. + /// Clears the map, removing all action-input bindings. /// /// Keeps the allocated memory for reuse. pub fn clear(&mut self) { @@ -387,67 +416,74 @@ impl InputMap { // Removing impl InputMap { - /// Clears all inputs registered for the `action` + /// Clears all input bindings associated with the `action`. pub fn clear_action(&mut self, action: &A) { self.map.remove(action); } - /// Removes the input for the `action` at the provided index + /// Removes the input for the `action` at the provided index. /// /// Returns `Some(input)` if found. - pub fn remove_at(&mut self, action: &A, index: usize) -> Option { - let input_vec = self.map.get_mut(action)?; - (input_vec.len() > index).then(|| input_vec.remove(index)) + pub fn remove_at(&mut self, action: &A, index: usize) -> Option> { + let input_bindings = self.map.get_mut(action)?; + (input_bindings.len() > index).then(|| input_bindings.remove(index)) } /// Removes the input for the `action` if it exists /// /// Returns [`Some`] with index if the input was found, or [`None`] if no matching input was found. - pub fn remove(&mut self, action: &A, input: impl Into) -> Option { - let input_vec = self.map.get_mut(action)?; - let user_input = input.into(); - let index = input_vec.iter().position(|i| i == &user_input)?; - input_vec.remove(index); + pub fn remove(&mut self, action: &A, input: impl UserInput) -> Option { + let bindings = self.map.get_mut(action)?; + let boxed_input: Box = Box::new(input); + let index = bindings.iter().position(|input| input == &boxed_input)?; + bindings.remove(index); Some(index) } } -impl From>> for InputMap { - /// Create `InputMap` from `HashMap>` +impl From>> for InputMap { + /// Converts a [`HashMap`] mapping actions to multiple [`UserInput`]s into an [`InputMap`]. /// - /// # Example - /// ```rust - /// use leafwing_input_manager::input_map::InputMap; - /// use leafwing_input_manager::user_input::UserInput; - /// use leafwing_input_manager::Actionlike; - /// use bevy::input::keyboard::KeyCode; - /// use bevy::reflect::Reflect; + /// # Examples /// + /// ```rust + /// use bevy::prelude::*; /// use bevy::utils::HashMap; + /// use leafwing_input_manager::prelude::*; /// /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] /// enum Action { /// Run, /// Jump, /// } - /// let mut map: HashMap> = HashMap::default(); + /// + /// // Create an InputMap from a HashMap mapping actions to their key bindings. + /// let mut map: HashMap> = HashMap::default(); + /// + /// // Bind the "run" action to either the left or right shift keys to trigger the action. /// map.insert( /// Action::Run, - /// vec![KeyCode::ShiftLeft.into(), KeyCode::ShiftRight.into()], + /// vec![KeyCode::ShiftLeft, KeyCode::ShiftRight], /// ); - /// let input_map = InputMap::::from(map); + /// + /// let input_map = InputMap::from(map); /// ``` - fn from(map: HashMap>) -> Self { - map.iter() - .flat_map(|(action, inputs)| inputs.iter().map(|input| (action.clone(), input.clone()))) - .collect() + fn from(raw_map: HashMap>) -> Self { + let mut input_map = Self::default(); + for (action, inputs) in raw_map.into_iter() { + input_map.insert_one_to_many(action, inputs); + } + input_map } } -impl FromIterator<(A, UserInput)> for InputMap { - /// Create `InputMap` from iterator with the item type `(A, UserInput)` - fn from_iter>(iter: T) -> Self { - InputMap::new(iter) +impl FromIterator<(A, U)> for InputMap { + fn from_iter>(iter: T) -> Self { + let mut input_map = Self::default(); + for (action, input) in iter.into_iter() { + input_map.insert(action, input); + } + input_map } } @@ -455,6 +491,7 @@ mod tests { use bevy::prelude::Reflect; use serde::{Deserialize, Serialize}; + use super::*; use crate as leafwing_input_manager; use crate::prelude::*; @@ -479,64 +516,82 @@ mod tests { } #[test] - fn insertion_idempotency() { + fn creation() { use bevy::input::keyboard::KeyCode; - let mut input_map = InputMap::::default(); - input_map.insert(Action::Run, KeyCode::Space); - - assert_eq!( - input_map.get(&Action::Run), - Some(&vec![KeyCode::Space.into()]) - ); - - // Duplicate insertions should not change anything - input_map.insert(Action::Run, KeyCode::Space); - assert_eq!( - input_map.get(&Action::Run), - Some(&vec![KeyCode::Space.into()]) - ); + let input_map = InputMap::default() + .with(Action::Run, KeyCode::KeyW) + .with(Action::Run, KeyCode::ShiftLeft) + // Duplicate associations should be ignored + .with(Action::Run, KeyCode::ShiftLeft) + .with_one_to_many(Action::Run, [KeyCode::KeyR, KeyCode::ShiftRight]) + .with_multiple([ + (Action::Jump, KeyCode::Space), + (Action::Hide, KeyCode::ControlLeft), + (Action::Hide, KeyCode::ControlRight), + ]); + + let expected_bindings: HashMap, Action> = HashMap::from([ + (Box::new(KeyCode::KeyW) as Box, Action::Run), + ( + Box::new(KeyCode::ShiftLeft) as Box, + Action::Run, + ), + (Box::new(KeyCode::KeyR) as Box, Action::Run), + ( + Box::new(KeyCode::ShiftRight) as Box, + Action::Run, + ), + (Box::new(KeyCode::Space) as Box, Action::Jump), + ( + Box::new(KeyCode::ControlLeft) as Box, + Action::Hide, + ), + ( + Box::new(KeyCode::ControlRight) as Box, + Action::Hide, + ), + ]); + + for (action, input) in input_map.bindings() { + let expected_action = expected_bindings.get(input).unwrap(); + assert_eq!(expected_action, action); + } } #[test] - fn multiple_insertion() { + fn insertion_idempotency() { use bevy::input::keyboard::KeyCode; - let mut input_map_1 = InputMap::::default(); - input_map_1.insert(Action::Run, KeyCode::Space); - input_map_1.insert(Action::Run, KeyCode::Enter); - - assert_eq!( - input_map_1.get(&Action::Run), - Some(&vec![KeyCode::Space.into(), KeyCode::Enter.into()]) - ); + let mut input_map = InputMap::default(); + input_map.insert(Action::Run, KeyCode::Space); - let input_map_2 = - InputMap::::new([(Action::Run, KeyCode::Space), (Action::Run, KeyCode::Enter)]); + let expected: Vec> = vec![Box::new(KeyCode::Space)]; + assert_eq!(input_map.get(&Action::Run), Some(&expected)); - assert_eq!(input_map_1, input_map_2); + // Duplicate insertions should not change anything + input_map.insert(Action::Run, KeyCode::Space); + assert_eq!(input_map.get(&Action::Run), Some(&expected)); } #[test] - fn chord_singleton_coercion() { - use crate::input_map::UserInput; + fn multiple_insertion() { use bevy::input::keyboard::KeyCode; - // Single items in a chord should be coerced to a singleton - let mut input_map_1 = InputMap::::default(); - input_map_1.insert(Action::Run, KeyCode::Space); - - let mut input_map_2 = InputMap::::default(); - input_map_2.insert(Action::Run, UserInput::chord([KeyCode::Space])); + let mut input_map = InputMap::default(); + input_map.insert(Action::Run, KeyCode::Space); + input_map.insert(Action::Run, KeyCode::Enter); - assert_eq!(input_map_1, input_map_2); + let expected: Vec> = + vec![Box::new(KeyCode::Space), Box::new(KeyCode::Enter)]; + assert_eq!(input_map.get(&Action::Run), Some(&expected)); } #[test] fn input_clearing() { use bevy::input::keyboard::KeyCode; - let mut input_map = InputMap::::default(); + let mut input_map = InputMap::default(); input_map.insert(Action::Run, KeyCode::Space); // Clearing action @@ -565,7 +620,11 @@ mod tests { let mut input_map = InputMap::default(); let mut default_keyboard_map = InputMap::default(); default_keyboard_map.insert(Action::Run, KeyCode::ShiftLeft); - default_keyboard_map.insert_chord(Action::Hide, [KeyCode::ControlLeft, KeyCode::KeyH]); + default_keyboard_map.insert( + Action::Hide, + InputChord::from_multiple([KeyCode::ControlLeft, KeyCode::KeyH]), + ); + let mut default_gamepad_map = InputMap::default(); default_gamepad_map.insert(Action::Run, GamepadButtonType::South); default_gamepad_map.insert(Action::Hide, GamepadButtonType::East); diff --git a/src/input_mocking.rs b/src/input_mocking.rs index 9d32b01f..c44ede4a 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -1,116 +1,176 @@ //! Helpful utilities for testing input management by sending mock input events //! //! The [`MockInput`] trait contains methods with the same API that operate at three levels: -//! [`App`], [`World`] and [`MutableInputStreams`], each passing down the supplied arguments to the next. //! -//! Inputs are provided in the convenient, high-level [`UserInput`] form. -//! These are then parsed down to their [`UserInput::raw_inputs()`], -//! which are then sent as [`bevy::input`] events of the appropriate types. +//! 1. [`App`]. +//! 2. [`World`]. +//! 3. [`MutableInputStreams`]. +//! +//! Each passing down the supplied arguments to the next. -use crate::axislike::{AxisType, MouseMotionAxisType, MouseWheelAxisType}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::input_streams::{InputStreams, MutableInputStreams}; -use crate::user_input::{RawInputs, UserInput}; +use bevy::ecs::system::SystemState; +use bevy::input::gamepad::{Gamepad, GamepadButton, GamepadButtonType, GamepadEvent}; +use bevy::input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent}; +use bevy::input::keyboard::{Key, KeyCode, KeyboardInput, NativeKey}; +use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}; +use bevy::input::touch::{TouchInput, Touches}; +use bevy::input::{ButtonInput, ButtonState}; +use bevy::prelude::{App, Entity, Events, GamepadAxisType, ResMut, Vec2, World}; +use bevy::window::CursorMoved; -use bevy::app::App; -use bevy::ecs::event::Events; -use bevy::ecs::system::{ResMut, SystemState}; -use bevy::ecs::world::World; #[cfg(feature = "ui")] use bevy::ecs::{component::Component, query::With, system::Query}; -use bevy::input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent}; -use bevy::input::keyboard::{Key, NativeKey}; -use bevy::input::mouse::MouseScrollUnit; -use bevy::input::ButtonState; -use bevy::input::{ - gamepad::{Gamepad, GamepadButton, GamepadEvent}, - keyboard::{KeyCode, KeyboardInput}, - mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, - touch::{TouchInput, Touches}, - ButtonInput, -}; -use bevy::math::Vec2; -use bevy::prelude::Entity; #[cfg(feature = "ui")] use bevy::ui::Interaction; -use bevy::window::CursorMoved; + +use crate::input_streams::{InputStreams, MutableInputStreams}; +use crate::user_input::*; /// Send fake input events for testing purposes /// /// In game code, you should (almost) always be setting the [`ActionState`](crate::action_state::ActionState) /// directly instead. /// +/// # Warning +/// +/// You *must* call [`app.update()`](App::update) at least once after sending input +/// with [`InputPlugin`](bevy::input::InputPlugin) included in your plugin set +/// for the raw input events to be processed into [`ButtonInput`] and [`Axis`](bevy::prelude::Axis) data. +/// /// # Examples +/// /// ```rust /// use bevy::prelude::*; /// use bevy::input::InputPlugin; /// use leafwing_input_manager::input_mocking::MockInput; +/// use leafwing_input_manager::prelude::*; /// -/// // Remember to add InputPlugin so the resources will be there! /// let mut app = App::new(); +/// +/// // This functionality requires Bevy's InputPlugin to be present in your plugin set. +/// // If you included the `DefaultPlugins`, then InputPlugin is already included. /// app.add_plugins(InputPlugin); /// -/// // Pay respects! -/// app.send_input(KeyCode::KeyF); -/// app.update(); -/// ``` +/// // Press a key press directly. +/// app.press_input(KeyCode::KeyD); /// -/// ```rust -/// use bevy::prelude::*; -/// use bevy::input::InputPlugin; -/// use leafwing_input_manager::{input_mocking::MockInput, user_input::UserInput}; +/// // Or use chords to press multiple keys at the same time! +/// let bevy = [KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY]; +/// app.press_input(InputChord::from_multiple(bevy)); /// -/// let mut app = App::new(); -/// app.add_plugins(InputPlugin); +/// // Send values to an axis. +/// app.send_axis_values(MouseScrollAxis::Y, [5.0]); +/// +/// // Send values to two axes. +/// app.send_axis_values(MouseMove::RAW, [5.0, 8.0]); /// -/// // Send inputs one at a time -/// let B_E_V_Y = [KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY]; +/// // Release or deactivate an input. +/// app.release_input(KeyCode::KeyR); /// -/// for letter in B_E_V_Y { -/// app.send_input(letter); -/// } +/// // Reset all inputs to their default state. +/// app.reset_inputs(); /// -/// // Or use chords! -/// app.send_input(UserInput::chord(B_E_V_Y)); +/// // Remember to call the update method at least once after sending input. /// app.update(); /// ``` pub trait MockInput { - /// Send the specified `user_input` directly + /// Simulates an activated event for the given `input`, + /// pressing all buttons and keys in the [`RawInputs`](raw_inputs::RawInputs) of the `input`. /// - /// These are sent as the raw input events, and do not set the value of [`ButtonInput`] or [`Axis`](bevy::input::Axis) directly. - /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called. + /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, + /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. + /// Axial inputs (e.g., analog thumb sticks) aren't affected. + /// Use [`Self::send_axis_values`] for those. /// - /// To send specific values for axislike inputs, set their `value` field. + /// # Input State Persistence /// - /// Gamepad input will be sent by the first registered controller found. - /// If none are found, gamepad input will be silently skipped. + /// Pressed inputs remain active until explicitly released or by calling [`Self::reset_inputs`]. /// - /// # Warning + /// # Gamepad Input /// - /// You *must* call `app.update()` at least once after sending input - /// with `InputPlugin` included in your plugin set - /// for the raw input events to be processed into [`ButtonInput`] and [`Axis`](bevy::input::Axis) data. - fn send_input(&mut self, input: impl Into); - - /// Send the specified `user_input` directly, using the specified gamepad + /// Gamepad input is sent by the first registered controller. + /// If no controllers are found, it is silently ignored. /// - /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called. + /// # Limitations /// - /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating. - fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option); - - /// Releases the specified `user_input` directly + /// Unfortunately, due to upstream constraints, + /// pressing a [`GamepadButtonType`] has no effect + /// because Bevy currently disregards all external [`GamepadButtonChangedEvent`] events. + /// See for more details. + fn press_input(&mut self, input: impl UserInput); + + /// Simulates an activated event for the given `input`, using the specified `gamepad`, + /// pressing all buttons and keys in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. /// - /// Gamepad input will be released by the first registered controller found. - /// If none are found, gamepad input will be silently skipped. - fn release_input(&mut self, input: impl Into); + /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, + /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. + /// Axial inputs (e.g., analog thumb sticks) aren't affected. + /// Use [`Self::send_axis_values_as_gamepad`] for those. + /// + /// # Input State Persistence + /// + /// Pressed inputs remain active until explicitly released or by calling [`Self::reset_inputs`]. + /// + /// # Limitations + /// + /// Unfortunately, due to upstream constraints, + /// pressing a [`GamepadButtonType`] has no effect + /// because Bevy currently disregards all external [`GamepadButtonChangedEvent`] events. + /// See for more details. + fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option); + + /// Simulates axis value changed events for the given `input`. + /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. + /// Missing axis values default to `0.0`. + /// + /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs + /// like [`MouseScrollAxis::Y`], [`MouseMove::RAW`] and [`GamepadStick::LEFT`]. + /// Non-axial inputs (e.g., keys and buttons) aren't affected; + /// the current value will be retained for the next encountered axis. + /// Use [`Self::press_input`] for those. + /// + /// # Input State Persistence + /// + /// Each axis remains at the specified value until explicitly changed or by calling [`Self::reset_inputs`]. + /// + /// # Gamepad Input + /// + /// Gamepad input is sent by the first registered controller. + /// If no controllers are found, it is silently ignored. + fn send_axis_values(&mut self, input: impl UserInput, values: impl IntoIterator); - /// Releases the specified `user_input` directly, using the specified gamepad + /// Simulates axis value changed events for the given `input`, using the specified `gamepad`. + /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. + /// Missing axis values default to `0.0`. + /// + /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs + /// like [`MouseScrollAxis::Y`], [`MouseMove::RAW`] and [`GamepadStick::LEFT`]. + /// Non-axial inputs (e.g., keys and buttons) aren't affected; + /// the current value will be retained for the next encountered axis. + /// Use [`Self::press_input_as_gamepad`] for those. + /// + /// # Input State Persistence /// - /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating. - fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option); + /// Each axis remains at the specified value until explicitly changed or by calling [`Self::reset_inputs`]. + fn send_axis_values_as_gamepad( + &mut self, + input: impl UserInput, + values: impl IntoIterator, + gamepad: Option, + ); + + /// Simulates a released or deactivated event for the given `input`. + /// + /// # Gamepad Input + /// + /// Gamepad input is sent by the first registered controller. + /// If no controllers are found, it is silently ignored. + fn release_input(&mut self, input: impl UserInput); + + /// Simulates a released or deactivated event for the given `input`, using the specified `gamepad`. + fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option); - /// Clears all user input streams, resetting them to their default state + /// Resets all inputs in the [`MutableInputStreams`] to their default state. /// /// All buttons are released, and `just_pressed` and `just_released` information on the [`ButtonInput`] type are lost. /// `just_pressed` and `just_released` on the [`ActionState`](crate::action_state::ActionState) will be kept. @@ -125,17 +185,17 @@ pub trait MockInput { /// In game code, you should (almost) always be using [`ActionState`](crate::action_state::ActionState) /// methods instead. pub trait QueryInput { - /// Is the provided `user_input` pressed? + /// Checks if the `input` is currently pressed. /// /// This method is intended as a convenience for testing; check the [`ButtonInput`] resource directly, /// or use an [`InputMap`](crate::input_map::InputMap) in real code. - fn pressed(&self, input: impl Into) -> bool; + fn pressed(&self, input: impl UserInput) -> bool; - /// Is the provided `user_input` pressed for the provided [`Gamepad`]? + /// Checks if the `input` is currently pressed the specified [`Gamepad`]. /// /// This method is intended as a convenience for testing; check the [`ButtonInput`] resource directly, /// or use an [`InputMap`](crate::input_map::InputMap) in real code. - fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool; + fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool; } /// Send fake UI interaction for testing purposes. @@ -153,96 +213,125 @@ pub trait MockUIInteraction { } impl MockInput for MutableInputStreams<'_> { - fn send_input(&mut self, input: impl Into) { - self.send_input_as_gamepad(input, self.guess_gamepad()); + fn press_input(&mut self, input: impl UserInput) { + self.press_input_as_gamepad(input, self.guess_gamepad()); } - fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { - let input_to_send: UserInput = input.into(); - // Extract the raw inputs - let raw_inputs = input_to_send.raw_inputs(); + fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { + let raw_inputs = input.raw_inputs(); - self.send_keyboard_input(ButtonState::Pressed, &raw_inputs); + // Press KeyCode + for keycode in raw_inputs.keycodes.iter() { + self.send_keycode_state(keycode, ButtonState::Pressed); + } - // Mouse buttons + // Press MouseButton for button in raw_inputs.mouse_buttons.iter() { - self.mouse_button_events.send(MouseButtonInput { - button: *button, - state: ButtonState::Pressed, - window: Entity::PLACEHOLDER, - }); + self.send_mouse_button_state(button, ButtonState::Pressed); } - // Discrete mouse wheel events - for mouse_wheel_direction in raw_inputs.mouse_wheel.iter() { - match *mouse_wheel_direction { - MouseWheelDirection::Left => self.send_mouse_wheel(-1.0, 0.0), - MouseWheelDirection::Right => self.send_mouse_wheel(1.0, 0.0), - MouseWheelDirection::Up => self.send_mouse_wheel(0.0, 1.0), - MouseWheelDirection::Down => self.send_mouse_wheel(0.0, -1.0), - }; + // Press MouseMoveDirection, discrete mouse motion events + for direction in raw_inputs.mouse_move_directions.iter() { + self.send_mouse_move(direction.0.full_active_value()); } - // Discrete mouse motion event - for mouse_motion_direction in raw_inputs.mouse_motion.iter() { - match *mouse_motion_direction { - MouseMotionDirection::Up => self.send_mouse_motion(0.0, 1.0), - MouseMotionDirection::Down => self.send_mouse_motion(0.0, -1.0), - MouseMotionDirection::Right => self.send_mouse_motion(1.0, 0.0), - MouseMotionDirection::Left => self.send_mouse_motion(-1.0, 0.0), - }; + // Press MouseScrollDirection, discrete mouse wheel events + for direction in raw_inputs.mouse_scroll_directions.iter() { + self.send_mouse_scroll(direction.0.full_active_value()); } - self.send_gamepad_button_changed(gamepad, &raw_inputs); - - // Axis data - for (outer_axis_type, maybe_position_data) in raw_inputs.axis_data.iter() { - if let Some(position_data) = *maybe_position_data { - match outer_axis_type { - AxisType::Gamepad(axis_type) => { - if let Some(gamepad) = gamepad { - self.gamepad_events - .send(GamepadEvent::Axis(GamepadAxisChangedEvent { - gamepad, - axis_type: *axis_type, - value: position_data, - })); - } - } - AxisType::MouseWheel(axis_type) => match *axis_type { - MouseWheelAxisType::X => self.send_mouse_wheel(position_data, 0.0), - MouseWheelAxisType::Y => self.send_mouse_wheel(0.0, position_data), - }, - AxisType::MouseMotion(axis_type) => match *axis_type { - MouseMotionAxisType::X => self.send_mouse_motion(position_data, 0.0), - MouseMotionAxisType::Y => self.send_mouse_motion(0.0, position_data), - }, - } + if let Some(gamepad) = gamepad { + for direction in raw_inputs.gamepad_control_directions.iter() { + self.send_gamepad_axis_value( + gamepad, + &direction.axis, + direction.direction.full_active_value(), + ); + } + + // Press GamepadButtonType. + // Unfortunately, due to upstream constraints, this has no effect + // because Bevy currently disregards all external GamepadButtonChangedEvent events. + // See for more details. + for button in raw_inputs.gamepad_buttons.iter() { + self.send_gamepad_button_state(gamepad, button, ButtonState::Pressed); + } + } + } + + fn send_axis_values(&mut self, input: impl UserInput, values: impl IntoIterator) { + self.send_axis_values_as_gamepad(input, values, self.guess_gamepad()) + } + + fn send_axis_values_as_gamepad( + &mut self, + input: impl UserInput, + values: impl IntoIterator, + gamepad: Option, + ) { + let raw_inputs = input.raw_inputs(); + let mut value_iter = values.into_iter(); + + if let Some(gamepad) = gamepad { + for axis in raw_inputs.gamepad_axes.iter() { + let value = value_iter.next().unwrap_or_default(); + self.send_gamepad_axis_value(gamepad, axis, value); } } + + for axis in raw_inputs.mouse_move_axes.iter() { + let value = value_iter.next().unwrap_or_default(); + let value = axis.dual_axis_value(value); + self.send_mouse_move(value); + } + + for axis in raw_inputs.mouse_scroll_axes.iter() { + let value = value_iter.next().unwrap_or_default(); + let value = axis.dual_axis_value(value); + self.send_mouse_scroll(value); + } } - fn release_input(&mut self, input: impl Into) { + fn release_input(&mut self, input: impl UserInput) { self.release_input_as_gamepad(input, self.guess_gamepad()) } - fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { - // Releasing axis-like inputs deliberately has no effect; it's unclear what this would do + fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { + let raw_inputs = input.raw_inputs(); - let input_to_release: UserInput = input.into(); - let raw_inputs = input_to_release.raw_inputs(); + // Release KeyCode + for keycode in raw_inputs.keycodes.iter() { + self.send_keycode_state(keycode, ButtonState::Released); + } + + // Release MouseButton + for button in raw_inputs.mouse_buttons.iter() { + self.send_mouse_button_state(button, ButtonState::Released); + } - self.send_gamepad_button_changed(gamepad, &raw_inputs); + // Release GamepadButtonType. + // Unfortunately, due to upstream constraints, this has no effect + // because Bevy currently disregards all external GamepadButtonChangedEvent events. + // See for more details. + if let Some(gamepad) = gamepad { + for button in raw_inputs.gamepad_buttons.iter() { + self.send_gamepad_button_state(gamepad, button, ButtonState::Released); + } + } - self.send_keyboard_input(ButtonState::Released, &raw_inputs); + // Deactivate GamepadAxisType + if let Some(gamepad) = gamepad { + for direction in raw_inputs.gamepad_control_directions.iter() { + self.send_gamepad_axis_value(gamepad, &direction.axis, 0.0); + } - for button in raw_inputs.mouse_buttons { - self.mouse_button_events.send(MouseButtonInput { - button, - state: ButtonState::Released, - window: Entity::PLACEHOLDER, - }); + for axis in raw_inputs.gamepad_axes.iter() { + self.send_gamepad_axis_value(gamepad, axis, 0.0); + } } + + // Mouse axial inputs don't require an explicit deactivating, + // as we directly check the state by reading the mouse input events. } fn reset_inputs(&mut self) { @@ -259,76 +348,109 @@ impl MockInput for MutableInputStreams<'_> { } impl MutableInputStreams<'_> { - fn send_keyboard_input(&mut self, button_state: ButtonState, raw_inputs: &RawInputs) { - for key_code in raw_inputs.keycodes.iter() { - self.keyboard_events.send(KeyboardInput { - logical_key: Key::Unidentified(NativeKey::Unidentified), - key_code: *key_code, - state: button_state, - window: Entity::PLACEHOLDER, - }); - } - } - - fn send_mouse_wheel(&mut self, x: f32, y: f32) { - // FIXME: MouseScrollUnit is not recorded and is always assumed to be Pixel - let unit = MouseScrollUnit::Pixel; - let window = Entity::PLACEHOLDER; - self.mouse_wheel.send(MouseWheel { unit, x, y, window }); + fn send_keycode_state(&mut self, keycode: &KeyCode, state: ButtonState) { + self.keyboard_events.send(KeyboardInput { + logical_key: Key::Unidentified(NativeKey::Unidentified), + key_code: *keycode, + state, + window: Entity::PLACEHOLDER, + }); + } + + fn send_mouse_button_state(&mut self, button: &MouseButton, state: ButtonState) { + self.mouse_button_events.send(MouseButtonInput { + button: *button, + state, + window: Entity::PLACEHOLDER, + }); + } + + fn send_mouse_scroll(&mut self, delta: Vec2) { + self.mouse_wheel.send(MouseWheel { + x: delta.x, + y: delta.y, + // FIXME: MouseScrollUnit is not recorded and is always assumed to be Pixel + unit: MouseScrollUnit::Pixel, + window: Entity::PLACEHOLDER, + }); + } + + fn send_mouse_move(&mut self, delta: Vec2) { + self.mouse_motion.send(MouseMotion { delta }); } - fn send_mouse_motion(&mut self, x: f32, y: f32) { - let delta = Vec2::new(x, y); - self.mouse_motion.send(MouseMotion { delta }); + fn send_gamepad_button_state( + &mut self, + gamepad: Gamepad, + button_type: &GamepadButtonType, + state: ButtonState, + ) { + let value = f32::from(state == ButtonState::Pressed); + let event = GamepadButtonChangedEvent::new(gamepad, *button_type, value); + self.gamepad_events.send(GamepadEvent::Button(event)); } - fn send_gamepad_button_changed(&mut self, gamepad: Option, raw_inputs: &RawInputs) { - if let Some(gamepad) = gamepad { - for button_type in raw_inputs.gamepad_buttons.iter() { - self.gamepad_events - .send(GamepadEvent::Button(GamepadButtonChangedEvent { - gamepad, - button_type: *button_type, - value: 1.0, - })); - } - } + fn send_gamepad_axis_value( + &mut self, + gamepad: Gamepad, + axis_type: &GamepadAxisType, + value: f32, + ) { + let event = GamepadAxisChangedEvent::new(gamepad, *axis_type, value); + self.gamepad_events.send(GamepadEvent::Axis(event)); } } impl QueryInput for InputStreams<'_> { - fn pressed(&self, input: impl Into) -> bool { - self.input_pressed(&input.into()) + fn pressed(&self, input: impl UserInput) -> bool { + input.pressed(self) } - fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { + fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { let mut input_streams = self.clone(); input_streams.associated_gamepad = gamepad; - input_streams.input_pressed(&input.into()) + input.pressed(&input_streams) } } impl MockInput for World { - fn send_input(&mut self, input: impl Into) { + fn press_input(&mut self, input: impl UserInput) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, None); + + mutable_input_streams.press_input(input); + } + + fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { + let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad); + + mutable_input_streams.press_input_as_gamepad(input, gamepad); + } + + fn send_axis_values(&mut self, input: impl UserInput, values: impl IntoIterator) { let mut mutable_input_streams = MutableInputStreams::from_world(self, None); - mutable_input_streams.send_input(input); + mutable_input_streams.send_axis_values(input, values); } - fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + fn send_axis_values_as_gamepad( + &mut self, + input: impl UserInput, + values: impl IntoIterator, + gamepad: Option, + ) { let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad); - mutable_input_streams.send_input_as_gamepad(input, gamepad); + mutable_input_streams.send_axis_values(input, values); } - fn release_input(&mut self, input: impl Into) { + fn release_input(&mut self, input: impl UserInput) { let mut mutable_input_streams = MutableInputStreams::from_world(self, None); mutable_input_streams.release_input(input); } - fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad); mutable_input_streams.release_input_as_gamepad(input, gamepad); @@ -380,14 +502,14 @@ impl MockInput for World { } impl QueryInput for World { - fn pressed(&self, input: impl Into) -> bool { - self.pressed_for_gamepad(input, None) + fn pressed(&self, input: impl UserInput) -> bool { + self.pressed_on_gamepad(input, None) } - fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { + fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { let input_streams = InputStreams::from_world(self, gamepad); - input_streams.input_pressed(&input.into()) + input.pressed(&input_streams) } } @@ -411,19 +533,33 @@ impl MockUIInteraction for World { } impl MockInput for App { - fn send_input(&mut self, input: impl Into) { - self.world.send_input(input); + fn press_input(&mut self, input: impl UserInput) { + self.world.press_input(input); + } + + fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { + self.world.press_input_as_gamepad(input, gamepad); + } + + fn send_axis_values(&mut self, input: impl UserInput, values: impl IntoIterator) { + self.world.send_axis_values(input, values); } - fn send_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { - self.world.send_input_as_gamepad(input, gamepad); + fn send_axis_values_as_gamepad( + &mut self, + input: impl UserInput, + values: impl IntoIterator, + gamepad: Option, + ) { + self.world + .send_axis_values_as_gamepad(input, values, gamepad); } - fn release_input(&mut self, input: impl Into) { + fn release_input(&mut self, input: impl UserInput) { self.world.release_input(input); } - fn release_input_as_gamepad(&mut self, input: impl Into, gamepad: Option) { + fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { self.world.release_input_as_gamepad(input, gamepad); } @@ -433,12 +569,12 @@ impl MockInput for App { } impl QueryInput for App { - fn pressed(&self, input: impl Into) -> bool { + fn pressed(&self, input: impl UserInput) -> bool { self.world.pressed(input) } - fn pressed_for_gamepad(&self, input: impl Into, gamepad: Option) -> bool { - self.world.pressed_for_gamepad(input, gamepad) + fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { + self.world.pressed_on_gamepad(input, gamepad) } } @@ -473,16 +609,16 @@ mod test { assert!(!app.pressed(KeyCode::Space)); assert!(!app.pressed(MouseButton::Right)); - // Send inputs - app.send_input(KeyCode::Space); - app.send_input(MouseButton::Right); + // Press buttons + app.press_input(KeyCode::Space); + app.press_input(MouseButton::Right); app.update(); // Verify that checking the resource value directly works - let keyboard_input: &ButtonInput = app.world.resource(); + let keyboard_input = app.world.resource::>(); assert!(keyboard_input.pressed(KeyCode::Space)); - // Test the convenient .pressed API + // Test the convenient `pressed` API assert!(app.pressed(KeyCode::Space)); assert!(app.pressed(MouseButton::Right)); @@ -510,28 +646,17 @@ mod test { app.update(); // Test that buttons are unpressed by default - assert!(!app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); + assert!(!app.pressed_on_gamepad(GamepadButtonType::North, Some(gamepad))); - // Send inputs - app.send_input_as_gamepad(GamepadButtonType::North, Some(gamepad)); + // Press buttons + app.press_input_as_gamepad(GamepadButtonType::North, Some(gamepad)); app.update(); - // Checking the old-fashioned way - // FIXME: put this in a gamepad_button.rs integration test. - let gamepad_input = app.world.resource::>(); - assert!(gamepad_input.pressed(GamepadButton { - gamepad, - button_type: GamepadButtonType::North, - })); - - // Test the convenient .pressed API - assert!(app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); - // Test that resetting inputs works app.reset_inputs(); app.update(); - assert!(!app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad))); + assert!(!app.pressed_on_gamepad(GamepadButtonType::North, Some(gamepad))); } #[test] @@ -552,11 +677,11 @@ mod test { // Test that buttons are unpressed by default assert!(!app.pressed(GamepadButtonType::North)); - // Send inputs - app.send_input(GamepadButtonType::North); + // Press buttons + app.press_input(GamepadButtonType::North); app.update(); - // Test the convenient .pressed API + // Test the convenient `pressed` API assert!(app.pressed(GamepadButtonType::North)); // Test that resetting inputs works @@ -581,6 +706,7 @@ mod test { // Marked button app.world.spawn((Interaction::None, ButtonMarker)); + // Unmarked button app.world.spawn(Interaction::None); diff --git a/src/input_processing/dual_axis/circle.rs b/src/input_processing/dual_axis/circle.rs index 3a59840d..d0f3eb45 100644 --- a/src/input_processing/dual_axis/circle.rs +++ b/src/input_processing/dual_axis/circle.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use std::hash::{Hash, Hasher}; -use bevy::prelude::*; +use bevy::prelude::{Reflect, Vec2}; use bevy::utils::FloatOrd; use serde::{Deserialize, Serialize}; @@ -399,6 +399,7 @@ impl Hash for CircleDeadZone { #[cfg(test)] mod tests { use super::*; + use bevy::prelude::FloatExt; #[test] fn test_circle_value_bounds() { diff --git a/src/input_processing/dual_axis/custom.rs b/src/input_processing/dual_axis/custom.rs index 1bd86c7e..93b96713 100644 --- a/src/input_processing/dual_axis/custom.rs +++ b/src/input_processing/dual_axis/custom.rs @@ -278,14 +278,14 @@ static mut PROCESSOR_REGISTRY: Lazy(&mut self) -> &mut Self where T: RegisterTypeTag<'de, dyn CustomDualAxisProcessor> + GetTypeRegistration; } -impl RegisterDualAxisProcessor for App { +impl RegisterDualAxisProcessorExt for App { fn register_dual_axis_processor<'de, T>(&mut self) -> &mut Self where T: RegisterTypeTag<'de, dyn CustomDualAxisProcessor> + GetTypeRegistration, diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index ff52a9d0..2bdd9603 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -1,10 +1,9 @@ //! Processors for dual-axis input values -use bevy::math::BVec2; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use bevy::prelude::{Reflect, Vec2}; +use bevy::prelude::{BVec2, Reflect, Vec2}; use bevy::utils::FloatOrd; use serde::{Deserialize, Serialize}; @@ -54,7 +53,7 @@ pub enum DualAxisProcessor { /// one for the current step and the other for the next step. /// /// For a straightforward creation of a [`DualAxisProcessor::Pipeline`], - /// you can use [`DualAxisProcessor::with_processor`] or [`From>::from`] methods. + /// you can use [`DualAxisProcessor::pipeline`] or [`DualAxisProcessor::with_processor`] methods. /// /// ```rust /// use std::sync::Arc; @@ -84,6 +83,12 @@ pub enum DualAxisProcessor { } impl DualAxisProcessor { + /// Creates a [`DualAxisProcessor::Pipeline`] from the given `processors`. + #[inline] + pub fn pipeline(processors: impl IntoIterator) -> Self { + Self::from_iter(processors) + } + /// Computes the result by processing the `input_value`. #[must_use] #[inline] @@ -142,6 +147,234 @@ impl FromIterator for DualAxisProcessor { } } +/// Provides methods for configuring and manipulating the processing pipeline for dual-axis input. +pub trait WithDualAxisProcessorExt: Sized { + /// Remove the current used [`DualAxisProcessor`]. + fn no_processor(self) -> Self; + + /// Replaces the current pipeline with the specified [`DualAxisProcessor`]. + fn replace_processor(self, processor: impl Into) -> Self; + + /// Appends the given [`DualAxisProcessor`] as the next processing step. + fn with_processor(self, processor: impl Into) -> Self; + + /// Appends a [`DualAxisInverted::ALL`] processor as the next processing step, + /// flipping the sign of values on both axes. + #[inline] + fn inverted(self) -> Self { + self.with_processor(DualAxisInverted::ALL) + } + + /// Appends a [`DualAxisInverted::ONLY_X`] processor as the next processing step, + /// only flipping the sign of the X-axis values. + #[inline] + fn inverted_x(self) -> Self { + self.with_processor(DualAxisInverted::ONLY_X) + } + + /// Appends a [`DualAxisInverted::ONLY_Y`] processor as the next processing step, + /// only flipping the sign of the Y-axis values. + #[inline] + fn inverted_y(self) -> Self { + self.with_processor(DualAxisInverted::ONLY_Y) + } + + /// Appends a [`DualAxisSensitivity`] processor as the next processing step, + /// multiplying values on both axes with the given sensitivity factor. + #[inline] + fn sensitivity(self, sensitivity: f32) -> Self { + self.with_processor(DualAxisSensitivity::all(sensitivity)) + } + + /// Appends a [`DualAxisSensitivity`] processor as the next processing step, + /// only multiplying the X-axis values with the given sensitivity factor. + #[inline] + fn sensitivity_x(self, sensitivity: f32) -> Self { + self.with_processor(DualAxisSensitivity::only_x(sensitivity)) + } + + /// Appends a [`DualAxisSensitivity`] processor as the next processing step, + /// only multiplying the Y-axis values with the given sensitivity factor. + #[inline] + fn sensitivity_y(self, sensitivity: f32) -> Self { + self.with_processor(DualAxisSensitivity::only_y(sensitivity)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// restricting values within the same range `[min, max]` on both axes. + #[inline] + fn with_bounds(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisBounds::all(min, max)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// restricting values within the same range `[-threshold, threshold]` on both axes. + #[inline] + fn with_bounds_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisBounds::symmetric_all(threshold)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// only restricting values within the range `[min, max]` on the X-axis. + #[inline] + fn with_bounds_x(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisBounds::only_x(min, max)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// restricting values within the range `[-threshold, threshold]` on the X-axis. + #[inline] + fn with_bounds_x_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisBounds::symmetric_all(threshold)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// only restricting values within the range `[min, max]` on the Y-axis. + #[inline] + fn with_bounds_y(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisBounds::only_y(min, max)) + } + + /// Appends a [`DualAxisBounds`] processor as the next processing step, + /// restricting values within the range `[-threshold, threshold]` on the Y-axis. + #[inline] + fn with_bounds_y_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisBounds::symmetric_all(threshold)) + } + + /// Appends a [`CircleBounds`] processor as the next processing step, + /// restricting values to a `max` magnitude. + #[inline] + fn with_circle_bounds(self, max: f32) -> Self { + self.with_processor(CircleBounds::new(max)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[min, max]` on both axes, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisDeadZone::all(min, max)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[-threshold, threshold]` on both axes, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisDeadZone::symmetric_all(threshold)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[min, max]` on the X-axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_x(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisDeadZone::only_x(min, max)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[-threshold, threshold]` on the X-axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_x_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisDeadZone::symmetric_only_x(threshold)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[min, max]` on the Y-axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_y(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisDeadZone::only_y(min, max)) + } + + /// Appends a [`DualAxisDeadZone`] processor as the next processing step, + /// excluding values within the deadzone range `[-threshold, threshold]` on the Y-axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_y_symmetric(self, threshold: f32) -> Self { + self.with_processor(DualAxisDeadZone::symmetric_only_y(threshold)) + } + + /// Appends a [`CircleDeadZone`] processor as the next processing step, + /// ignoring values below a `min` magnitude, treating them as zeros, + /// then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`CircleBounds::new(1.0)`](CircleBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_circle_deadzone(self, min: f32) -> Self { + self.with_processor(CircleDeadZone::new(min)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// ignoring values within the dead zone range `[min, max]` on both axes, + /// treating them as zeros. + #[inline] + fn with_deadzone_unscaled(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisExclusion::all(min, max)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// ignoring values within the dead zone range `[-threshold, threshold]` on both axes, + /// treating them as zeros. + #[inline] + fn with_deadzone_symmetric_unscaled(self, threshold: f32) -> Self { + self.with_processor(DualAxisExclusion::symmetric_all(threshold)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// only ignoring values within the dead zone range `[min, max]` on the X-axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_x_unscaled(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisExclusion::only_x(min, max)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// only ignoring values within the dead zone range `[-threshold, threshold]` on the X-axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_x_symmetric_unscaled(self, threshold: f32) -> Self { + self.with_processor(DualAxisExclusion::symmetric_only_x(threshold)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// only ignoring values within the dead zone range `[min, max]` on the Y-axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_y_unscaled(self, min: f32, max: f32) -> Self { + self.with_processor(DualAxisExclusion::only_y(min, max)) + } + + /// Appends a [`DualAxisExclusion`] processor as the next processing step, + /// only ignoring values within the dead zone range `[-threshold, threshold]` on the Y-axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_y_symmetric_unscaled(self, threshold: f32) -> Self { + self.with_processor(DualAxisExclusion::symmetric_only_y(threshold)) + } + + /// Appends a [`CircleExclusion`] processor as the next processing step, + /// ignoring values below a `min` magnitude, treating them as zeros. + #[inline] + fn with_circle_deadzone_unscaled(self, min: f32) -> Self { + self.with_processor(CircleExclusion::new(min)) + } +} + /// Flips the sign of dual-axis input values, resulting in a directional reversal of control. /// /// ```rust @@ -295,7 +528,7 @@ mod tests { use super::*; #[test] - fn test_axis_processor_pipeline() { + fn test_dual_axis_processing_pipeline() { let pipeline = DualAxisProcessor::Pipeline(vec![ Arc::new(DualAxisInverted::ALL.into()), Arc::new(DualAxisSensitivity::all(2.0).into()), @@ -313,17 +546,37 @@ mod tests { } #[test] - fn test_dual_axis_processor_from_list() { + fn test_dual_axis_processing_pipeline_creation() { + assert_eq!( + DualAxisProcessor::pipeline([]), + DualAxisProcessor::Pipeline(vec![]) + ); assert_eq!( DualAxisProcessor::from_iter([]), DualAxisProcessor::Pipeline(vec![]) ); + assert_eq!( + DualAxisProcessor::pipeline([DualAxisInverted::ALL.into()]), + DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) + ); + assert_eq!( DualAxisProcessor::from_iter([DualAxisInverted::ALL.into()]), DualAxisProcessor::Pipeline(vec![Arc::new(DualAxisInverted::ALL.into())]) ); + assert_eq!( + DualAxisProcessor::pipeline([ + DualAxisInverted::ALL.into(), + DualAxisSensitivity::all(2.0).into(), + ]), + DualAxisProcessor::Pipeline(vec![ + Arc::new(DualAxisInverted::ALL.into()), + Arc::new(DualAxisSensitivity::all(2.0).into()), + ]) + ); + assert_eq!( DualAxisProcessor::from_iter([ DualAxisInverted::ALL.into(), diff --git a/src/input_processing/dual_axis/range.rs b/src/input_processing/dual_axis/range.rs index 4e0f33cb..30285a40 100644 --- a/src/input_processing/dual_axis/range.rs +++ b/src/input_processing/dual_axis/range.rs @@ -131,12 +131,12 @@ impl DualAxisBounds { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self { Self { - bounds_x: AxisBounds::magnitude(threshold_x), - bounds_y: AxisBounds::magnitude(threshold_y), + bounds_x: AxisBounds::symmetric(threshold_x), + bounds_y: AxisBounds::symmetric(threshold_y), } } @@ -149,10 +149,10 @@ impl DualAxisBounds { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_all")] + #[doc(alias = "magnitude_all")] #[inline] - pub fn magnitude_all(threshold: f32) -> Self { - Self::magnitude(threshold, threshold) + pub fn symmetric_all(threshold: f32) -> Self { + Self::symmetric(threshold, threshold) } /// Creates a [`DualAxisBounds`] that only restricts X values within the range `[-threshold, threshold]`. @@ -164,11 +164,11 @@ impl DualAxisBounds { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_x")] + #[doc(alias = "magnitude_only_x")] #[inline] - pub fn magnitude_only_x(threshold: f32) -> Self { + pub fn symmetric_only_x(threshold: f32) -> Self { Self { - bounds_x: AxisBounds::magnitude(threshold), + bounds_x: AxisBounds::symmetric(threshold), ..Self::FULL_RANGE } } @@ -182,11 +182,11 @@ impl DualAxisBounds { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_y")] + #[doc(alias = "magnitude_only_y")] #[inline] - pub fn magnitude_only_y(threshold: f32) -> Self { + pub fn symmetric_only_y(threshold: f32) -> Self { Self { - bounds_y: AxisBounds::magnitude(threshold), + bounds_y: AxisBounds::symmetric(threshold), ..Self::FULL_RANGE } } @@ -430,7 +430,7 @@ impl DualAxisExclusion { } } - /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on both axis. + /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on both axes. /// /// # Requirements /// @@ -488,12 +488,12 @@ impl DualAxisExclusion { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self { Self { - exclusion_x: AxisExclusion::magnitude(threshold_x), - exclusion_y: AxisExclusion::magnitude(threshold_y), + exclusion_x: AxisExclusion::symmetric(threshold_x), + exclusion_y: AxisExclusion::symmetric(threshold_y), } } @@ -506,10 +506,10 @@ impl DualAxisExclusion { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_all")] + #[doc(alias = "magnitude_all")] #[inline] - pub fn magnitude_all(threshold: f32) -> Self { - Self::magnitude(threshold, threshold) + pub fn symmetric_all(threshold: f32) -> Self { + Self::symmetric(threshold, threshold) } /// Creates a [`DualAxisExclusion`] that only ignores X values within the range `[-threshold, threshold]`. @@ -521,11 +521,11 @@ impl DualAxisExclusion { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_x")] + #[doc(alias = "magnitude_only_x")] #[inline] - pub fn magnitude_only_x(threshold: f32) -> Self { + pub fn symmetric_only_x(threshold: f32) -> Self { Self { - exclusion_x: AxisExclusion::magnitude(threshold), + exclusion_x: AxisExclusion::symmetric(threshold), ..Self::ZERO } } @@ -539,11 +539,11 @@ impl DualAxisExclusion { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_y")] + #[doc(alias = "magnitude_only_y")] #[inline] - pub fn magnitude_only_y(threshold: f32) -> Self { + pub fn symmetric_only_y(threshold: f32) -> Self { Self { - exclusion_y: AxisExclusion::magnitude(threshold), + exclusion_y: AxisExclusion::symmetric(threshold), ..Self::ZERO } } @@ -660,7 +660,7 @@ impl From for DualAxisProcessor { } /// A scaled version of [`DualAxisExclusion`] with the bounds -/// set to [`DualAxisBounds::magnitude_all(1.0)`](DualAxisBounds::default) +/// set to [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default) /// that normalizes non-excluded input values into the "live zone", /// the remaining range within the bounds after dead zone exclusion. /// @@ -791,12 +791,12 @@ impl DualAxisDeadZone { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold_x: f32, threshold_y: f32) -> Self { + pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self { Self { - deadzone_x: AxisDeadZone::magnitude(threshold_x), - deadzone_y: AxisDeadZone::magnitude(threshold_y), + deadzone_x: AxisDeadZone::symmetric(threshold_x), + deadzone_y: AxisDeadZone::symmetric(threshold_y), } } @@ -809,10 +809,10 @@ impl DualAxisDeadZone { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_all")] + #[doc(alias = "magnitude_all")] #[inline] - pub fn magnitude_all(threshold: f32) -> Self { - Self::magnitude(threshold, threshold) + pub fn symmetric_all(threshold: f32) -> Self { + Self::symmetric(threshold, threshold) } /// Creates a [`DualAxisDeadZone`] that only excludes X values within the range `[-threshold, threshold]`. @@ -824,11 +824,11 @@ impl DualAxisDeadZone { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_x")] + #[doc(alias = "magnitude_only_x")] #[inline] - pub fn magnitude_only_x(threshold: f32) -> Self { + pub fn symmetric_only_x(threshold: f32) -> Self { Self { - deadzone_x: AxisDeadZone::magnitude(threshold), + deadzone_x: AxisDeadZone::symmetric(threshold), ..Self::ZERO } } @@ -842,11 +842,11 @@ impl DualAxisDeadZone { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric_only_y")] + #[doc(alias = "magnitude_only_y")] #[inline] - pub fn magnitude_only_y(threshold: f32) -> Self { + pub fn symmetric_only_y(threshold: f32) -> Self { Self { - deadzone_y: AxisDeadZone::magnitude(threshold), + deadzone_y: AxisDeadZone::symmetric(threshold), ..Self::ZERO } } @@ -1078,16 +1078,16 @@ mod tests { let bounds = DualAxisBounds::only_y(-1.0, 1.5); test_bounds(bounds, full_range, (-1.0, 1.5)); - let bounds = DualAxisBounds::magnitude(2.0, 2.5); + let bounds = DualAxisBounds::symmetric(2.0, 2.5); test_bounds(bounds, (-2.0, 2.0), (-2.5, 2.5)); - let bounds = DualAxisBounds::magnitude_all(2.5); + let bounds = DualAxisBounds::symmetric_all(2.5); test_bounds(bounds, (-2.5, 2.5), (-2.5, 2.5)); - let bounds = DualAxisBounds::magnitude_only_x(2.5); + let bounds = DualAxisBounds::symmetric_only_x(2.5); test_bounds(bounds, (-2.5, 2.5), full_range); - let bounds = DualAxisBounds::magnitude_only_y(2.5); + let bounds = DualAxisBounds::symmetric_only_y(2.5); test_bounds(bounds, full_range, (-2.5, 2.5)); let bounds = DualAxisBounds::at_least(2.0, 2.5); @@ -1214,16 +1214,16 @@ mod tests { let exclusion = DualAxisExclusion::only_y(-0.1, 0.4); test_exclusion(exclusion, zero_size, (-0.1, 0.4)); - let exclusion = DualAxisExclusion::magnitude(0.2, 0.3); + let exclusion = DualAxisExclusion::symmetric(0.2, 0.3); test_exclusion(exclusion, (-0.2, 0.2), (-0.3, 0.3)); - let exclusion = DualAxisExclusion::magnitude_all(0.3); + let exclusion = DualAxisExclusion::symmetric_all(0.3); test_exclusion(exclusion, (-0.3, 0.3), (-0.3, 0.3)); - let exclusion = DualAxisExclusion::magnitude_only_x(0.3); + let exclusion = DualAxisExclusion::symmetric_only_x(0.3); test_exclusion(exclusion, (-0.3, 0.3), zero_size); - let exclusion = DualAxisExclusion::magnitude_only_y(0.3); + let exclusion = DualAxisExclusion::symmetric_only_y(0.3); test_exclusion(exclusion, zero_size, (-0.3, 0.3)); let exclusion_x = AxisExclusion::new(-0.2, 0.3); @@ -1353,16 +1353,16 @@ mod tests { let deadzone = DualAxisDeadZone::only_y(-0.1, 0.4); test_deadzone(deadzone, zero_size, (-0.1, 0.4)); - let deadzone = DualAxisDeadZone::magnitude(0.2, 0.3); + let deadzone = DualAxisDeadZone::symmetric(0.2, 0.3); test_deadzone(deadzone, (-0.2, 0.2), (-0.3, 0.3)); - let deadzone = DualAxisDeadZone::magnitude_all(0.3); + let deadzone = DualAxisDeadZone::symmetric_all(0.3); test_deadzone(deadzone, (-0.3, 0.3), (-0.3, 0.3)); - let deadzone = DualAxisDeadZone::magnitude_only_x(0.3); + let deadzone = DualAxisDeadZone::symmetric_only_x(0.3); test_deadzone(deadzone, (-0.3, 0.3), zero_size); - let deadzone = DualAxisDeadZone::magnitude_only_y(0.3); + let deadzone = DualAxisDeadZone::symmetric_only_y(0.3); test_deadzone(deadzone, zero_size, (-0.3, 0.3)); let deadzone_x = AxisDeadZone::new(-0.2, 0.3); diff --git a/src/input_processing/mod.rs b/src/input_processing/mod.rs index 6c4e666a..c04bff79 100644 --- a/src/input_processing/mod.rs +++ b/src/input_processing/mod.rs @@ -26,8 +26,8 @@ //! //! You can also use these methods to create a pipeline. //! -//! - [`AxisProcessor::with_processor`] or [`From>::from`] for [`AxisProcessor::Pipeline`]. -//! - [`DualAxisProcessor::with_processor`] or [`From>::from`] for [`DualAxisProcessor::Pipeline`]. +//! - [`AxisProcessor::pipeline`] or [`AxisProcessor::with_processor`] for [`AxisProcessor::Pipeline`]. +//! - [`DualAxisProcessor::pipeline`] or [`DualAxisProcessor::with_processor`] for [`DualAxisProcessor::Pipeline`]. //! //! ## Inversion //! @@ -80,10 +80,10 @@ //! the remaining region within the bounds after dead zone exclusion. //! //! - [`AxisDeadZone`]: A scaled version of [`AxisExclusion`] with the bounds -//! set to [`AxisBounds::magnitude(1.0)`](AxisBounds::default), +//! set to [`AxisBounds::symmetric(1.0)`](AxisBounds::default), //! implemented [`Into`] and [`Into`]. //! - [`DualAxisDeadZone`]: A scaled version of [`DualAxisExclusion`] with the bounds -//! set to [`DualAxisBounds::magnitude_all(1.0)`](DualAxisBounds::default), implemented [`Into`]. +//! set to [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default), implemented [`Into`]. //! - [`CircleDeadZone`]: A scaled version of [`CircleExclusion`] with the bounds //! set to [`CircleBounds::new(1.0)`](CircleBounds::default), implemented [`Into`]. diff --git a/src/input_processing/single_axis/custom.rs b/src/input_processing/single_axis/custom.rs index d7c76197..868e5007 100644 --- a/src/input_processing/single_axis/custom.rs +++ b/src/input_processing/single_axis/custom.rs @@ -277,14 +277,14 @@ static mut PROCESSOR_REGISTRY: Lazy> Lazy::new(|| RwLock::new(MapRegistry::new("CustomAxisProcessor"))); /// A trait for registering a specific [`CustomAxisProcessor`]. -pub trait RegisterCustomAxisProcessor { +pub trait RegisterCustomAxisProcessorExt { /// Registers the specified [`CustomAxisProcessor`]. fn register_axis_processor<'de, T>(&mut self) -> &mut Self where T: RegisterTypeTag<'de, dyn CustomAxisProcessor> + GetTypeRegistration; } -impl RegisterCustomAxisProcessor for App { +impl RegisterCustomAxisProcessorExt for App { fn register_axis_processor<'de, T>(&mut self) -> &mut Self where T: RegisterTypeTag<'de, dyn CustomAxisProcessor> + GetTypeRegistration, diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs index eb4adbab..4d1ad4bd 100644 --- a/src/input_processing/single_axis/mod.rs +++ b/src/input_processing/single_axis/mod.rs @@ -61,7 +61,7 @@ pub enum AxisProcessor { /// Processes input values sequentially through a sequence of [`AxisProcessor`]s. /// /// For a straightforward creation of a [`AxisProcessor::Pipeline`], - /// you can use [`AxisProcessor::with_processor`] or [`From>::from`] methods. + /// you can use [`AxisProcessor::pipeline`] or [`AxisProcessor::with_processor`] methods. /// /// ```rust /// use std::sync::Arc; @@ -92,6 +92,12 @@ pub enum AxisProcessor { } impl AxisProcessor { + /// Creates an [`AxisProcessor::Pipeline`] from the given `processors`. + #[inline] + pub fn pipeline(processors: impl IntoIterator) -> Self { + Self::from_iter(processors) + } + /// Computes the result by processing the `input_value`. #[must_use] #[inline] @@ -165,6 +171,82 @@ impl Hash for AxisProcessor { } } +/// Provides methods for configuring and manipulating the processing pipeline for single-axis input. +pub trait WithAxisProcessorExt: Sized { + /// Remove the current used [`AxisProcessor`]. + fn no_processor(self) -> Self; + + /// Replaces the current pipeline with the specified [`AxisProcessor`]. + fn replace_processor(self, processor: impl Into) -> Self; + + /// Appends the given [`AxisProcessor`] as the next processing step. + fn with_processor(self, processor: impl Into) -> Self; + + /// Appends an [`AxisProcessor::Inverted`] processor as the next processing step, + /// flipping the sign of values on the axis. + #[inline] + fn inverted(self) -> Self { + self.with_processor(AxisProcessor::Inverted) + } + + /// Appends an [`AxisProcessor::Sensitivity`] processor as the next processing step, + /// multiplying values on the axis with the given sensitivity factor. + #[inline] + fn sensitivity(self, sensitivity: f32) -> Self { + self.with_processor(AxisProcessor::Sensitivity(sensitivity)) + } + + /// Appends an [`AxisBounds`] processor as the next processing step, + /// restricting values within the range `[min, max]` on the axis. + #[inline] + fn with_bounds(self, min: f32, max: f32) -> Self { + self.with_processor(AxisBounds::new(min, max)) + } + + /// Appends an [`AxisBounds`] processor as the next processing step, + /// restricting values to a `threshold` magnitude. + #[inline] + fn with_bounds_symmetric(self, threshold: f32) -> Self { + self.with_processor(AxisBounds::symmetric(threshold)) + } + + /// Appends an [`AxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[negative_max, positive_min]` on the axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`AxisBounds::magnitude(1.0)`](AxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone(self, negative_max: f32, positive_min: f32) -> Self { + self.with_processor(AxisDeadZone::new(negative_max, positive_min)) + } + + /// Appends an [`AxisDeadZone`] processor as the next processing step, + /// excluding values within the dead zone range `[-threshold, threshold]` on the axis, + /// treating them as zeros, then normalizing non-excluded input values into the "live zone", + /// the remaining range within the [`AxisBounds::magnitude(1.0)`](AxisBounds::default) + /// after dead zone exclusion. + #[inline] + fn with_deadzone_symmetric(self, threshold: f32) -> Self { + self.with_processor(AxisDeadZone::symmetric(threshold)) + } + + /// Appends an [`AxisExclusion`] processor as the next processing step, + /// ignoring values within the dead zone range `[negative_max, positive_min]` on the axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_unscaled(self, negative_max: f32, positive_min: f32) -> Self { + self.with_processor(AxisExclusion::new(negative_max, positive_min)) + } + + /// Appends an [`AxisExclusion`] processor as the next processing step, + /// ignoring values within the dead zone range `[-threshold, threshold]` on the axis, + /// treating them as zeros. + #[inline] + fn with_deadzone_symmetric_unscaled(self, threshold: f32) -> Self { + self.with_processor(AxisExclusion::symmetric(threshold)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -194,7 +276,7 @@ mod tests { } #[test] - fn test_axis_processor_pipeline() { + fn test_axis_processing_pipeline() { let pipeline = AxisProcessor::Pipeline(vec![ Arc::new(AxisProcessor::Inverted), Arc::new(AxisProcessor::Sensitivity(2.0)), @@ -208,19 +290,34 @@ mod tests { } #[test] - fn test_axis_processor_from_list() { + fn test_axis_processing_pipeline_creation() { + assert_eq!(AxisProcessor::pipeline([]), AxisProcessor::Pipeline(vec![])); + assert_eq!( AxisProcessor::from_iter([]), AxisProcessor::Pipeline(vec![]) ); + assert_eq!( + AxisProcessor::pipeline([AxisProcessor::Inverted]), + AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), + ); + assert_eq!( AxisProcessor::from_iter([AxisProcessor::Inverted]), AxisProcessor::Pipeline(vec![Arc::new(AxisProcessor::Inverted)]), ); assert_eq!( - AxisProcessor::from_iter([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0),]), + AxisProcessor::pipeline([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0)]), + AxisProcessor::Pipeline(vec![ + Arc::new(AxisProcessor::Inverted), + Arc::new(AxisProcessor::Sensitivity(2.0)), + ]) + ); + + assert_eq!( + AxisProcessor::from_iter([AxisProcessor::Inverted, AxisProcessor::Sensitivity(2.0)]), AxisProcessor::Pipeline(vec![ Arc::new(AxisProcessor::Inverted), Arc::new(AxisProcessor::Sensitivity(2.0)), diff --git a/src/input_processing/single_axis/range.rs b/src/input_processing/single_axis/range.rs index ce686a43..14e9d0ae 100644 --- a/src/input_processing/single_axis/range.rs +++ b/src/input_processing/single_axis/range.rs @@ -69,9 +69,9 @@ impl AxisBounds { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold: f32) -> Self { + pub fn symmetric(threshold: f32) -> Self { Self::new(-threshold, threshold) } @@ -231,9 +231,9 @@ impl AxisExclusion { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold: f32) -> Self { + pub fn symmetric(threshold: f32) -> Self { Self::new(-threshold, threshold) } @@ -418,9 +418,9 @@ impl AxisDeadZone { /// # Panics /// /// Panics if the requirements aren't met. - #[doc(alias = "symmetric")] + #[doc(alias = "magnitude")] #[inline] - pub fn magnitude(threshold: f32) -> Self { + pub fn symmetric(threshold: f32) -> Self { AxisDeadZone::new(-threshold, threshold) } @@ -565,7 +565,7 @@ mod tests { let bounds = AxisBounds::new(-2.0, 2.5); test_bounds(bounds, -2.0, 2.5); - let bounds = AxisBounds::magnitude(2.0); + let bounds = AxisBounds::symmetric(2.0); test_bounds(bounds, -2.0, 2.0); let bounds = AxisBounds::at_least(-1.0); @@ -609,7 +609,7 @@ mod tests { let exclusion = AxisExclusion::new(-2.0, 2.5); test_exclusion(exclusion, -2.0, 2.5); - let exclusion = AxisExclusion::magnitude(1.5); + let exclusion = AxisExclusion::symmetric(1.5); test_exclusion(exclusion, -1.5, 1.5); } @@ -673,7 +673,7 @@ mod tests { let deadzone = AxisDeadZone::new(-0.2, 0.3); test_deadzone(deadzone, -0.2, 0.3); - let deadzone = AxisDeadZone::magnitude(0.4); + let deadzone = AxisDeadZone::symmetric(0.4); test_deadzone(deadzone, -0.4, 0.4); } } diff --git a/src/input_streams.rs b/src/input_streams.rs index 1cff7797..67548589 100644 --- a/src/input_streams.rs +++ b/src/input_streams.rs @@ -8,14 +8,6 @@ use bevy::input::{ mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}, Axis, ButtonInput, }; -use bevy::math::Vec2; -use bevy::utils::HashSet; - -use crate::axislike::{AxisType, MouseMotionAxisType, MouseWheelAxisType, SingleAxis}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::prelude::DualAxis; -use crate::user_input::{InputKind, UserInput}; -use crate::user_inputs::axislike::DualAxisData; /// A collection of [`ButtonInput`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap). /// @@ -72,275 +64,6 @@ impl<'a> InputStreams<'a> { } } -// Input checking -impl<'a> InputStreams<'a> { - /// Is the `input` matched by the [`InputStreams`]? - pub fn input_pressed(&self, input: &UserInput) -> bool { - match input { - UserInput::Chord(buttons) => self.all_buttons_pressed(buttons), - _ => input.iter().any(|button| self.button_pressed(&button)), - } - } - - /// Is at least one of the `inputs` pressed? - #[must_use] - pub fn any_pressed(&self, inputs: &HashSet) -> bool { - inputs.iter().any(|input| self.input_pressed(input)) - } - - /// Is the `button` pressed? - #[must_use] - pub fn button_pressed(&self, button: &InputKind) -> bool { - match button { - InputKind::DualAxis(axis) => self - .extract_dual_axis_data(axis) - .is_some_and(|data| data.xy() != Vec2::ZERO), - InputKind::SingleAxis(_) => { - let input: UserInput = button.clone().into(); - self.input_value(&input) != 0.0 - } - InputKind::GamepadButton(button_type) => { - let button_pressed = |gamepad: Gamepad| -> bool { - self.gamepad_buttons.pressed(GamepadButton { - gamepad, - button_type: *button_type, - }) - }; - if let Some(gamepad) = self.associated_gamepad { - button_pressed(gamepad) - } else { - self.gamepads.iter().any(button_pressed) - } - } - InputKind::PhysicalKey(keycode) => { - matches!(self.keycodes, Some(keycodes) if keycodes.pressed(*keycode)) - } - InputKind::Modifier(modifier) => { - let key_codes = modifier.key_codes(); - // Short-circuiting is probably not worth the branch here - matches!(self.keycodes, Some(keycodes) if keycodes.pressed(key_codes[0]) | keycodes.pressed(key_codes[1])) - } - InputKind::Mouse(mouse_button) => { - matches!(self.mouse_buttons, Some(mouse_buttons) if mouse_buttons.pressed(*mouse_button)) - } - InputKind::MouseWheel(mouse_wheel_direction) => { - let Some(mouse_wheel) = &self.mouse_wheel else { - return false; - }; - - // The compiler will compile this into a direct f64 accumulation when opt-level >= 1. - // - // PERF: this summing is computed for every individual input - // This should probably be computed once, and then cached / read - // Fix upstream! - let Vec2 { x, y } = mouse_wheel - .iter() - .map(|wheel| Vec2::new(wheel.x, wheel.y)) - .sum(); - match mouse_wheel_direction { - MouseWheelDirection::Up => y > 0.0, - MouseWheelDirection::Down => y < 0.0, - MouseWheelDirection::Left => x < 0.0, - MouseWheelDirection::Right => x > 0.0, - } - } - InputKind::MouseMotion(mouse_motion_direction) => { - // The compiler will compile this into a direct f64 accumulation when opt-level >= 1. - let Vec2 { x, y } = self.mouse_motion.iter().map(|motion| motion.delta).sum(); - match mouse_motion_direction { - MouseMotionDirection::Up => y > 0.0, - MouseMotionDirection::Down => y < 0.0, - MouseMotionDirection::Left => x < 0.0, - MouseMotionDirection::Right => x > 0.0, - } - } - } - } - - /// Are all the `buttons` pressed? - #[must_use] - pub fn all_buttons_pressed(&self, buttons: &[InputKind]) -> bool { - buttons.iter().all(|button| self.button_pressed(button)) - } - - /// Get the "value" of the input. - /// - /// For binary inputs such as buttons, this will always be either `0.0` or `1.0`. For analog - /// inputs such as axes, this will be the axis value. - /// - /// [`UserInput::Chord`] inputs are also considered binary and will return `0.0` or `1.0` based - /// on whether the chord has been pressed. - /// - /// # Warning - /// - /// If you need to ensure that this value is always in the range `[-1., 1.]`, - /// be sure to clamp the returned data. - pub fn input_value(&self, input: &UserInput) -> f32 { - let use_button_value = || -> f32 { f32::from(self.input_pressed(input)) }; - - match input { - UserInput::Single(InputKind::SingleAxis(single_axis)) => { - match single_axis.axis_type { - AxisType::Gamepad(axis_type) => { - let get_gamepad_value = |gamepad: Gamepad| -> f32 { - self.gamepad_axes - .get(GamepadAxis { gamepad, axis_type }) - .unwrap_or_default() - }; - if let Some(gamepad) = self.associated_gamepad { - let value = get_gamepad_value(gamepad); - single_axis.input_value(value) - } else { - self.gamepads - .iter() - .map(get_gamepad_value) - .find(|value| *value != 0.0) - .map_or(0.0, |value| single_axis.input_value(value)) - } - } - AxisType::MouseWheel(axis_type) => { - let Some(mouse_wheel) = &self.mouse_wheel else { - return 0.0; - }; - - // The compiler will compile this into a direct f64 accumulation when opt-level >= 1. - let Vec2 { x, y } = mouse_wheel - .iter() - .map(|wheel| Vec2::new(wheel.x, wheel.y)) - .sum(); - let movement = match axis_type { - MouseWheelAxisType::X => x, - MouseWheelAxisType::Y => y, - }; - single_axis.input_value(movement) - } - AxisType::MouseMotion(axis_type) => { - // The compiler will compile this into a direct f64 accumulation when opt-level >= 1. - let Vec2 { x, y } = self.mouse_motion.iter().map(|e| e.delta).sum(); - let movement = match axis_type { - MouseMotionAxisType::X => x, - MouseMotionAxisType::Y => y, - }; - single_axis.input_value(movement) - } - } - } - UserInput::VirtualAxis(axis) => { - let data = self.extract_single_axis_data(&axis.positive, &axis.negative); - axis.input_value(data) - } - UserInput::Single(InputKind::DualAxis(_)) => { - self.input_axis_pair(input).unwrap_or_default().length() - } - UserInput::VirtualDPad { .. } => { - self.input_axis_pair(input).unwrap_or_default().length() - } - UserInput::Chord(inputs) => { - let mut value = 0.0; - let mut has_axis = false; - - // Prioritize axis over button input values - for input in inputs.iter() { - value += match input { - InputKind::SingleAxis(axis) => { - has_axis = true; - self.input_value(&InputKind::SingleAxis(axis.clone()).into()) - } - InputKind::MouseWheel(axis) => { - has_axis = true; - self.input_value(&InputKind::MouseWheel(*axis).into()) - } - InputKind::MouseMotion(axis) => { - has_axis = true; - self.input_value(&InputKind::MouseMotion(*axis).into()) - } - _ => 0.0, - } - } - - if has_axis { - return value; - } - - use_button_value() - } - // This is required because upstream bevy::input still waffles about whether triggers are buttons or axes - UserInput::Single(InputKind::GamepadButton(button_type)) => { - let get_gamepad_value = |gamepad: Gamepad| -> f32 { - self.gamepad_button_axes - .get(GamepadButton { - gamepad, - button_type: *button_type, - }) - .unwrap_or_else(use_button_value) - }; - if let Some(gamepad) = self.associated_gamepad { - get_gamepad_value(gamepad) - } else { - self.gamepads - .iter() - .map(get_gamepad_value) - .find(|value| *value != 0.0) - .unwrap_or_default() - } - } - _ => use_button_value(), - } - } - - /// Get the axis pair associated to the user input. - /// - /// If `input` is a chord, returns result of the first dual axis in the chord. - /// If `input` is not a [`DualAxis`] or [`VirtualDPad`](crate::axislike::VirtualDPad), returns [`None`]. - /// - /// # Warning - /// - /// If you need to ensure that this value is always in the range `[-1., 1.]`, - /// be sure to clamp the returned data. - pub fn input_axis_pair(&self, input: &UserInput) -> Option { - match input { - UserInput::Chord(inputs) => { - if self.all_buttons_pressed(inputs) { - for input_kind in inputs.iter() { - // Return result of the first dual axis in the chord. - if let InputKind::DualAxis(dual_axis) = input_kind { - let data = self.extract_dual_axis_data(dual_axis); - return Some(data.unwrap_or_default()); - } - } - } - None - } - UserInput::Single(InputKind::DualAxis(dual_axis)) => { - Some(self.extract_dual_axis_data(dual_axis).unwrap_or_default()) - } - UserInput::VirtualDPad(dpad) => { - let x = self.extract_single_axis_data(&dpad.right, &dpad.left); - let y = self.extract_single_axis_data(&dpad.up, &dpad.down); - - let data = dpad.input_value(Vec2::new(x, y)); - Some(DualAxisData::from_xy(data)) - } - _ => None, - } - } - - fn extract_single_axis_data(&self, positive: &InputKind, negative: &InputKind) -> f32 { - let positive = self.input_value(&UserInput::Single(positive.clone())); - let negative = self.input_value(&UserInput::Single(negative.clone())); - - positive.abs() - negative.abs() - } - - fn extract_dual_axis_data(&self, dual_axis: &DualAxis) -> Option { - let x = self.input_value(&SingleAxis::new(dual_axis.x_axis_type).into()); - let y = self.input_value(&SingleAxis::new(dual_axis.y_axis_type).into()); - - let data = dual_axis.input_value(Vec2::new(x, y)); - Some(DualAxisData::from_xy(data)) - } -} - // Clones and collects the received events into a `Vec`. #[inline] fn collect_events_cloned(events: &Events) -> Vec { @@ -470,39 +193,3 @@ impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> { } } } - -#[cfg(test)] -mod tests { - use super::{InputStreams, MutableInputStreams}; - use crate::prelude::{MockInput, QueryInput}; - use bevy::input::InputPlugin; - use bevy::prelude::*; - - #[test] - fn modifier_key_triggered_by_either_input() { - use crate::user_input::Modifier; - let mut app = App::new(); - app.add_plugins(InputPlugin); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(!InputStreams::from(&input_streams).pressed(Modifier::Control)); - - input_streams.send_input(KeyCode::ControlLeft); - app.update(); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(InputStreams::from(&input_streams).pressed(Modifier::Control)); - - input_streams.reset_inputs(); - app.update(); - - let mut input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(!InputStreams::from(&input_streams).pressed(Modifier::Control)); - - input_streams.send_input(KeyCode::ControlRight); - app.update(); - - let input_streams = MutableInputStreams::from_world(&mut app.world, None); - assert!(InputStreams::from(&input_streams).pressed(Modifier::Control)); - } -} diff --git a/src/lib.rs b/src/lib.rs index 9031351f..e07243a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,6 @@ pub mod axislike; pub mod buttonlike; pub mod clashing_inputs; pub mod common_conditions; -mod display_impl; pub mod errors; pub mod input_map; pub mod input_mocking; @@ -27,7 +26,6 @@ pub mod systems; pub mod timing; pub mod typetag; pub mod user_input; -pub mod user_inputs; // Importing the derive macro pub use leafwing_input_manager_macros::Actionlike; @@ -36,15 +34,13 @@ pub use leafwing_input_manager_macros::Actionlike; pub mod prelude { pub use crate::action_driver::ActionStateDriver; pub use crate::action_state::ActionState; - pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualAxis, VirtualDPad}; - pub use crate::buttonlike::MouseWheelDirection; pub use crate::clashing_inputs::ClashStrategy; pub use crate::input_map::InputMap; #[cfg(feature = "ui")] pub use crate::input_mocking::MockUIInteraction; pub use crate::input_mocking::{MockInput, QueryInput}; pub use crate::input_processing::*; - pub use crate::user_input::{InputKind, Modifier, UserInput}; + pub use crate::user_input::*; pub use crate::plugin::InputManagerPlugin; pub use crate::plugin::ToggleActions; diff --git a/src/plugin.rs b/src/plugin.rs index 70644e4a..5fe3a390 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,29 +1,26 @@ //! Contains the main plugin exported by this crate. - -use crate::action_state::{ActionData, ActionState}; -use crate::axislike::{ - AxisType, DualAxis, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, - VirtualAxis, VirtualDPad, -}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::clashing_inputs::ClashStrategy; -use crate::input_map::InputMap; -use crate::input_processing::*; -use crate::timing::Timing; -use crate::user_input::{InputKind, Modifier, UserInput}; -use crate::Actionlike; -use core::hash::Hash; -use core::marker::PhantomData; +//! use std::fmt::Debug; +use std::hash::Hash; +use std::marker::PhantomData; use bevy::app::{App, Plugin}; use bevy::ecs::prelude::*; use bevy::input::{ButtonState, InputSystem}; use bevy::prelude::{PostUpdate, PreUpdate}; use bevy::reflect::TypePath; + #[cfg(feature = "ui")] use bevy::ui::UiSystem; +use crate::action_state::{ActionData, ActionState}; +use crate::axislike::DualAxisData; +use crate::clashing_inputs::ClashStrategy; +use crate::input_map::InputMap; +use crate::input_processing::*; +use crate::timing::Timing; +use crate::Actionlike; + /// A [`Plugin`] that collects [`ButtonInput`](bevy::input::ButtonInput) from disparate sources, /// producing an [`ActionState`] that can be conveniently checked /// @@ -163,23 +160,11 @@ impl Plugin for InputManagerPlugin { app.register_type::>() .register_type::>() - .register_type::() - .register_type::() .register_type::() - .register_type::() .register_type::>() .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::() // Processors .register_type::() .register_type::() diff --git a/src/systems.rs b/src/systems.rs index 1823b9bb..f77a7071 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -136,7 +136,7 @@ pub fn update_action_state( associated_gamepad: input_map.gamepad(), }; - action_state.update(input_map.which_pressed(&input_streams, *clash_strategy)); + action_state.update(input_map.process_actions(&input_streams, *clash_strategy)); } } diff --git a/src/user_input.rs b/src/user_input.rs deleted file mode 100644 index 4a362c97..00000000 --- a/src/user_input.rs +++ /dev/null @@ -1,621 +0,0 @@ -//! Helpful abstractions over user inputs of all sorts - -use bevy::input::{gamepad::GamepadButtonType, keyboard::KeyCode, mouse::MouseButton}; -use bevy::reflect::Reflect; -use bevy::utils::HashSet; -use serde::{Deserialize, Serialize}; - -use crate::axislike::VirtualAxis; -use crate::{ - axislike::{AxisType, DualAxis, SingleAxis, VirtualDPad}, - buttonlike::{MouseMotionDirection, MouseWheelDirection}, -}; - -/// Some combination of user input, which may cross input-mode boundaries. -/// -/// For example, this may store mouse, keyboard or gamepad input, including cross-device chords! -/// -/// Suitable for use in an [`InputMap`](crate::input_map::InputMap) -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub enum UserInput { - /// A single button - Single(InputKind), - /// A combination of buttons, pressed simultaneously - // Note: we cannot use a HashSet here because of https://users.rust-lang.org/t/hash-not-implemented-why-cant-it-be-derived/92416/8 - // We cannot use a BTreeSet because the underlying types don't impl Ord - // We don't want to use a PetitSet here because of memory bloat - // So a vec it is! - // RIP your uniqueness guarantees - Chord(Vec), - /// A virtual D-pad that you can get a [`DualAxis`] from - VirtualDPad(VirtualDPad), - /// A virtual axis that you can get a [`SingleAxis`] from - VirtualAxis(VirtualAxis), -} - -impl UserInput { - /// Creates a [`UserInput::Chord`] from a [`Modifier`] and an `input` which can be converted into an [`InputKind`] - /// - /// When working with keyboard modifiers, - /// should be preferred to manually specifying both the left and right variant. - pub fn modified(modifier: Modifier, input: impl Into) -> UserInput { - let modifier: InputKind = modifier.into(); - let input: InputKind = input.into(); - - UserInput::chord(vec![modifier, input]) - } - - /// Creates a [`UserInput::Chord`] from an iterator over inputs of the same type convertible into an [`InputKind`]s - /// - /// If `inputs` has a length of 1, a [`UserInput::Single`] variant will be returned instead. - pub fn chord(inputs: impl IntoIterator>) -> Self { - // We can't just check the length unless we add an ExactSizeIterator bound :( - let vec: Vec = inputs.into_iter().map(|input| input.into()).collect(); - - match vec.len() { - 1 => UserInput::Single(vec[0].clone()), - _ => UserInput::Chord(vec), - } - } - - /// The number of logical inputs that make up the [`UserInput`]. - /// - /// - A [`Single`][UserInput::Single] input returns 1 - /// - A [`Chord`][UserInput::Chord] returns the number of buttons in the chord - /// - A [`VirtualDPad`][UserInput::VirtualDPad] returns 1 - /// - A [`VirtualAxis`][UserInput::VirtualAxis] returns 1 - pub fn len(&self) -> usize { - match self { - UserInput::Chord(button_set) => button_set.len(), - _ => 1, - } - } - - /// Is the number of buttons in the [`UserInput`] 0? - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// How many of the provided `buttons` are found in the [`UserInput`] - /// - /// # Example - /// ```rust - /// use bevy::input::keyboard::KeyCode::*; - /// use bevy::utils::HashSet; - /// use leafwing_input_manager::user_input::UserInput; - /// - /// let buttons = HashSet::from_iter([ControlLeft.into(), AltLeft.into()]); - /// let a: UserInput = KeyA.into(); - /// let ctrl_a = UserInput::chord([ControlLeft, KeyA]); - /// let ctrl_alt_a = UserInput::chord([ControlLeft, AltLeft, KeyA]); - /// - /// assert_eq!(a.n_matching(&buttons), 0); - /// assert_eq!(ctrl_a.n_matching(&buttons), 1); - /// assert_eq!(ctrl_alt_a.n_matching(&buttons), 2); - /// ``` - pub fn n_matching(&self, buttons: &HashSet) -> usize { - self.iter() - .filter(|button| buttons.contains(button)) - .count() - } - - /// Returns the raw inputs that make up this [`UserInput`] - pub fn raw_inputs(&self) -> RawInputs { - self.iter() - .fold(RawInputs::default(), |mut raw_inputs, input| { - raw_inputs.merge_input_data(&input); - raw_inputs - }) - } - - pub(crate) fn iter(&self) -> UserInputIter { - match self { - UserInput::Single(button) => UserInputIter::Single(Some(button.clone())), - UserInput::Chord(buttons) => UserInputIter::Chord(buttons.iter()), - UserInput::VirtualDPad(dpad) => UserInputIter::VirtualDPad( - Some(dpad.up.clone()), - Some(dpad.down.clone()), - Some(dpad.left.clone()), - Some(dpad.right.clone()), - ), - UserInput::VirtualAxis(axis) => { - UserInputIter::VirtualAxis(Some(axis.negative.clone()), Some(axis.positive.clone())) - } - } - } -} - -pub(crate) enum UserInputIter<'a> { - Single(Option), - Chord(std::slice::Iter<'a, InputKind>), - VirtualDPad( - Option, - Option, - Option, - Option, - ), - VirtualAxis(Option, Option), -} - -impl<'a> Iterator for UserInputIter<'a> { - type Item = InputKind; - - fn next(&mut self) -> Option { - match self { - Self::Single(ref mut input) => input.take(), - Self::Chord(ref mut iter) => iter.next().cloned(), - Self::VirtualDPad(ref mut up, ref mut down, ref mut left, ref mut right) => up - .take() - .or_else(|| down.take().or_else(|| left.take().or_else(|| right.take()))), - Self::VirtualAxis(ref mut negative, ref mut positive) => { - negative.take().or_else(|| positive.take()) - } - } - } -} - -impl From for UserInput { - fn from(input: InputKind) -> Self { - UserInput::Single(input) - } -} - -impl From for UserInput { - fn from(input: DualAxis) -> Self { - UserInput::Single(InputKind::DualAxis(input)) - } -} - -impl From for UserInput { - fn from(input: SingleAxis) -> Self { - UserInput::Single(InputKind::SingleAxis(input)) - } -} - -impl From for UserInput { - fn from(input: VirtualDPad) -> Self { - UserInput::VirtualDPad(input) - } -} - -impl From for UserInput { - fn from(input: VirtualAxis) -> Self { - UserInput::VirtualAxis(input) - } -} - -impl From for UserInput { - fn from(input: GamepadButtonType) -> Self { - UserInput::Single(InputKind::GamepadButton(input)) - } -} - -impl From for UserInput { - fn from(input: KeyCode) -> Self { - UserInput::Single(InputKind::PhysicalKey(input)) - } -} - -impl From for UserInput { - fn from(input: MouseButton) -> Self { - UserInput::Single(InputKind::Mouse(input)) - } -} - -impl From for UserInput { - fn from(input: MouseWheelDirection) -> Self { - UserInput::Single(InputKind::MouseWheel(input)) - } -} - -impl From for UserInput { - fn from(input: MouseMotionDirection) -> Self { - UserInput::Single(InputKind::MouseMotion(input)) - } -} - -impl From for UserInput { - fn from(input: Modifier) -> Self { - UserInput::Single(InputKind::Modifier(input)) - } -} - -/// The different kinds of supported input bindings. -/// -/// Commonly stored in the [`UserInput`] enum. -/// -/// Unfortunately, we cannot use a trait object here, as the types used by `ButtonInput` -/// require traits that are not object-safe. -/// -/// Please contact the maintainers if you need support for another type! -#[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub enum InputKind { - /// A button on a gamepad - GamepadButton(GamepadButtonType), - /// A single axis of continuous motion - SingleAxis(SingleAxis), - /// Two paired axes of continuous motion - DualAxis(DualAxis), - /// The physical location of a key on the keyboard. - PhysicalKey(KeyCode), - /// A keyboard modifier, like `Ctrl` or `Alt`, which doesn't care about which side it's on. - Modifier(Modifier), - /// A button on a mouse - Mouse(MouseButton), - /// A discretized mousewheel movement - MouseWheel(MouseWheelDirection), - /// A discretized mouse movement - MouseMotion(MouseMotionDirection), -} - -impl From for InputKind { - fn from(input: DualAxis) -> Self { - InputKind::DualAxis(input) - } -} - -impl From for InputKind { - fn from(input: SingleAxis) -> Self { - InputKind::SingleAxis(input) - } -} - -impl From for InputKind { - fn from(input: GamepadButtonType) -> Self { - InputKind::GamepadButton(input) - } -} - -impl From for InputKind { - fn from(input: KeyCode) -> Self { - InputKind::PhysicalKey(input) - } -} - -impl From for InputKind { - fn from(input: MouseButton) -> Self { - InputKind::Mouse(input) - } -} - -impl From for InputKind { - fn from(input: MouseWheelDirection) -> Self { - InputKind::MouseWheel(input) - } -} - -impl From for InputKind { - fn from(input: MouseMotionDirection) -> Self { - InputKind::MouseMotion(input) - } -} - -impl From for InputKind { - fn from(input: Modifier) -> Self { - InputKind::Modifier(input) - } -} - -/// A keyboard modifier that combines two [`KeyCode`] values into one representation. -/// -/// This buttonlike input is stored in [`InputKind`], and will be triggered whenever either of these buttons is pressed. -/// This will be decomposed into both values when converted into [`RawInputs`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub enum Modifier { - /// Corresponds to [`KeyCode::AltLeft`] and [`KeyCode::AltRight`]. - Alt, - /// Corresponds to [`KeyCode::ControlLeft`] and [`KeyCode::ControlRight`]. - Control, - /// The key that makes letters capitalized, corresponding to [`KeyCode::ShiftLeft`] and [`KeyCode::ShiftRight`] - Shift, - /// The OS or "Windows" key, corresponding to [`KeyCode::SuperLeft`] and [`KeyCode::SuperRight`]. - Super, -} - -impl Modifier { - /// Returns the pair of [`KeyCode`] values associated with this modifier. - /// - /// The left variant will always be in the first position, and the right variant is always in the second position. - #[inline] - pub fn key_codes(self) -> [KeyCode; 2] { - match self { - Modifier::Alt => [KeyCode::AltLeft, KeyCode::AltRight], - Modifier::Control => [KeyCode::ControlLeft, KeyCode::ControlRight], - Modifier::Shift => [KeyCode::ShiftLeft, KeyCode::ShiftRight], - Modifier::Super => [KeyCode::SuperLeft, KeyCode::SuperRight], - } - } -} - -/// The basic input events that make up a [`UserInput`]. -/// -/// Obtained by calling [`UserInput::raw_inputs()`]. -#[derive(Default, Debug, Clone, PartialEq)] -pub struct RawInputs { - /// Physical key locations. - pub keycodes: Vec, - /// Mouse buttons - pub mouse_buttons: Vec, - /// Discretized mouse wheel inputs - pub mouse_wheel: Vec, - /// Discretized mouse motion inputs - pub mouse_motion: Vec, - /// Gamepad buttons, independent of a [`Gamepad`](bevy::input::gamepad::Gamepad) - pub gamepad_buttons: Vec, - /// Axis-like data - /// - /// The `f32` stores the magnitude of the axis motion, and is only used for input mocking. - pub axis_data: Vec<(AxisType, Option)>, -} - -impl RawInputs { - /// Merges the data from the given `input_kind` into `self`. - fn merge_input_data(&mut self, input_kind: &InputKind) { - match input_kind { - InputKind::DualAxis(DualAxis { - x_axis_type, - y_axis_type, - value, - .. - }) => { - self.axis_data.push((*x_axis_type, value.map(|v| v.x))); - self.axis_data.push((*y_axis_type, value.map(|v| v.y))); - } - InputKind::SingleAxis(single_axis) => self - .axis_data - .push((single_axis.axis_type, single_axis.value)), - InputKind::GamepadButton(button) => self.gamepad_buttons.push(*button), - InputKind::PhysicalKey(key_code) => self.keycodes.push(*key_code), - InputKind::Modifier(modifier) => { - self.keycodes.extend_from_slice(&modifier.key_codes()); - } - InputKind::Mouse(button) => self.mouse_buttons.push(*button), - InputKind::MouseWheel(button) => self.mouse_wheel.push(*button), - InputKind::MouseMotion(button) => self.mouse_motion.push(*button), - } - } -} - -#[cfg(test)] -impl RawInputs { - fn from_keycode(keycode: KeyCode) -> RawInputs { - RawInputs { - keycodes: vec![keycode], - ..Default::default() - } - } - - fn from_mouse_button(mouse_button: MouseButton) -> RawInputs { - RawInputs { - mouse_buttons: vec![mouse_button], - ..Default::default() - } - } - - fn from_gamepad_button(gamepad_button: GamepadButtonType) -> RawInputs { - RawInputs { - gamepad_buttons: vec![gamepad_button], - ..Default::default() - } - } - - fn from_mouse_wheel(direction: MouseWheelDirection) -> RawInputs { - RawInputs { - mouse_wheel: vec![direction], - ..Default::default() - } - } - - fn from_mouse_direction(direction: MouseMotionDirection) -> RawInputs { - RawInputs { - mouse_motion: vec![direction], - ..Default::default() - } - } - - fn from_dual_axis(axis: DualAxis) -> RawInputs { - RawInputs { - axis_data: vec![ - (axis.x_axis_type, axis.value.map(|v| v.x)), - (axis.y_axis_type, axis.value.map(|v| v.y)), - ], - ..Default::default() - } - } - - fn from_single_axis(axis: SingleAxis) -> RawInputs { - RawInputs { - axis_data: vec![(axis.axis_type, axis.value)], - ..Default::default() - } - } -} - -#[cfg(test)] -mod raw_input_tests { - use crate::{ - axislike::AxisType, - user_input::{InputKind, RawInputs, UserInput}, - }; - - #[test] - fn simple_chord() { - use bevy::input::gamepad::GamepadButtonType; - - let buttons = vec![GamepadButtonType::Start, GamepadButtonType::Select]; - let raw_inputs = UserInput::chord(buttons.clone()).raw_inputs(); - let expected = RawInputs { - gamepad_buttons: buttons, - ..Default::default() - }; - - assert_eq!(expected, raw_inputs); - } - - #[test] - fn mixed_chord() { - use crate::axislike::SingleAxis; - use bevy::input::gamepad::GamepadAxisType; - use bevy::input::gamepad::GamepadButtonType; - - let chord = UserInput::chord([ - InputKind::GamepadButton(GamepadButtonType::Start), - InputKind::SingleAxis(SingleAxis::new(GamepadAxisType::LeftZ)), - ]); - - let raw = chord.raw_inputs(); - let expected = RawInputs { - gamepad_buttons: vec![GamepadButtonType::Start], - axis_data: vec![(AxisType::Gamepad(GamepadAxisType::LeftZ), None)], - ..Default::default() - }; - - assert_eq!(expected, raw); - } - - mod gamepad { - use crate::user_input::{RawInputs, UserInput}; - - #[test] - fn gamepad_button() { - use bevy::input::gamepad::GamepadButtonType; - - let button = GamepadButtonType::Start; - let expected = RawInputs::from_gamepad_button(button); - let raw = UserInput::from(button).raw_inputs(); - assert_eq!(expected, raw); - } - - #[test] - fn single_gamepad_axis() { - use crate::axislike::SingleAxis; - use bevy::input::gamepad::GamepadAxisType; - - let direction = SingleAxis::from_value(GamepadAxisType::LeftStickX, 1.0); - let expected = RawInputs::from_single_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - - #[test] - fn dual_gamepad_axis() { - use crate::axislike::DualAxis; - use bevy::input::gamepad::GamepadAxisType; - - let direction = DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.5, - 0.7, - ); - let expected = RawInputs::from_dual_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - } - - mod keyboard { - use crate::user_input::{RawInputs, UserInput}; - - #[test] - fn keyboard_button() { - use bevy::input::keyboard::KeyCode; - - let button = KeyCode::KeyA; - let expected = RawInputs::from_keycode(button); - let raw = UserInput::from(button).raw_inputs(); - assert_eq!(expected, raw); - } - - #[test] - fn modifier_key_decomposes_into_both_inputs() { - use crate::user_input::Modifier; - use bevy::input::keyboard::KeyCode; - - let input = UserInput::modified(Modifier::Control, KeyCode::KeyS); - let expected = RawInputs { - keycodes: vec![KeyCode::ControlLeft, KeyCode::ControlRight, KeyCode::KeyS], - ..Default::default() - }; - let raw = input.raw_inputs(); - assert_eq!(expected, raw); - } - } - - mod mouse { - use crate::user_input::{RawInputs, UserInput}; - - #[test] - fn mouse_button() { - use bevy::input::mouse::MouseButton; - - let button = MouseButton::Left; - let expected = RawInputs::from_mouse_button(button); - let raw = UserInput::from(button).raw_inputs(); - assert_eq!(expected, raw); - } - - #[test] - fn mouse_wheel() { - use crate::buttonlike::MouseWheelDirection; - - let direction = MouseWheelDirection::Down; - let expected = RawInputs::from_mouse_wheel(direction); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw); - } - - #[test] - fn mouse_motion() { - use crate::buttonlike::MouseMotionDirection; - - let direction = MouseMotionDirection::Up; - let expected = RawInputs::from_mouse_direction(direction); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw); - } - - #[test] - fn single_mousewheel_axis() { - use crate::axislike::{MouseWheelAxisType, SingleAxis}; - - let direction = SingleAxis::from_value(MouseWheelAxisType::X, 1.0); - let expected = RawInputs::from_single_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - - #[test] - fn dual_mousewheel_axis() { - use crate::axislike::{DualAxis, MouseWheelAxisType}; - - let direction = - DualAxis::from_value(MouseWheelAxisType::X, MouseWheelAxisType::Y, 1.0, 1.0); - let expected = RawInputs::from_dual_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - - #[test] - fn single_mouse_motion_axis() { - use crate::axislike::{MouseMotionAxisType, SingleAxis}; - - let direction = SingleAxis::from_value(MouseMotionAxisType::X, 1.0); - let expected = RawInputs::from_single_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - - #[test] - fn dual_mouse_motion_axis() { - use crate::axislike::{DualAxis, MouseMotionAxisType}; - - let direction = - DualAxis::from_value(MouseMotionAxisType::X, MouseMotionAxisType::Y, 1.0, 1.0); - let expected = RawInputs::from_dual_axis(direction.clone()); - let raw = UserInput::from(direction).raw_inputs(); - assert_eq!(expected, raw) - } - } -} diff --git a/src/user_inputs/axislike.rs b/src/user_inputs/axislike.rs deleted file mode 100644 index 7e16adfd..00000000 --- a/src/user_inputs/axislike.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::input_processing::{AxisProcessor, DualAxisProcessor}; -use crate::input_streams::InputStreams; -use crate::orientation::Rotation; -use bevy::math::Vec2; -use bevy::prelude::{Direction2d, Reflect}; -use bevy::utils::petgraph::matrix_graph::Zero; -use serde::{Deserialize, Serialize}; - -/// Different ways that user input is represented on an axis. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Reflect)] -#[must_use] -pub enum AxisInputMode { - /// Continuous input values, typically range from a negative maximum (e.g., `-1.0`) - /// to a positive maximum (e.g., `1.0`), allowing for smooth and precise control. - Analog, - - /// Discrete input values, using three distinct values to represent the states: - /// `-1.0` for active in negative direction, `0.0` for inactive, and `1.0` for active in positive direction. - Digital, -} - -impl AxisInputMode { - /// Converts the given `f32` value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0`, leaving others as is. - #[must_use] - #[inline] - pub fn axis_value(&self, value: f32) -> f32 { - match self { - Self::Analog => value, - Self::Digital => { - if value < 0.0 { - -1.0 - } else if value > 0.0 { - 1.0 - } else { - value - } - } - } - } - - /// Converts the given [`Vec2`] value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0` along each axis, leaving others as is. - #[must_use] - #[inline] - pub fn dual_axis_value(&self, value: Vec2) -> Vec2 { - match self { - Self::Analog => value, - Self::Digital => Vec2::new(self.axis_value(value.x), self.axis_value(value.y)), - } - } - - /// Computes the magnitude of given [`Vec2`] value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: `1.0` for non-zero values, `0.0` for others. - #[must_use] - #[inline] - pub fn dual_axis_magnitude(&self, value: Vec2) -> f32 { - match self { - Self::Analog => value.length(), - Self::Digital => f32::from(value != Vec2::ZERO), - } - } -} - -/// The axes for dual-axis inputs. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub enum DualAxis { - /// The X-axis (typically horizontal movement). - X, - - /// The Y-axis (typically vertical movement). - Y, -} - -impl DualAxis { - /// Gets the component on the specified axis. - #[must_use] - #[inline] - pub fn value(&self, value: Vec2) -> f32 { - match self { - Self::X => value.x, - Self::Y => value.y, - } - } -} - -/// The directions for dual-axis inputs. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub enum DualAxisDirection { - /// Upward direction. - Up, - - /// Downward direction. - Down, - - /// Leftward direction. - Left, - - /// Rightward direction. - Right, -} - -impl DualAxisDirection { - /// Checks if the given `value` is active in the specified direction. - #[must_use] - #[inline] - pub fn is_active(&self, value: Vec2) -> bool { - match self { - Self::Up => value.y > 0.0, - Self::Down => value.y < 0.0, - Self::Left => value.x < 0.0, - Self::Right => value.x > 0.0, - } - } -} - -/// A combined input data from two axes (X and Y). -/// -/// This struct stores the X and Y values as a [`Vec2`] in a device-agnostic way, -/// meaning it works consistently regardless of the specific input device (gamepad, joystick, etc.). -/// It assumes any calibration (deadzone correction, rescaling, drift correction, etc.) -/// has already been applied at an earlier stage of processing. -/// -/// The neutral origin of this input data is always at `(0, 0)`. -/// When working with gamepad axes, both X and Y values are typically bounded by `[-1.0, 1.0]`. -/// However, this range may not apply to other input types, such as mousewheel data which can have a wider range. -#[derive(Default, Debug, Copy, Clone, PartialEq, Deserialize, Serialize, Reflect)] -#[must_use] -pub struct DualAxisData(Vec2); - -impl DualAxisData { - /// Creates a [`DualAxisData`] with the given values. - pub const fn new(x: f32, y: f32) -> Self { - Self(Vec2::new(x, y)) - } - - /// Creates a [`DualAxisData`] directly from the given [`Vec2`]. - pub const fn from_xy(xy: Vec2) -> Self { - Self(xy) - } - - /// Combines the directional input from this [`DualAxisData`] with another. - /// - /// This is useful if you have multiple sticks bound to the same game action, - /// and you want to get their combined position. - /// - /// # Warning - /// - /// This method performs vector addition on the X and Y components. - /// While the direction is preserved, the combined magnitude might exceed the expected - /// range for certain input devices (e.g., gamepads typically have a maximum magnitude of `1.0`). - /// - /// To ensure the combined input stays within the expected range, - /// consider using [`Self::clamp_length`] on the returned value. - pub fn merged_with(&self, other: Self) -> Self { - Self::analog(self.0 + other.0) - } - - /// The value along the X-axis, typically ranging from `-1.0` to `1.0`. - #[must_use] - #[inline] - pub const fn x(&self) -> f32 { - self.0.x - } - - /// The value along the Y-axis, typically ranging from `-1.0` to `1.0`. - #[must_use] - #[inline] - pub const fn y(&self) -> f32 { - self.0.y - } - - /// The values along each axis, each typically ranging from `-1.0` to `1.0`. - #[must_use] - #[inline] - pub const fn xy(&self) -> Vec2 { - self.0 - } - - /// The [`Direction2d`] that this axis is pointing towards, if any - /// - /// If the axis is neutral (x, y) = (0, 0), this will be `None`. - #[must_use] - #[inline] - pub fn direction(&self) -> Option { - Direction2d::new(self.0).ok() - } - - /// The [`Rotation`] (measured clockwise from midnight) that this axis is pointing towards, if any - /// - /// If the axis is neutral (x, y) = (0, 0), this will be `None`. - #[must_use] - #[inline] - pub fn rotation(&self) -> Option { - Rotation::from_xy(self.0).ok() - } - - /// How far from the origin is this axis's position? - /// - /// Typically bounded by 0 and 1. - /// - /// If you only need to compare relative magnitudes, use [`Self::length_squared`] instead for faster computation. - #[must_use] - #[inline] - pub fn length(&self) -> f32 { - self.0.length() - } - - /// The square of the axis' magnitude - /// - /// Typically bounded by 0 and 1. - /// - /// This is faster than [`Self::length`], as it avoids a square root, but will generally have less natural behavior. - #[must_use] - #[inline] - pub fn length_squared(&self) -> f32 { - self.0.length_squared() - } - - /// Clamps the magnitude of the axis - pub fn clamp_length(&mut self, max: f32) { - self.0 = self.0.clamp_length_max(max); - } -} - -impl From for Vec2 { - fn from(data: DualAxisData) -> Vec2 { - data.0 - } -} diff --git a/src/user_inputs/chord.rs b/src/user_inputs/chord.rs deleted file mode 100644 index ec9fbfe7..00000000 --- a/src/user_inputs/chord.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! This module contains [`ChordInput`] and its supporting methods and impls.. - -use bevy::prelude::{Reflect, Vec2}; -use leafwing_input_manager_macros::serde_typetag; -use serde::{Deserialize, Serialize}; - -use crate::input_streams::InputStreams; -use crate::user_inputs::UserInput; - -/// A combined input that holds multiple [`UserInput`]s to represent simultaneous button presses. -/// -/// # Behaviors -/// -/// When it treated as a button, it checks if all inner inputs are active simultaneously. -/// When it treated as a single-axis input, it uses the sum of values from all inner single-axis inputs. -/// When it treated as a dual-axis input, it only uses the value of the first inner dual-axis input. -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub struct ChordInput( - // Note: we cannot use a HashSet here because of https://users.rust-lang.org/t/hash-not-implemented-why-cant-it-be-derived/92416/8 - // We cannot use a BTreeSet because the underlying types don't impl Ord - // We don't want to use a PetitSet here because of memory bloat - // So a vec it is! - // RIP your uniqueness guarantees - Vec>, -); - -// #[serde_typetag] -impl UserInput for ChordInput { - /// Checks if all the inner inputs are active. - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - self.0.iter().all(|input| input.is_active(input_streams)) - } - - /// Returns a combined value representing the input. - /// - /// # Returns - /// - /// - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - match self.0.iter().next() { - Some(input) => input.value(input_streams), - None => 0.0, - } - } - - /// Retrieves the value of the first inner dual-axis input. - #[inline] - fn axis_pair(&self, input_streams: &InputStreams) -> Option { - self.0 - .iter() - .find_map(|input| input.axis_pair(input_streams)) - } -} diff --git a/src/user_inputs/gamepad.rs b/src/user_inputs/gamepad.rs deleted file mode 100644 index 6522051f..00000000 --- a/src/user_inputs/gamepad.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Gamepad inputs - -use bevy::prelude::{Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType}; -use leafwing_input_manager_macros::serde_typetag; - -use crate as leafwing_input_manager; -use crate::input_streams::InputStreams; -use crate::user_inputs::UserInput; - -// Built-in support for Bevy's GamepadButtonType. -#[serde_typetag] -impl UserInput for GamepadButtonType { - /// Checks if the specified [`GamepadButtonType`] is currently pressed down. - /// - /// When a [`Gamepad`] is specified, only checks if the button is pressed on the gamepad. - /// Otherwise, checks if the button is pressed on any connected gamepads. - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let gamepad_pressed_self = |gamepad: Gamepad| -> bool { - let button = GamepadButton::new(gamepad, *self); - input_streams.gamepad_buttons.pressed(button) - }; - - if let Some(gamepad) = input_streams.associated_gamepad { - gamepad_pressed_self(gamepad) - } else { - input_streams.gamepads.iter().any(gamepad_pressed_self) - } - } - - /// Retrieves the strength of the button press for the specified [`GamepadButtonType`]. - /// - /// When a [`Gamepad`] is specified, only retrieves the value of the button on the gamepad. - /// Otherwise, retrieves the value on any connected gamepads. - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - // This implementation differs from `is_active()` because the upstream bevy::input - // still waffles about whether triggers are buttons or axes. - // So, we consider the axes for consistency with other gamepad axes (e.g., thumbs ticks). - - let gamepad_value_self = |gamepad: Gamepad| -> Option { - let button = GamepadButton::new(gamepad, *self); - input_streams.gamepad_button_axes.get(button) - }; - - if let Some(gamepad) = input_streams.associated_gamepad { - gamepad_value_self(gamepad).unwrap_or_else(|| f32::from(self.is_active(input_streams))) - } else { - input_streams - .gamepads - .iter() - .map(gamepad_value_self) - .flatten() - .find(|value| *value != 0.0) - .unwrap_or_default() - } - } -} - -// Built-in support for Bevy's GamepadAxisType. -// #[serde_typetag] -impl UserInput for GamepadAxisType { - /// Checks if the specified [`GamepadAxisType`] is currently active. - /// - /// When a [`Gamepad`] is specified, only checks if the axis is triggered on the gamepad. - /// Otherwise, checks if the axis is triggered on any connected gamepads. - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - self.value(input_streams) != 0.0 - } - - /// Retrieves the strength of the specified [`GamepadAxisType`]. - /// - /// When a [`Gamepad`] is specified, only retrieves the value of the axis on the gamepad. - /// Otherwise, retrieves the axis on any connected gamepads. - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - let gamepad_value_self = |gamepad: Gamepad| -> Option { - let axis = GamepadAxis::new(gamepad, *self); - input_streams.gamepad_axes.get(axis) - }; - - if let Some(gamepad) = input_streams.associated_gamepad { - gamepad_value_self(gamepad).unwrap_or_else(|| f32::from(self.is_active(input_streams))) - } else { - input_streams - .gamepads - .iter() - .map(gamepad_value_self) - .flatten() - .find(|value| *value != 0.0) - .unwrap_or_default() - } - } -} diff --git a/src/user_inputs/keyboard.rs b/src/user_inputs/keyboard.rs deleted file mode 100644 index 22236a9e..00000000 --- a/src/user_inputs/keyboard.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Keyboard inputs - -use bevy::prelude::{KeyCode, Reflect, Vec2}; -use leafwing_input_manager_macros::serde_typetag; -use serde::{Deserialize, Serialize}; - -use crate as leafwing_input_manager; -use crate::input_streams::InputStreams; -use crate::user_inputs::axislike::DualAxisData; -use crate::user_inputs::UserInput; - -// Built-in support for Bevy's KeyCode -#[serde_typetag] -impl UserInput for KeyCode { - /// Checks if the specified [`KeyCode`] is currently pressed down. - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - input_streams - .keycodes - .is_some_and(|keys| keys.pressed(*self)) - } - - /// Retrieves the strength of the key press for the specified [`KeyCode`], - /// returning `0.0` for no press and `1.0` for a currently pressed key. - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.is_active(input_streams)) - } -} - -/// Keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). -/// -/// Each variant represents a pair of [`KeyCode`]s, the left and right version of the modifier key, -/// allowing for handling modifiers regardless of which side is pressed. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub enum ModifierKey { - /// The Alt key, representing either [`KeyCode::AltLeft`] or [`KeyCode::AltRight`]. - Alt, - - /// The Control key, representing either [`KeyCode::ControlLeft`] or [`KeyCode::ControlRight`]. - Control, - - /// The Shift key, representing either [`KeyCode::ShiftLeft`] or [`KeyCode::ShiftRight`]. - Shift, - - /// The Super (OS symbol) key, representing either [`KeyCode::SuperLeft`] or [`KeyCode::SuperRight`]. - Super, -} - -impl ModifierKey { - /// Returns a pair of [`KeyCode`]s corresponding to both modifier keys. - pub const fn keys(&self) -> [KeyCode; 2] { - [self.left(), self.right()] - } - - /// Returns the [`KeyCode`] corresponding to the left modifier key. - pub const fn left(&self) -> KeyCode { - match self { - ModifierKey::Alt => KeyCode::AltLeft, - ModifierKey::Control => KeyCode::ControlLeft, - ModifierKey::Shift => KeyCode::ShiftLeft, - ModifierKey::Super => KeyCode::SuperLeft, - } - } - - /// Returns the [`KeyCode`] corresponding to the right modifier key. - pub const fn right(&self) -> KeyCode { - match self { - ModifierKey::Alt => KeyCode::AltRight, - ModifierKey::Control => KeyCode::ControlRight, - ModifierKey::Shift => KeyCode::ShiftRight, - ModifierKey::Super => KeyCode::SuperRight, - } - } -} - -#[serde_typetag] -impl UserInput for ModifierKey { - /// Checks if the specified [`ModifierKey`] is currently pressed down. - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let modifiers = self.keys(); - input_streams - .keycodes - .is_some_and(|keys| keys.pressed(modifiers[0]) | keys.pressed(modifiers[1])) - } - - /// Gets the strength of the key press for the specified [`ModifierKey`], - /// returning `0.0` for no press and `1.0` for a currently pressed key. - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.is_active(input_streams)) - } -} diff --git a/src/user_inputs/mod.rs b/src/user_inputs/mod.rs deleted file mode 100644 index f08131a7..00000000 --- a/src/user_inputs/mod.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Helpful abstractions over user inputs of all sorts. -//! -//! This module provides abstractions and utilities for defining and handling user inputs -//! across various input devices such as gamepads, keyboards, and mice. -//! It offers a unified interface for querying input values and states, -//! making it easier to manage and process user interactions within a Bevy application. -//! -//! # Traits -//! -//! - [`UserInput`]: A trait for defining a specific kind of user input. -//! It provides methods for checking if the input is active, -//! retrieving its current value, and detecting when it started or finished. -//! -//! # Modules -//! -//! ## General Input Settings -//! -//! - [`axislike_processors`]: Utilities for configuring axis-like inputs. -//! -//! ## General Inputs -//! -//! - [`gamepad`]: Utilities for handling gamepad inputs. -//! - [`keyboard`]: Utilities for handling keyboard inputs. -//! - [`mouse`]: Utilities for handling mouse inputs. -//! -//! ## Specific Inputs -//! -//! - [`chord`]: A combination of buttons, pressed simultaneously. - -use std::any::{Any, TypeId}; -use std::fmt::{Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::sync::RwLock; - -use bevy::prelude::{App, Vec2}; -use bevy::reflect::utility::{reflect_hasher, GenericTypePathCell, NonGenericTypeInfoCell}; -use bevy::reflect::{ - erased_serde, DynamicTypePath, FromReflect, FromType, GetTypeRegistration, Reflect, - ReflectDeserialize, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - ReflectSerialize, TypeInfo, TypePath, TypeRegistration, Typed, ValueInfo, -}; -use dyn_clone::DynClone; -use dyn_eq::DynEq; -use dyn_hash::DynHash; -use once_cell::sync::Lazy; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_flexitos::ser::require_erased_serialize_impl; -use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; - -use self::axislike::DualAxisData; -use crate::input_streams::InputStreams; -use crate::orientation::Rotation; -use crate::typetag::RegisterTypeTag; - -pub use self::chord::*; -pub use self::gamepad::*; -pub use self::keyboard::*; -pub use self::mouse::*; - -pub mod axislike; -pub mod chord; -pub mod gamepad; -pub mod keyboard; -pub mod mouse; - -#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] -pub enum UserInputDataType { - Button, - Axis, - DualAxis, -} - -/// A trait for defining the behavior expected from different user input sources. -/// -/// Implementers of this trait should provide methods for accessing and processing user input data. -pub trait UserInput: - Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize -{ - /// Checks if the input is currently active. - fn is_active(&self, input_streams: &InputStreams) -> bool; - - /// Retrieves the current value of the input. - fn value(&self, input_streams: &InputStreams) -> f32; - - /// Attempts to retrieve the current [`DualAxisData`] of the input if applicable. - /// - /// This method is intended for inputs that represent movement on two axes. - /// However, some input types (e.g., buttons, mouse scroll) don't inherently provide separate X and Y information. - /// - /// For implementers that don't represent dual-axis input, this method should always return [`None`]. - fn axis_pair(&self, _input_streams: &InputStreams) -> Option { - None - } -} - -dyn_clone::clone_trait_object!(UserInput); -dyn_eq::eq_trait_object!(UserInput); -dyn_hash::hash_trait_object!(UserInput); - -impl Reflect for Box { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(Self::type_info()) - } - - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - *self = value.clone(); - } else { - panic!( - "Value is not a std::boxed::Box.", - module_path!(), - ); - } - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Value - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Value(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Value(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Value(self) - } - - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - let type_id = TypeId::of::(); - Hash::hash(&type_id, &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - value - .as_any() - .downcast_ref::() - .map(|value| self.dyn_eq(value)) - .or(Some(false)) - } - - fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self, f) - } -} - -impl Typed for Box { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) - } -} - -impl TypePath for Box { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| { - { - format!("std::boxed::Box", module_path!()) - } - }) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| "Box".to_string()) - } - - fn type_ident() -> Option<&'static str> { - Some("Box") - } - - fn crate_name() -> Option<&'static str> { - Some(module_path!().split(':').next().unwrap()) - } - - fn module_path() -> Option<&'static str> { - Some(module_path!()) - } -} - -impl GetTypeRegistration for Box { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for Box { - fn from_reflect(reflect: &dyn Reflect) -> Option { - Some(reflect.as_any().downcast_ref::()?.clone()) - } -} - -impl<'a> Serialize for dyn UserInput + 'a { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Check that `UserInput` has `erased_serde::Serialize` as a super trait, - // preventing infinite recursion at runtime. - const fn __check_erased_serialize_super_trait() { - require_erased_serialize_impl::(); - } - serialize_trait_object(serializer, self.reflect_short_type_path(), self) - } -} - -impl<'de> Deserialize<'de> for Box { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let registry = unsafe { INPUT_REGISTRY.read().unwrap() }; - registry.deserialize_trait_object(deserializer) - } -} - -/// Registry of deserializers for [`UserInput`]s. -static mut INPUT_REGISTRY: Lazy>> = - Lazy::new(|| RwLock::new(MapRegistry::new("UserInput"))); - -/// A trait for registering a specific [`UserInput`]. -pub trait RegisterUserInput { - /// Registers the specified [`UserInput`]. - fn register_user_input<'de, T>(&mut self) -> &mut Self - where - T: RegisterTypeTag<'de, dyn UserInput> + GetTypeRegistration; -} - -impl RegisterUserInput for App { - fn register_user_input<'de, T>(&mut self) -> &mut Self - where - T: RegisterTypeTag<'de, dyn UserInput> + GetTypeRegistration, - { - let mut registry = unsafe { INPUT_REGISTRY.write().unwrap() }; - T::register_typetag(&mut registry); - self.register_type::(); - self - } -} diff --git a/src/user_inputs/mouse.rs b/src/user_inputs/mouse.rs deleted file mode 100644 index 1ca71ee7..00000000 --- a/src/user_inputs/mouse.rs +++ /dev/null @@ -1,595 +0,0 @@ -//! Mouse inputs - -use bevy::prelude::{MouseButton, Reflect, Vec2}; -use leafwing_input_manager_macros::serde_typetag; -use serde::{Deserialize, Serialize}; - -use crate as leafwing_input_manager; -use crate::input_processing::*; -use crate::input_streams::InputStreams; -use crate::user_inputs::axislike::{AxisInputMode, DualAxis, DualAxisData, DualAxisDirection}; -use crate::user_inputs::UserInput; - -// Built-in support for Bevy's MouseButton -impl UserInput for MouseButton { - /// Checks if the specified [`MouseButton`] is currently pressed down. - fn is_active(&self, input_streams: &InputStreams) -> bool { - input_streams - .mouse_buttons - .is_some_and(|buttons| buttons.pressed(*self)) - } - - /// Retrieves the strength of the button press for the specified [`MouseButton`]. - /// - /// # Returns - /// - /// - `1.0` if the button is currently pressed down, indicating an active input. - /// - `0.0` if the button is not pressed, signifying no input. - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.is_active(input_streams)) - } - - /// Always returns [`None`] as [`MouseButton`]s don't represent dual-axis input. - fn axis_pair(&self, _input_streams: &InputStreams) -> Option { - None - } -} - -/// Retrieves the total mouse displacement. -#[must_use] -#[inline] -fn accumulate_mouse_movement(input_streams: &InputStreams) -> Vec2 { - // PERF: this summing is computed for every individual input - // This should probably be computed once, and then cached / read - // Fix upstream! - input_streams - .mouse_motion - .iter() - .map(|event| event.delta) - .sum() -} - -/// Input associated with mouse movement on both X and Y axes. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseMove { - /// The [`AxisInputMode`] for both axes. - pub(crate) input_mode: AxisInputMode, - - /// The [`DualAxisProcessor`] used to handle input values. - pub(crate) processor: DualAxisProcessor, -} - -impl MouseMove { - /// Creates a [`MouseMove`] for continuous movement on X and Y axes without any processing applied. - pub const fn analog() -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - } - } - - /// Creates a [`MouseMove`] for discrete movement on X and Y axes without any processing applied. - pub const fn digital() -> Self { - Self { - input_mode: AxisInputMode::Digital, - processor: DualAxisProcessor::None, - } - } - - /// Creates a [`MouseMove`] for continuous movement on X and Y axes with the specified [`DualAxisProcessor`]. - pub fn analog_using(processor: impl Into) -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseMove`] for discrete movement on X and Y axes with the specified [`DualAxisProcessor`]. - pub fn digital_using(processor: impl Into) -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Appends the given [`DualAxisProcessor`] as the next processing step. - #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self - } - - /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. - #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self - } - - /// Removes the current used [`DualAxisProcessor`]. - #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = DualAxisProcessor::None; - self - } -} - -#[serde_typetag] -impl UserInput for MouseMove { - /// Checks if there is any recent mouse movement. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_mouse_movement(input_streams); - self.processor.process(movement) != Vec2::ZERO - } - - /// Retrieves the amount of the mouse movement - /// after processing by the associated [`DualAxisProcessor`]. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_mouse_movement(input_streams); - let value = self.processor.process(movement); - self.input_mode.dual_axis_magnitude(value) - } - - /// Retrieves the mouse displacement - /// after processing by the associated [`DualAxisProcessor`]. - #[must_use] - #[inline] - fn axis_pair(&self, input_streams: &InputStreams) -> Option { - let movement = accumulate_mouse_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); - Some(DualAxisData::from_xy(value)) - } -} - -/// Input associated with mouse movement on a specific axis. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseMoveAxis { - /// The axis that this input tracks. - pub(crate) axis: DualAxis, - - /// The [`AxisInputMode`] for the axis. - pub(crate) input_mode: AxisInputMode, - - /// The [`AxisProcessor`] used to handle input values. - pub(crate) processor: AxisProcessor, -} - -impl MouseMoveAxis { - /// Creates a [`MouseMoveAxis`] for continuous movement on the X-axis without any processing applied. - pub const fn analog_x() -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseMoveAxis`] for continuous movement on the Y-axis without any processing applied. - pub const fn analog_y() -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseMoveAxis`] for discrete movement on the X-axis without any processing applied. - pub const fn digital_x() -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseMoveAxis`] for discrete movement on the Y-axis without any processing applied. - pub const fn digital_y() -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - } - } - /// Creates a [`MouseMoveAxis`] for continuous movement on the X-axis with the specified [`AxisProcessor`]. - pub fn analog_x_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseMoveAxis`] for continuous movement on the Y-axis with the specified [`AxisProcessor`]. - pub fn analog_y_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseMoveAxis`] for discrete movement on the X-axis with the specified [`AxisProcessor`]. - pub fn digital_x_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseMoveAxis`] for discrete movement on the Y-axis with the specified [`AxisProcessor`]. - pub fn digital_y_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Appends the given [`AxisProcessor`] as the next processing step. - #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self - } - - /// Replaces the current [`AxisProcessor`] with the specified `processor`. - #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self - } - - /// Removes the current used [`AxisProcessor`]. - #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = AxisProcessor::None; - self - } -} - -#[serde_typetag] -impl UserInput for MouseMoveAxis { - /// Checks if there is any recent mouse movement along the specified axis. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_mouse_movement(input_streams); - let value = self.axis.value(movement); - self.processor.process(value) != 0.0 - } - - /// Retrieves the amount of the mouse movement along the specified axis - /// after processing by the associated [`AxisProcessor`]. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_mouse_movement(input_streams); - let value = self.axis.value(movement); - let value = self.processor.process(value); - self.input_mode.axis_value(value) - } -} - -/// Input associated with mouse movement on a specific direction, treated as a button press. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseMoveDirection(DualAxisDirection); - -impl MouseMoveDirection { - /// Movement in the upward direction. - const UP: Self = Self(DualAxisDirection::Up); - - /// Movement in the downward direction. - const DOWN: Self = Self(DualAxisDirection::Down); - - /// Movement in the leftward direction. - const LEFT: Self = Self(DualAxisDirection::Left); - - /// Movement in the rightward direction. - const RIGHT: Self = Self(DualAxisDirection::Right); -} - -#[serde_typetag] -impl UserInput for MouseMoveDirection { - /// Checks if there is any recent mouse movement along the specified direction. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_mouse_movement(input_streams); - self.0.is_active(movement) - } - - /// Retrieves the amount of the mouse movement along the specified direction, - /// returning `0.0` for no movement and `1.0` for a currently active direction. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.is_active(input_streams)) - } -} - -/// Accumulates the mouse wheel movement. -#[must_use] -#[inline] -fn accumulate_wheel_movement(input_streams: &InputStreams) -> Vec2 { - let Some(wheel) = &input_streams.mouse_wheel else { - return Vec2::ZERO; - }; - - wheel.iter().map(|event| Vec2::new(event.x, event.y)).sum() -} - -/// Input associated with mouse wheel movement on both X and Y axes. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseScroll { - /// The [`AxisInputMode`] for both axes. - pub(crate) input_mode: AxisInputMode, - - /// The [`DualAxisProcessor`] used to handle input values. - pub(crate) processor: DualAxisProcessor, -} - -impl MouseScroll { - /// Creates a [`MouseScroll`] for continuous movement on X and Y axes without any processing applied. - pub const fn analog() -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - } - } - - /// Creates a [`MouseScroll`] for discrete movement on X and Y axes without any processing applied. - pub const fn digital() -> Self { - Self { - input_mode: AxisInputMode::Digital, - processor: DualAxisProcessor::None, - } - } - - /// Creates a [`MouseScroll`] for continuous movement on X and Y axes with the specified [`DualAxisProcessor`]. - pub fn analog_using(processor: impl Into) -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseScroll`] for discrete movement on X and Y axes with the specified [`DualAxisProcessor`]. - pub fn digital_using(processor: impl Into) -> Self { - Self { - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Appends the given [`DualAxisProcessor`] as the next processing step. - #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self - } - - /// Replaces the current [`DualAxisProcessor`] with the specified `processor`. - #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self - } - - /// Removes the current used [`DualAxisProcessor`]. - #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = DualAxisProcessor::None; - self - } -} - -#[serde_typetag] -impl UserInput for MouseScroll { - /// Checks if there is any recent mouse wheel movement. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_wheel_movement(input_streams); - self.processor.process(movement) != Vec2::ZERO - } - - /// Retrieves the amount of the mouse wheel movement on both axes - /// after processing by the associated [`DualAxisProcessor`]. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_wheel_movement(input_streams); - let value = self.processor.process(movement); - self.input_mode.dual_axis_magnitude(value) - } - - /// Retrieves the mouse scroll movement on both axes - /// after processing by the associated [`DualAxisProcessor`]. - #[must_use] - #[inline] - fn axis_pair(&self, input_streams: &InputStreams) -> Option { - let movement = accumulate_wheel_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); - Some(DualAxisData::from_xy(value)) - } -} - -/// Input associated with mouse wheel movement on a specific axis. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseScrollAxis { - /// The axis that this input tracks. - pub(crate) axis: DualAxis, - - /// The [`AxisInputMode`] for the axis. - pub(crate) input_mode: AxisInputMode, - - /// The [`AxisProcessor`] used to handle input values. - pub(crate) processor: AxisProcessor, -} - -impl MouseScrollAxis { - /// Creates a [`MouseScrollAxis`] for continuous movement on the X-axis without any processing applied. - pub const fn analog_x() -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseScrollAxis`] for continuous movement on the Y-axis without any processing applied. - pub const fn analog_y() -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseScrollAxis`] for discrete movement on the X-axis without any processing applied. - pub const fn digital_x() -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - } - } - - /// Creates a [`MouseScrollAxis`] for discrete movement on the Y-axis without any processing applied. - pub const fn digital_y() -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - } - } - /// Creates a [`MouseScrollAxis`] for continuous movement on the X-axis with the specified [`AxisProcessor`]. - pub fn analog_x_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseScrollAxis`] for continuous movement on the Y-axis with the specified [`AxisProcessor`]. - pub fn analog_y_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseScrollAxis`] for discrete movement on the X-axis with the specified [`AxisProcessor`]. - pub fn digital_x_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::X, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Creates a [`MouseScrollAxis`] for discrete movement on the Y-axis with the specified [`AxisProcessor`]. - pub fn digital_y_using(processor: impl Into) -> Self { - Self { - axis: DualAxis::Y, - input_mode: AxisInputMode::Analog, - processor: processor.into(), - } - } - - /// Appends the given [`AxisProcessor`] as the next processing step. - #[inline] - pub fn with_processor(mut self, processor: impl Into) -> Self { - self.processor = self.processor.with_processor(processor); - self - } - - /// Replaces the current [`AxisProcessor`] with the specified `processor`. - #[inline] - pub fn replace_processor(mut self, processor: impl Into) -> Self { - self.processor = processor.into(); - self - } - - /// Removes the current used [`AxisProcessor`]. - #[inline] - pub fn no_processor(mut self) -> Self { - self.processor = AxisProcessor::None; - self - } -} - -#[serde_typetag] -impl UserInput for MouseScrollAxis { - /// Checks if there is any recent mouse wheel movement along the specified axis. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_wheel_movement(input_streams); - let value = self.axis.value(movement); - self.processor.process(value) != 0.0 - } - - /// Retrieves the amount of the mouse wheel movement along the specified axis - /// after processing by the associated [`AxisProcessor`]. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - let movement = accumulate_wheel_movement(input_streams); - let value = self.axis.value(movement); - let value = self.input_mode.axis_value(value); - self.processor.process(value) - } -} - -/// Input associated with mouse wheel movement on a specific direction, treated as a button press. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[must_use] -pub struct MouseScrollDirection(DualAxisDirection); - -impl MouseScrollDirection { - /// Movement in the upward direction. - const UP: Self = Self(DualAxisDirection::Up); - - /// Movement in the downward direction. - const DOWN: Self = Self(DualAxisDirection::Down); - - /// Movement in the leftward direction. - const LEFT: Self = Self(DualAxisDirection::Left); - - /// Movement in the rightward direction. - const RIGHT: Self = Self(DualAxisDirection::Right); -} - -#[serde_typetag] -impl UserInput for MouseMoveDirection { - /// Checks if there is any recent mouse wheel movement along the specified direction. - #[must_use] - #[inline] - fn is_active(&self, input_streams: &InputStreams) -> bool { - let movement = accumulate_wheel_movement(input_streams); - self.0.is_active(movement) - } - - /// Retrieves the magnitude of the mouse wheel movement along the specified direction, - /// returning `0.0` for no movement and `1.0` for a currently active direction. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.is_active(input_streams)) - } -} diff --git a/tests/clashes.rs b/tests/clashes.rs index f98db53b..92e5a3e3 100644 --- a/tests/clashes.rs +++ b/tests/clashes.rs @@ -50,12 +50,18 @@ fn spawn_input_map(mut commands: Commands) { input_map.insert(One, Digit1); input_map.insert(Two, Digit2); - input_map.insert_chord(OneAndTwo, [Digit1, Digit2]); - input_map.insert_chord(TwoAndThree, [Digit2, Digit3]); - input_map.insert_chord(OneAndTwoAndThree, [Digit1, Digit2, Digit3]); - input_map.insert_chord(CtrlOne, [ControlLeft, Digit1]); - input_map.insert_chord(AltOne, [AltLeft, Digit1]); - input_map.insert_chord(CtrlAltOne, [ControlLeft, AltLeft, Digit1]); + input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); + input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); + input_map.insert( + OneAndTwoAndThree, + InputChord::from_multiple([Digit1, Digit2, Digit3]), + ); + input_map.insert(CtrlOne, InputChord::from_multiple([ControlLeft, Digit1])); + input_map.insert(AltOne, InputChord::from_multiple([AltLeft, Digit1])); + input_map.insert( + CtrlAltOne, + InputChord::from_multiple([ControlLeft, AltLeft, Digit1]), + ); commands.spawn(input_map); } @@ -111,8 +117,8 @@ fn two_inputs_clash_handling() { let mut app = test_app(); // Two inputs - app.send_input(Digit1); - app.send_input(Digit2); + app.press_input(Digit1); + app.press_input(Digit2); app.update(); app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, Two, OneAndTwo]); @@ -128,9 +134,9 @@ fn three_inputs_clash_handling() { // Three inputs app.reset_inputs(); - app.send_input(Digit1); - app.send_input(Digit2); - app.send_input(Digit3); + app.press_input(Digit1); + app.press_input(Digit2); + app.press_input(Digit3); app.update(); app.assert_input_map_actions_eq( @@ -149,10 +155,10 @@ fn modifier_clash_handling() { // Modifier app.reset_inputs(); - app.send_input(Digit1); - app.send_input(Digit2); - app.send_input(Digit3); - app.send_input(ControlLeft); + app.press_input(Digit1); + app.press_input(Digit2); + app.press_input(Digit3); + app.press_input(ControlLeft); app.update(); app.assert_input_map_actions_eq( @@ -174,9 +180,9 @@ fn multiple_modifiers_clash_handling() { // Multiple modifiers app.reset_inputs(); - app.send_input(Digit1); - app.send_input(ControlLeft); - app.send_input(AltLeft); + app.press_input(Digit1); + app.press_input(ControlLeft); + app.press_input(AltLeft); app.update(); app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, CtrlOne, AltOne, CtrlAltOne]); @@ -192,8 +198,8 @@ fn action_order_clash_handling() { // Action order app.reset_inputs(); - app.send_input(Digit3); - app.send_input(Digit2); + app.press_input(Digit3); + app.press_input(Digit2); app.update(); app.assert_input_map_actions_eq(ClashStrategy::PressAll, [Two, TwoAndThree]); diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs index 831e90ef..4e555aff 100644 --- a/tests/gamepad_axis.rs +++ b/tests/gamepad_axis.rs @@ -3,7 +3,7 @@ use bevy::input::gamepad::{ }; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData}; +use leafwing_input_manager::axislike::DualAxisData; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] @@ -53,7 +53,7 @@ fn raw_gamepad_axis_events() { let mut app = test_app(); app.insert_resource(InputMap::new([( ButtonlikeTestAction::Up, - SingleAxis::new(GamepadAxisType::RightStickX).with_processor(AxisDeadZone::default()), + GamepadControlAxis::RIGHT_X.with_deadzone_symmetric(0.1), )])); let mut events = app.world.resource_mut::>(); @@ -75,13 +75,9 @@ fn game_pad_single_axis_mocking() { let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(-1.), - processor: AxisProcessor::None, - }; + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [-1.0]); - app.send_input(input); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 1); } @@ -93,13 +89,9 @@ fn game_pad_dual_axis_mocking() { let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = DualAxis { - x_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - y_axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - processor: CircleDeadZone::default().into(), - value: Some(Vec2::X), - }; - app.send_input(input); + let input = GamepadStick::LEFT; + app.send_axis_values(input, [1.0, 0.0]); + let mut events = app.world.resource_mut::>(); // Dual axis events are split out assert_eq!(events.drain().count(), 2); @@ -111,88 +103,60 @@ fn game_pad_single_axis() { app.insert_resource(InputMap::new([ ( AxislikeTestAction::X, - SingleAxis::new(GamepadAxisType::LeftStickX).with_processor(AxisDeadZone::default()), + GamepadControlAxis::LEFT_X.with_deadzone_symmetric(0.1), ), ( AxislikeTestAction::Y, - SingleAxis::new(GamepadAxisType::LeftStickY).with_processor(AxisDeadZone::default()), + GamepadControlAxis::LEFT_Y.with_deadzone_symmetric(0.1), ), ])); // +X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // -X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // +Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // -Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 // Usually a small deadzone threshold will be set - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(0.0), - processor: AxisDeadZone::default().into(), - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, [0.0]); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); - // None - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: None, - processor: AxisProcessor::None, - }; - app.send_input(input); + // No value + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, []); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); // Scaled value - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(0.2), - processor: AxisDeadZone::default().into(), - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [0.2]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); @@ -205,61 +169,45 @@ fn game_pad_single_axis_inverted() { app.insert_resource(InputMap::new([ ( AxislikeTestAction::X, - SingleAxis::new(GamepadAxisType::LeftStickX) - .with_processor(AxisDeadZone::default()) - .with_processor(AxisProcessor::Inverted), + GamepadControlAxis::LEFT_X + .with_deadzone_symmetric(0.1) + .inverted(), ), ( AxislikeTestAction::Y, - SingleAxis::new(GamepadAxisType::LeftStickY) - .with_processor(AxisDeadZone::default()) - .with_processor(AxisProcessor::Inverted), + GamepadControlAxis::LEFT_Y + .with_deadzone_symmetric(0.1) + .inverted(), ), ])); // +X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(1.), - processor: AxisProcessor::Inverted, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); assert_eq!(action_state.value(&AxislikeTestAction::X), -1.0); // -X - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_X; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); assert_eq!(action_state.value(&AxislikeTestAction::X), 1.0); // +Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); assert_eq!(action_state.value(&AxislikeTestAction::Y), -1.0); // -Y - let input = SingleAxis { - axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = GamepadControlAxis::LEFT_Y; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); @@ -271,17 +219,12 @@ fn game_pad_dual_axis_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().replace_processor(DualAxisDeadZone::default()), + GamepadStick::LEFT.with_deadzone_symmetric(0.1), )])); // Test that an input inside the dual-axis deadzone is filtered out. - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.04, - 0.1, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.04, 0.1]); app.update(); let action_state = app.world.resource::>(); @@ -289,17 +232,12 @@ fn game_pad_dual_axis_deadzone() { assert_eq!(action_state.value(&AxislikeTestAction::XY), 0.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) + DualAxisData::ZERO ); // Test that an input outside the dual-axis deadzone is not filtered out. - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 1.0, - 0.2, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [1.0, 0.2]); app.update(); let action_state = app.world.resource::>(); @@ -311,13 +249,8 @@ fn game_pad_dual_axis_deadzone() { ); // Test that each axis of the dual-axis deadzone is filtered independently. - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.8, - 0.1, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.8, 0.1]); app.update(); let action_state = app.world.resource::>(); @@ -334,17 +267,12 @@ fn game_pad_circle_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().replace_processor(CircleDeadZone::default()), + GamepadStick::LEFT.with_circle_deadzone(0.1), )])); // Test that an input inside the circle deadzone is filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.06, - 0.06, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.06, 0.06]); app.update(); let action_state = app.world.resource::>(); @@ -352,17 +280,12 @@ fn game_pad_circle_deadzone() { assert_eq!(action_state.value(&AxislikeTestAction::XY), 0.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) + DualAxisData::ZERO ); // Test that an input outside the circle deadzone is not filtered out, assuming values of 0.1 - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.2, - 0.0, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.2, 0.0]); app.update(); let action_state = app.world.resource::>(); @@ -379,17 +302,12 @@ fn test_zero_dual_axis_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().replace_processor(DualAxisDeadZone::ZERO), + GamepadStick::LEFT.with_deadzone_symmetric(0.0), )])); // Test that an input of zero will be `None` even with no deadzone. - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.0, - 0.0, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.0, 0.0]); app.update(); let action_state = app.world.resource::>(); @@ -397,7 +315,7 @@ fn test_zero_dual_axis_deadzone() { assert_eq!(action_state.value(&AxislikeTestAction::XY), 0.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) + DualAxisData::ZERO ); } @@ -406,17 +324,12 @@ fn test_zero_circle_deadzone() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - DualAxis::left_stick().replace_processor(CircleDeadZone::ZERO), + GamepadStick::LEFT.with_circle_deadzone(0.0), )])); // Test that an input of zero will be `None` even with no deadzone. - app.send_input(DualAxis::from_value( - GamepadAxisType::LeftStickX, - GamepadAxisType::LeftStickY, - 0.0, - 0.0, - )); - + let input = GamepadStick::LEFT; + app.send_axis_values(input, [0.0, 0.0]); app.update(); let action_state = app.world.resource::>(); @@ -424,30 +337,30 @@ fn test_zero_circle_deadzone() { assert_eq!(action_state.value(&AxislikeTestAction::XY), 0.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - DualAxisData::new(0.0, 0.0) + DualAxisData::ZERO ); } #[test] -#[ignore = "Input mocking is subtly broken: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/407"] +#[ignore = "Input mocking is subtly broken: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/516"] fn game_pad_virtual_dpad() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - VirtualDPad::dpad(), + GamepadVirtualDPad::DPAD, )])); - app.send_input(GamepadButtonType::DPadLeft); + app.press_input(GamepadButtonType::DPadLeft); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::XY)); - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad assert_eq!(action_state.value(&AxislikeTestAction::XY), 1.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad DualAxisData::new(-1.0, 0.0) ); } diff --git a/tests/integration.rs b/tests/integration.rs index a16ccac7..0054f9e5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -71,7 +71,7 @@ fn disable_input() { .add_systems(PreUpdate, respect_fades); // Press F to pay respects - app.send_input(KeyCode::KeyF); + app.press_input(KeyCode::KeyF); app.update(); let respect = app.world.resource::(); assert_eq!(*respect, Respect(true)); @@ -86,7 +86,7 @@ fn disable_input() { assert_eq!(*respect, Respect(false)); // And even pressing F cannot bring it back - app.send_input(KeyCode::KeyF); + app.press_input(KeyCode::KeyF); app.update(); let respect = app.world.resource::(); assert_eq!(*respect, Respect(false)); @@ -113,7 +113,7 @@ fn release_when_input_map_removed() { .add_systems(PreUpdate, respect_fades); // Press F to pay respects - app.send_input(KeyCode::KeyF); + app.press_input(KeyCode::KeyF); app.update(); let respect = app.world.resource::(); assert_eq!(*respect, Respect(true)); @@ -129,7 +129,7 @@ fn release_when_input_map_removed() { assert_eq!(*respect, Respect(false)); // And even pressing F cannot bring it back - app.send_input(KeyCode::KeyF); + app.press_input(KeyCode::KeyF); app.update(); let respect = app.world.resource::(); assert_eq!(*respect, Respect(false)); @@ -243,7 +243,7 @@ fn duration() { app.update(); // Press - app.send_input(KeyCode::KeyF); + app.press_input(KeyCode::KeyF); // Hold std::thread::sleep(2 * RESPECTFUL_DURATION); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 1638d8ce..39104f8e 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -1,8 +1,7 @@ use bevy::input::mouse::MouseMotion; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData, MouseMotionAxisType}; -use leafwing_input_manager::buttonlike::MouseMotionDirection; +use leafwing_input_manager::axislike::DualAxisData; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] @@ -44,12 +43,9 @@ fn test_app() -> App { } #[test] -fn raw_mouse_motion_events() { +fn raw_mouse_move_events() { let mut app = test_app(); - app.insert_resource(InputMap::new([( - AxislikeTestAction::X, - SingleAxis::from_value(AxisType::MouseMotion(MouseMotionAxisType::Y), 1.0), - )])); + app.insert_resource(InputMap::new([(AxislikeTestAction::X, MouseMoveAxis::Y)])); let mut events = app.world.resource_mut::>(); events.send(MouseMotion { @@ -62,68 +58,63 @@ fn raw_mouse_motion_events() { } #[test] -fn mouse_motion_discrete_mocking() { +fn mouse_move_discrete_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - app.send_input(MouseMotionDirection::Up); + app.press_input(MouseMoveDirection::UP); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 1); } #[test] -fn mouse_motion_single_axis_mocking() { +fn mouse_move_single_axis_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - value: Some(-1.), - processor: AxisProcessor::None, - }; + let input = MouseMoveAxis::X; + app.send_axis_values(input, [-1.0]); - app.send_input(input); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 1); } #[test] -fn mouse_motion_dual_axis_mocking() { +fn mouse_move_dual_axis_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = DualAxis { - x_axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - y_axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - processor: DualAxisProcessor::None, - value: Some(Vec2::X), - }; - app.send_input(input); + let input = MouseMove::RAW; + app.send_axis_values(input, [1.0, 0.0]); + let mut events = app.world.resource_mut::>(); // Dual axis events are split out assert_eq!(events.drain().count(), 2); } #[test] -fn mouse_motion_buttonlike() { +fn mouse_move_buttonlike() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (ButtonlikeTestAction::Up, MouseMotionDirection::Up), - (ButtonlikeTestAction::Down, MouseMotionDirection::Down), - (ButtonlikeTestAction::Left, MouseMotionDirection::Left), - (ButtonlikeTestAction::Right, MouseMotionDirection::Right), + (ButtonlikeTestAction::Up, MouseMoveDirection::UP), + (ButtonlikeTestAction::Down, MouseMoveDirection::DOWN), + (ButtonlikeTestAction::Left, MouseMoveDirection::LEFT), + (ButtonlikeTestAction::Right, MouseMoveDirection::RIGHT), ])); for action in ButtonlikeTestAction::variants() { let input_map = app.world.resource::>(); // Get the first associated input let input = input_map.get(action).unwrap().first().unwrap().clone(); + let direction = Reflect::as_any(&input) + .downcast_ref::() + .unwrap(); - app.send_input(input.clone()); + app.press_input(*direction); app.update(); let action_state = app.world.resource::>(); @@ -132,17 +123,17 @@ fn mouse_motion_buttonlike() { } #[test] -fn mouse_motion_buttonlike_cancels() { +fn mouse_move_buttonlike_cancels() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (ButtonlikeTestAction::Up, MouseMotionDirection::Up), - (ButtonlikeTestAction::Down, MouseMotionDirection::Down), - (ButtonlikeTestAction::Left, MouseMotionDirection::Left), - (ButtonlikeTestAction::Right, MouseMotionDirection::Right), + (ButtonlikeTestAction::Up, MouseMoveDirection::UP), + (ButtonlikeTestAction::Down, MouseMoveDirection::DOWN), + (ButtonlikeTestAction::Left, MouseMoveDirection::LEFT), + (ButtonlikeTestAction::Right, MouseMoveDirection::RIGHT), ])); - app.send_input(MouseMotionDirection::Up); - app.send_input(MouseMotionDirection::Down); + app.press_input(MouseMoveDirection::UP); + app.press_input(MouseMoveDirection::DOWN); // Correctly flushes the world app.update(); @@ -154,96 +145,63 @@ fn mouse_motion_buttonlike_cancels() { } #[test] -fn mouse_motion_single_axis() { +fn mouse_move_single_axis() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (AxislikeTestAction::X, SingleAxis::mouse_motion_x()), - (AxislikeTestAction::Y, SingleAxis::mouse_motion_y()), + (AxislikeTestAction::X, MouseMoveAxis::X), + (AxislikeTestAction::Y, MouseMoveAxis::Y), ])); // +X - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseMoveAxis::X; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // -X - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::X), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseMoveAxis::X; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // +Y - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseMoveAxis::Y; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // -Y - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseMoveAxis::Y; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 - // Usually a small deadzone threshold will be set - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - value: Some(0.0), - processor: AxisDeadZone::default().into(), - }; - app.send_input(input); + let input = MouseMoveAxis::Y; + app.send_axis_values(input, [0.0]); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); - // None - let input = SingleAxis { - axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y), - value: None, - processor: AxisProcessor::None, - }; - app.send_input(input); + // No value + let input = MouseMoveAxis::Y; + app.send_axis_values(input, []); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); } #[test] -fn mouse_motion_dual_axis() { +fn mouse_move_dual_axis() { let mut app = test_app(); - app.insert_resource(InputMap::new([( - AxislikeTestAction::XY, - DualAxis::mouse_motion(), - )])); - - app.send_input(DualAxis::from_value( - MouseMotionAxisType::X, - MouseMotionAxisType::Y, - 5.0, - 0.0, - )); + app.insert_resource(InputMap::new([(AxislikeTestAction::XY, MouseMove::RAW)])); + let input = MouseMove::RAW; + app.send_axis_values(input, [5.0, 0.0]); app.update(); let action_state = app.world.resource::>(); @@ -257,29 +215,25 @@ fn mouse_motion_dual_axis() { } #[test] -fn mouse_motion_virtual_dpad() { +fn mouse_move_discrete() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - VirtualDPad::mouse_motion(), + MouseMove::DIGITAL, )])); - app.send_input(DualAxis::from_value( - MouseMotionAxisType::X, - MouseMotionAxisType::Y, - 0.0, - -2.0, - )); + let input = MouseMove::RAW; + app.send_axis_values(input, [0.0, -2.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::XY)); - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad assert_eq!(action_state.value(&AxislikeTestAction::XY), 1.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad DualAxisData::new(0.0, -1.0) ); } @@ -290,23 +244,16 @@ fn mouse_drag() { let mut input_map = InputMap::default(); - input_map.insert_chord( + input_map.insert( AxislikeTestAction::XY, - [ - InputKind::from(DualAxis::mouse_motion()), - InputKind::from(MouseButton::Right), - ], + InputChord::from_single(MouseMove::RAW).with(MouseButton::Right), ); app.insert_resource(input_map); - app.send_input(DualAxis::from_value( - MouseMotionAxisType::X, - MouseMotionAxisType::Y, - 5.0, - 0.0, - )); - app.send_input(MouseButton::Right); + let input = MouseMove::RAW; + app.send_axis_values(input, [5.0, 0.0]); + app.press_input(MouseButton::Right); app.update(); let action_state = app.world.resource::>(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index 03d243e8..1e660c22 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -1,7 +1,7 @@ use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; use bevy::input::InputPlugin; use bevy::prelude::*; -use leafwing_input_manager::axislike::{AxisType, DualAxisData}; +use leafwing_input_manager::axislike::DualAxisData; use leafwing_input_manager::prelude::*; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] @@ -38,11 +38,11 @@ fn test_app() -> App { } #[test] -fn raw_mouse_wheel_events() { +fn raw_mouse_scroll_events() { let mut app = test_app(); app.insert_resource(InputMap::new([( ButtonlikeTestAction::Up, - MouseWheelDirection::Up, + MouseScrollDirection::UP, )])); let mut events = app.world.resource_mut::>(); @@ -59,68 +59,63 @@ fn raw_mouse_wheel_events() { } #[test] -fn mouse_wheel_discrete_mocking() { +fn mouse_scroll_discrete_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - app.send_input(MouseWheelDirection::Up); + app.press_input(MouseScrollDirection::UP); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 1); } #[test] -fn mouse_wheel_single_axis_mocking() { +fn mouse_scroll_single_axis_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - value: Some(-1.), - processor: AxisProcessor::None, - }; + let input = MouseScrollAxis::X; + app.send_axis_values(input, [-1.0]); - app.send_input(input); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 1); } #[test] -fn mouse_wheel_dual_axis_mocking() { +fn mouse_scroll_dual_axis_mocking() { let mut app = test_app(); let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = DualAxis { - x_axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - y_axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - processor: DualAxisProcessor::None, - value: Some(Vec2::X), - }; - app.send_input(input); + let input = MouseScroll::RAW; + app.send_axis_values(input, [-1.0]); + let mut events = app.world.resource_mut::>(); // Dual axis events are split out assert_eq!(events.drain().count(), 2); } #[test] -fn mouse_wheel_buttonlike() { +fn mouse_scroll_buttonlike() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (ButtonlikeTestAction::Up, MouseWheelDirection::Up), - (ButtonlikeTestAction::Down, MouseWheelDirection::Down), - (ButtonlikeTestAction::Left, MouseWheelDirection::Left), - (ButtonlikeTestAction::Right, MouseWheelDirection::Right), + (ButtonlikeTestAction::Up, MouseScrollDirection::UP), + (ButtonlikeTestAction::Down, MouseScrollDirection::DOWN), + (ButtonlikeTestAction::Left, MouseScrollDirection::LEFT), + (ButtonlikeTestAction::Right, MouseScrollDirection::RIGHT), ])); for action in ButtonlikeTestAction::variants() { let input_map = app.world.resource::>(); // Get the first associated input let input = input_map.get(action).unwrap().first().unwrap().clone(); + let direction = Reflect::as_any(&input) + .downcast_ref::() + .unwrap(); - app.send_input(input.clone()); + app.press_input(*direction); app.update(); let action_state = app.world.resource::>(); @@ -129,17 +124,17 @@ fn mouse_wheel_buttonlike() { } #[test] -fn mouse_wheel_buttonlike_cancels() { +fn mouse_scroll_buttonlike_cancels() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (ButtonlikeTestAction::Up, MouseWheelDirection::Up), - (ButtonlikeTestAction::Down, MouseWheelDirection::Down), - (ButtonlikeTestAction::Left, MouseWheelDirection::Left), - (ButtonlikeTestAction::Right, MouseWheelDirection::Right), + (ButtonlikeTestAction::Up, MouseScrollDirection::UP), + (ButtonlikeTestAction::Down, MouseScrollDirection::DOWN), + (ButtonlikeTestAction::Left, MouseScrollDirection::LEFT), + (ButtonlikeTestAction::Right, MouseScrollDirection::RIGHT), ])); - app.send_input(MouseWheelDirection::Up); - app.send_input(MouseWheelDirection::Down); + app.press_input(MouseScrollDirection::UP); + app.press_input(MouseScrollDirection::DOWN); // Correctly flushes the world app.update(); @@ -151,96 +146,63 @@ fn mouse_wheel_buttonlike_cancels() { } #[test] -fn mouse_wheel_single_axis() { +fn mouse_scroll_single_axis() { let mut app = test_app(); app.insert_resource(InputMap::new([ - (AxislikeTestAction::X, SingleAxis::mouse_wheel_x()), - (AxislikeTestAction::Y, SingleAxis::mouse_wheel_y()), + (AxislikeTestAction::X, MouseScrollAxis::X), + (AxislikeTestAction::Y, MouseScrollAxis::Y), ])); // +X - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseScrollAxis::X; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // -X - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::X), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseScrollAxis::X; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::X)); // +Y - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - value: Some(1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseScrollAxis::Y; + app.send_axis_values(input, [1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // -Y - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - value: Some(-1.), - processor: AxisProcessor::None, - }; - app.send_input(input); + let input = MouseScrollAxis::Y; + app.send_axis_values(input, [-1.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::Y)); // 0 - // Usually a small deadzone threshold will be set - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - value: Some(0.0), - processor: AxisDeadZone::default().into(), - }; - app.send_input(input); + let input = MouseScrollAxis::Y; + app.send_axis_values(input, [0.0]); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); - // None - let input = SingleAxis { - axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y), - value: None, - processor: AxisProcessor::None, - }; - app.send_input(input); + // No value + let input = MouseScrollAxis::Y; + app.send_axis_values(input, []); app.update(); let action_state = app.world.resource::>(); assert!(!action_state.pressed(&AxislikeTestAction::Y)); } #[test] -fn mouse_wheel_dual_axis() { +fn mouse_scroll_dual_axis() { let mut app = test_app(); - app.insert_resource(InputMap::new([( - AxislikeTestAction::XY, - DualAxis::mouse_wheel(), - )])); - - app.send_input(DualAxis::from_value( - MouseWheelAxisType::X, - MouseWheelAxisType::Y, - 5.0, - 0.0, - )); + app.insert_resource(InputMap::new([(AxislikeTestAction::XY, MouseScroll::RAW)])); + let input = MouseScroll::RAW; + app.send_axis_values(input, [5.0, 0.0]); app.update(); let action_state = app.world.resource::>(); @@ -254,29 +216,25 @@ fn mouse_wheel_dual_axis() { } #[test] -fn mouse_wheel_virtualdpad() { +fn mouse_scroll_discrete() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - VirtualDPad::mouse_wheel(), + MouseScroll::DIGITAL, )])); - app.send_input(DualAxis::from_value( - MouseWheelAxisType::X, - MouseWheelAxisType::Y, - 0.0, - -2.0, - )); + let input = MouseScroll::RAW; + app.send_axis_values(input, [0.0, -2.0]); app.update(); let action_state = app.world.resource::>(); assert!(action_state.pressed(&AxislikeTestAction::XY)); - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad assert_eq!(action_state.value(&AxislikeTestAction::XY), 1.0); assert_eq!( action_state.axis_pair(&AxislikeTestAction::XY).unwrap(), - // This should be a unit length, because we're working with a VirtualDpad + // This should be a unit length, because we're working with a VirtualDPad DualAxisData::new(0.0, -1.0) ); } From 5f0d52881718821c215c39b32826dd2dcbccb92e Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 14:16:14 +0800 Subject: [PATCH 10/20] RELEASES.md --- RELEASES.md | 99 ++++++++++++++++++++----- examples/input_processing.rs | 6 +- src/clashing_inputs.rs | 14 ++-- src/input_mocking.rs | 6 +- src/input_processing/dual_axis/mod.rs | 10 +-- src/input_processing/single_axis/mod.rs | 10 +-- src/lib.rs | 1 + src/plugin.rs | 2 +- src/{user_input => }/raw_inputs.rs | 2 +- src/user_input/chord.rs | 37 +++++---- src/user_input/gamepad.rs | 83 +++++++++++---------- src/user_input/keyboard.rs | 94 +++++++++++++---------- src/user_input/mod.rs | 23 +++--- src/user_input/mouse.rs | 78 ++++++++++--------- tests/mouse_motion.rs | 2 +- tests/mouse_wheel.rs | 4 +- 16 files changed, 280 insertions(+), 191 deletions(-) rename src/{user_input => }/raw_inputs.rs (99%) diff --git a/RELEASES.md b/RELEASES.md index 75ac970c..63434e71 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,21 +4,82 @@ ### Breaking Changes +- removed `UserInput` enum in favor of the new `UserInput` trait and its impls (see 'Enhancements: New Inputs' for details). + - renamed `Modifier` enum to `ModifierKey`. + - refactored `InputKind` variants for representing user inputs, it now represents the kind of data an input can provide (e.g., button-like input, axis-like input). + - by default, all input events are unprocessed now, using `With*ProcessingPipelineExt` methods to define your preferred processing steps. + - applied clashing check to continuous mouse inputs, for example: + - `MouseScrollAxis::Y` will clash with `MouseScrollDirection::UP` and `MouseScrollDirection::DOWN`. + - `MouseMove` will clash with all the two axes and the four directions. +- removed `InputMap::insert_chord` and `InputMap::insert_modified` methods since they’re a little bit +- removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). +- renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. +- removed `DeadZoneShape` in favor of new dead zone processors (see 'Enhancements: Input Processors' for details). +- refactored the method signatures of `InputMap` to fit the new input types. +- refactored the fields and methods of `RawInputs` to fit the new input types. - removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. -- replaced axis-like input handling with new input processors (see 'Enhancements: Input Processors' for details). - - removed `DeadZoneShape` in favor of new dead zone processors. +- split `MockInput::send_input` method to two methods: + - `fn press_input(&self, input: impl UserInput)` for focusing on simulating button and key presses. + - `fn send_axis_values(&self, input: impl UserInput, values: impl IntoIterator)` for sending value changed events to each axis of the input. - made the dependency on bevy's `bevy_gilrs` feature optional. - it is still enabled by leafwing-input-manager's default features. - if you're using leafwing-input-manager with `default_features = false`, you can readd it by adding `bevy/bevy_gilrs` as a dependency. -- removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). -- renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. -- split `MockInput::send_input` method to two methods: - - `fn press_input(&self, input: UserInput)` for focusing on simulating button and key presses. - - `fn send_axis_values(&self, input: UserInput, values: impl IntoIterator)` for sending value changed events to each axis of the input. -- removed the hacky `value` field and `from_value` method from `SingleAxis` and `DualAxis`, in favor of new input mocking. ### Enhancements +#### New Inputs + +- added `UserInput` trait. +- added `UserInput` impls for gamepad input events: + - implemented `UserInput` for Bevy’s `GamepadAxisType`-related inputs. + - `GamepadStick`: Continuous or discrete movement events of the left or right gamepad stick along both X and Y axes. + - `GamepadControlAxis`: Continuous or discrete movement events of a `GamepadAxisType`. + - `GamepadControlDirection`: Discrete movement direction events of a `GamepadAxisType`, treated as a button press. + - implemented `UserInput` for Bevy’s `GamepadButtonType` directly. + - added `GamepadVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `GamepadButtonType`s. + - added `GamepadVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `GamepadButtonType`s. +- added `UserInput` impls for keyboard inputs: + - implemented `UserInput` for Bevy’s `KeyCode` directly. + - implemented `UserInput` for `ModifierKey`. + - added `KeyboardKey` enum, including individual `KeyCode`s, `ModifierKey`s, and checks for any key press. + - added `KeyboardVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `KeyboardKey`s. + - added `KeyboardVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `KeyboardKey`s. +- added `UserInput` impls for mouse inputs: + - implemented `UserInput` for movement-related inputs. + - `MouseMove`: Continuous or discrete movement events of the mouse both X and Y axes. + - `MouseMoveAxis`: Continuous or discrete movement events of the mouse on an axis, similar to the old `SingleAxis::mouse_motion_*`. + - `MouseMoveDirection`: Discrete movement direction events of the mouse on an axis, similar to the old `MouseMotionDirection`. + - implemented `UserInput` for wheel-related inputs. + - `MouseScroll`: Continuous or discrete movement events of the mouse wheel both X and Y axes. + - `MouseScrollAxis`: Continuous or discrete movement events of the mouse wheel on an axis, similar to the old `SingleAxis::mouse_wheel_*`. + - `MouseScrollDirection`: Discrete movement direction events of the mouse wheel on an axis, similar to the old `MouseWheelDirection`. +- added `InputChord` for combining multiple inputs, similar to the old `UserInput::Chord`. + +##### Migration Guide + +- the old `SingleAxis` is now: + - `GamepadControlAxis` for gamepad axes. + - `MouseMoveAxis::X` and `MouseMoveAxis::Y` for continuous mouse movement. + - `MouseScrollAxis::X` and `MouseScrollAxis::Y` for continuous mouse wheel movement. +- the old `DualAxis` is now: + - `GamepadStick` for gamepad sticks. + - `MouseMove::RAW` for continuous mouse movement. + - `MouseScroll::RAW` for continuous mouse wheel movement. +- the old `Modifier` is now `ModifierKey`. +- the old `MouseMotionDirection` is now `MouseMoveDirection`. +- the old `MouseWheelDirection` is now `MouseScrollDirection`. +- the old `UserInput::Chord` is now `InputChord`. +- the old `UserInput::VirtualAxis` is now: + - `GamepadVirtualAxis` for four gamepad buttons. + - `KeyboardVirtualAxis` for four keys. + - `MouseMoveAxis::X_DIGITAL` and `MouseMoveAxis::X_DIGITAL` for discrete mouse movement. + - `MouseScrollAxis::X_DIGITAL` and `MouseScrollAxis::Y_DIGITAL` for discrete mouse wheel movement. +- the old `UserInput::VirtualDPad` is now: + - `GamepadVirtualDPad` for four gamepad buttons. + - `KeyboardVirtualDPad` for four keys. + - `MouseMove::DIGITAL` for discrete mouse movement. + - `MouseScroll::DIGITAL` for discrete mouse wheel movement. + #### Input Processors Input processors allow you to create custom logic for axis-like input manipulation. @@ -29,8 +90,6 @@ Input processors allow you to create custom logic for axis-like input manipulati - added processor traits for defining custom processors: - `CustomAxisProcessor`: Handles single-axis values. - `CustomDualAxisProcessor`: Handles dual-axis values. -- implemented `WithAxisProcessorExt` to manage processors for `SingleAxis` and `VirtualAxis`. -- implemented `WithDualAxisProcessorExt` to manage processors for `DualAxis` and `VirtualDpad`. - added App extensions: `register_axis_processor` and `register_dual_axis_processor` for registration of processors. - added built-in processors (variants of processor enums and `Into` implementors): - Pipelines: Handle input values sequentially through a sequence of processors. @@ -58,22 +117,22 @@ Input processors allow you to create custom logic for axis-like input manipulati - `AxisDeadZone`: Normalizes single-axis values based on `AxisExclusion` and `AxisBounds::default`, implemented `Into` and `Into`. - `DualAxisDeadZone`: Normalizes dual-axis values based on `DualAxisExclusion` and `DualAxisBounds::default`, implemented `Into`. - `CircleDeadZone`: Normalizes dual-axis values based on `CircleExclusion` and `CircleBounds::default`, implemented `Into`. +- implemented `WithAxisProcessingPipelineExt` to manage processors for `SingleAxis` and `VirtualAxis`, integrating the common processing configuration. +- implemented `WithDualAxisProcessingPipelineExt` to manage processors for `DualAxis` and `VirtualDpad`, integrating the common processing configuration. ### Usability #### InputMap -Introduce new fluent builders for creating a new `InputMap` with short configurations: - -- `fn with(mut self, action: A, input: impl Into)`. -- `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. -- `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. -- `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`. - -Introduce new iterators over `InputMap`: +- added new fluent builders for creating a new `InputMap` with short configurations: + - `fn with(mut self, action: A, input: impl UserInput)`. + - `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. + - `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. + - `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`. -- `bindings(&self) -> impl Iterator` for iterating over all registered action-input bindings. -- `actions(&self) -> impl Iterator` for iterating over all registered actions. +- added new iterators over `InputMap`: + - `bindings(&self) -> impl Iterator` for iterating over all registered action-input bindings. + - `actions(&self) -> impl Iterator` for iterating over all registered actions. ### Bugs diff --git a/examples/input_processing.rs b/examples/input_processing.rs index f542e14b..b1916810 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -38,9 +38,9 @@ fn spawn_player(mut commands: Commands) { Action::Move, GamepadStick::LEFT // You can replace the currently used pipeline with another processor. - .replace_processor(CircleDeadZone::default()) - // Or remove the pipeline directly, leaving no any processing applied. - .no_processor(), + .replace_processing_pipeline(CircleDeadZone::default()) + // Or reset the pipeline directly, leaving no any processing applied. + .reset_processing_pipeline(), ) .with( Action::LookAround, diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 70fee141..073736fc 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -57,10 +57,14 @@ fn check_clashed(lhs: &dyn UserInput, rhs: &dyn UserInput) -> bool { list_a.len() > 1 && list_b.iter().all(|a| list_a.contains(a)) } - let lhs_inners = lhs.destructure(); - let rhs_inners = rhs.destructure(); + let lhs_inners = lhs.to_clashing_checker(); + let rhs_inners = rhs.to_clashing_checker(); - clash(&lhs_inners, &rhs_inners) || clash(&rhs_inners, &lhs_inners) + let res = clash(&lhs_inners, &rhs_inners) || clash(&rhs_inners, &lhs_inners); + + dbg!(lhs_inners, rhs_inners, res); + + res } impl InputMap { @@ -240,13 +244,13 @@ fn resolve_clash( ClashStrategy::PrioritizeLongest => { let longest_a: usize = reasons_a_is_pressed .iter() - .map(|input| input.destructure().len()) + .map(|input| input.to_clashing_checker().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); let longest_b: usize = reasons_b_is_pressed .iter() - .map(|input| input.destructure().len()) + .map(|input| input.to_clashing_checker().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); diff --git a/src/input_mocking.rs b/src/input_mocking.rs index c44ede4a..9f784e88 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -218,7 +218,7 @@ impl MockInput for MutableInputStreams<'_> { } fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { - let raw_inputs = input.raw_inputs(); + let raw_inputs = input.to_raw_inputs(); // Press KeyCode for keycode in raw_inputs.keycodes.iter() { @@ -269,7 +269,7 @@ impl MockInput for MutableInputStreams<'_> { values: impl IntoIterator, gamepad: Option, ) { - let raw_inputs = input.raw_inputs(); + let raw_inputs = input.to_raw_inputs(); let mut value_iter = values.into_iter(); if let Some(gamepad) = gamepad { @@ -297,7 +297,7 @@ impl MockInput for MutableInputStreams<'_> { } fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { - let raw_inputs = input.raw_inputs(); + let raw_inputs = input.to_raw_inputs(); // Release KeyCode for keycode in raw_inputs.keycodes.iter() { diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index 2bdd9603..61609f51 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -148,12 +148,12 @@ impl FromIterator for DualAxisProcessor { } /// Provides methods for configuring and manipulating the processing pipeline for dual-axis input. -pub trait WithDualAxisProcessorExt: Sized { - /// Remove the current used [`DualAxisProcessor`]. - fn no_processor(self) -> Self; +pub trait WithDualAxisProcessingPipelineExt: Sized { + /// Resets the processing pipeline, removing any currently applied processors. + fn reset_processing_pipeline(self) -> Self; - /// Replaces the current pipeline with the specified [`DualAxisProcessor`]. - fn replace_processor(self, processor: impl Into) -> Self; + /// Replaces the current processing pipeline with the specified [`DualAxisProcessor`]. + fn replace_processing_pipeline(self, processor: impl Into) -> Self; /// Appends the given [`DualAxisProcessor`] as the next processing step. fn with_processor(self, processor: impl Into) -> Self; diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs index 4d1ad4bd..f26b652c 100644 --- a/src/input_processing/single_axis/mod.rs +++ b/src/input_processing/single_axis/mod.rs @@ -172,12 +172,12 @@ impl Hash for AxisProcessor { } /// Provides methods for configuring and manipulating the processing pipeline for single-axis input. -pub trait WithAxisProcessorExt: Sized { - /// Remove the current used [`AxisProcessor`]. - fn no_processor(self) -> Self; +pub trait WithAxisProcessingPipelineExt: Sized { + /// Resets the processing pipeline, removing any currently applied processors. + fn reset_processing_pipeline(self) -> Self; - /// Replaces the current pipeline with the specified [`AxisProcessor`]. - fn replace_processor(self, processor: impl Into) -> Self; + /// Replaces the current processing pipeline with the specified [`AxisProcessor`]. + fn replace_processing_pipeline(self, processor: impl Into) -> Self; /// Appends the given [`AxisProcessor`] as the next processing step. fn with_processor(self, processor: impl Into) -> Self; diff --git a/src/lib.rs b/src/lib.rs index e07243a7..3b30e6ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ pub mod input_processing; pub mod input_streams; pub mod orientation; pub mod plugin; +pub mod raw_inputs; pub mod systems; pub mod timing; pub mod typetag; diff --git a/src/plugin.rs b/src/plugin.rs index 5fe3a390..881edfb3 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,5 +1,5 @@ //! Contains the main plugin exported by this crate. -//! + use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; diff --git a/src/user_input/raw_inputs.rs b/src/raw_inputs.rs similarity index 99% rename from src/user_input/raw_inputs.rs rename to src/raw_inputs.rs index 9d699a41..c2aef77c 100644 --- a/src/user_input/raw_inputs.rs +++ b/src/raw_inputs.rs @@ -10,7 +10,7 @@ use crate::user_input::{GamepadControlDirection, MouseMoveDirection, MouseScroll /// The basic input events that make up a [`UserInput`](crate::user_input::UserInput). /// -/// Typically obtained by calling [`UserInput::raw_inputs`](crate::user_input::UserInput::raw_inputs). +/// Typically obtained by calling [`UserInput::raw_inputs`](crate::user_input::UserInput::to_raw_inputs). #[derive(Default, Debug, Clone, PartialEq)] #[must_use] pub struct RawInputs { diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index 06ef6caf..5cee94b6 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -6,13 +6,20 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::input_streams::InputStreams; -use crate::user_input::raw_inputs::RawInputs; +use crate::raw_inputs::RawInputs; use crate::user_input::{DualAxisData, InputKind, UserInput}; -/// A combined input that holds multiple [`UserInput`]s together, -/// enabling you to check if **all inputs are active simultaneously**, -/// or combine the values from all inner single-axis inputs if available, -/// or retrieve the values from the **first** dual-axis input. +/// A combined input that groups multiple [`UserInput`]s together, +/// which is useful for creating input combinations like hotkeys, shortcuts, and macros. +/// +/// # Behaviors +/// +/// - Simultaneous Activation Check: You can check if all the included inputs +/// are actively pressed at the same time. +/// - Single-Axis Input Combination: If some inner inputs are single-axis (like mouse wheel), +/// the input chord can combine (sum) their values into a single value. +/// - First Dual-Axis Input Only: Retrieves the values only from the first included +/// dual-axis input (like gamepad triggers). The state of other dual-axis inputs is ignored. /// /// # Warning /// @@ -97,19 +104,19 @@ impl UserInput for InputChord { /// Returns the [`RawInputs`] that combines the raw input events of all inner inputs. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { self.0.iter().fold(RawInputs::default(), |inputs, next| { - inputs.merge_input(&next.raw_inputs()) + inputs.merge_input(&next.to_raw_inputs()) }) } /// Retrieves a list of simple, atomic [`UserInput`]s that compose the chord. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { self.0 .iter() - .flat_map(|input| input.destructure()) + .flat_map(|input| input.to_clashing_checker()) .collect() } @@ -252,7 +259,7 @@ mod tests { assert_eq!(chord.0, expected_inners); let expected_raw_inputs = RawInputs::from_keycodes(required_keys); - assert_eq!(chord.raw_inputs(), expected_raw_inputs); + assert_eq!(chord.to_raw_inputs(), expected_raw_inputs); // No keys pressed, resulting in a released chord with a value of zero. let mut app = test_app(); @@ -314,11 +321,11 @@ mod tests { assert_eq!(chord.0, expected_inners); let expected_raw_inputs = RawInputs::from_keycodes(required_keys) - .merge_input(&MouseScrollAxis::X.raw_inputs()) - .merge_input(&MouseScrollAxis::Y.raw_inputs()) - .merge_input(&GamepadStick::LEFT.raw_inputs()) - .merge_input(&GamepadStick::RIGHT.raw_inputs()); - assert_eq!(chord.raw_inputs(), expected_raw_inputs); + .merge_input(&MouseScrollAxis::X.to_raw_inputs()) + .merge_input(&MouseScrollAxis::Y.to_raw_inputs()) + .merge_input(&GamepadStick::LEFT.to_raw_inputs()) + .merge_input(&GamepadStick::RIGHT.to_raw_inputs()); + assert_eq!(chord.to_raw_inputs(), expected_raw_inputs); // No input events, resulting in a released chord with values of zeros. let zeros = Some(DualAxisData::default()); diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index 4f88e576..71b08673 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -9,10 +9,11 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::axislike::{AxisDirection, AxisInputMode, DualAxisData}; use crate::input_processing::{ - AxisProcessor, DualAxisProcessor, WithAxisProcessorExt, WithDualAxisProcessorExt, + AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt, + WithDualAxisProcessingPipelineExt, }; use crate::input_streams::InputStreams; -use crate::user_input::raw_inputs::RawInputs; +use crate::raw_inputs::RawInputs; use crate::user_input::{InputKind, UserInput}; /// Retrieves the current value of the specified `axis`. @@ -102,7 +103,7 @@ impl UserInput for GamepadControlDirection { /// Creates a [`RawInputs`] from the direction directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_control_directions([*self]) } @@ -110,7 +111,7 @@ impl UserInput for GamepadControlDirection { /// as it represents a simple virtual button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -225,14 +226,14 @@ impl UserInput for GamepadControlAxis { /// Creates a [`RawInputs`] from the [`GamepadAxisType`] used by the axis. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_axes([self.axis]) } /// Returns both positive and negative [`GamepadControlDirection`]s to represent the movement. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(GamepadControlDirection::negative(self.axis)), Box::new(GamepadControlDirection::positive(self.axis)), @@ -269,15 +270,15 @@ impl UserInput for GamepadControlAxis { } } -impl WithAxisProcessorExt for GamepadControlAxis { +impl WithAxisProcessingPipelineExt for GamepadControlAxis { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = AxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -375,14 +376,14 @@ impl UserInput for GamepadStick { /// Creates a [`RawInputs`] from two [`GamepadAxisType`]s used by the stick. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_axes([self.x, self.y]) } /// Returns four [`GamepadControlDirection`]s to represent the movement. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(GamepadControlDirection::negative(self.x)), Box::new(GamepadControlDirection::positive(self.x)), @@ -424,15 +425,15 @@ impl UserInput for GamepadStick { } } -impl WithDualAxisProcessorExt for GamepadStick { +impl WithDualAxisProcessingPipelineExt for GamepadStick { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = DualAxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -494,7 +495,7 @@ impl UserInput for GamepadButtonType { /// Creates a [`RawInputs`] from the button directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_buttons([*self]) } @@ -502,7 +503,7 @@ impl UserInput for GamepadButtonType { /// as it represents a simple physical button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -614,14 +615,14 @@ impl UserInput for GamepadVirtualAxis { /// Creates a [`RawInputs`] from two [`GamepadButtonType`]s used by this axis. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_buttons([self.negative, self.positive]) } /// Returns the two [`GamepadButtonType`]s used by this axis. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(self.negative), Box::new(self.positive)] } @@ -662,15 +663,15 @@ impl UserInput for GamepadVirtualAxis { } } -impl WithAxisProcessorExt for GamepadVirtualAxis { +impl WithAxisProcessingPipelineExt for GamepadVirtualAxis { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = AxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -783,14 +784,14 @@ impl UserInput for GamepadVirtualDPad { /// Creates a [`RawInputs`] from four [`GamepadButtonType`]s used by this D-pad. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_gamepad_buttons([self.up, self.down, self.left, self.right]) } /// Returns the four [`GamepadButtonType`]s used by this D-pad. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(self.up), Box::new(self.down), @@ -831,15 +832,15 @@ impl UserInput for GamepadVirtualDPad { } } -impl WithDualAxisProcessorExt for GamepadVirtualDPad { +impl WithDualAxisProcessingPipelineExt for GamepadVirtualDPad { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = DualAxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -909,44 +910,44 @@ mod tests { let left_up = GamepadControlDirection::LEFT_UP; assert_eq!(left_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); - assert_eq!(left_up.raw_inputs(), raw_inputs); + assert_eq!(left_up.to_raw_inputs(), raw_inputs); // The opposite of left up let left_down = GamepadControlDirection::LEFT_DOWN; assert_eq!(left_down.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); - assert_eq!(left_up.raw_inputs(), raw_inputs); + assert_eq!(left_up.to_raw_inputs(), raw_inputs); let left_x = GamepadControlAxis::LEFT_X; assert_eq!(left_x.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_x.axis]); - assert_eq!(left_x.raw_inputs(), raw_inputs); + assert_eq!(left_x.to_raw_inputs(), raw_inputs); let left_y = GamepadControlAxis::LEFT_Y; assert_eq!(left_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_y.axis]); - assert_eq!(left_y.raw_inputs(), raw_inputs); + assert_eq!(left_y.to_raw_inputs(), raw_inputs); let left = GamepadStick::LEFT; assert_eq!(left.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([left.x, left.y]); - assert_eq!(left.raw_inputs(), raw_inputs); + assert_eq!(left.to_raw_inputs(), raw_inputs); // Up; but for the other stick let right_up = GamepadControlDirection::RIGHT_DOWN; assert_eq!(right_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([right_up]); - assert_eq!(right_up.raw_inputs(), raw_inputs); + assert_eq!(right_up.to_raw_inputs(), raw_inputs); let right_y = GamepadControlAxis::RIGHT_Y; assert_eq!(right_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); - assert_eq!(right_y.raw_inputs(), raw_inputs); + assert_eq!(right_y.to_raw_inputs(), raw_inputs); let right = GamepadStick::RIGHT; assert_eq!(right.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); - assert_eq!(right_y.raw_inputs(), raw_inputs); + assert_eq!(right_y.to_raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); @@ -1014,37 +1015,37 @@ mod tests { let up = GamepadButtonType::DPadUp; assert_eq!(up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([up]); - assert_eq!(up.raw_inputs(), raw_inputs); + assert_eq!(up.to_raw_inputs(), raw_inputs); let left = GamepadButtonType::DPadLeft; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([left]); - assert_eq!(left.raw_inputs(), raw_inputs); + assert_eq!(left.to_raw_inputs(), raw_inputs); let down = GamepadButtonType::DPadDown; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([down]); - assert_eq!(down.raw_inputs(), raw_inputs); + assert_eq!(down.to_raw_inputs(), raw_inputs); let right = GamepadButtonType::DPadRight; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([right]); - assert_eq!(right.raw_inputs(), raw_inputs); + assert_eq!(right.to_raw_inputs(), raw_inputs); let x_axis = GamepadVirtualAxis::DPAD_X; assert_eq!(x_axis.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([left, right]); - assert_eq!(x_axis.raw_inputs(), raw_inputs); + assert_eq!(x_axis.to_raw_inputs(), raw_inputs); let y_axis = GamepadVirtualAxis::DPAD_Y; assert_eq!(y_axis.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([down, up]); - assert_eq!(y_axis.raw_inputs(), raw_inputs); + assert_eq!(y_axis.to_raw_inputs(), raw_inputs); let dpad = GamepadVirtualDPad::DPAD; assert_eq!(dpad.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_buttons([up, down, left, right]); - assert_eq!(dpad.raw_inputs(), raw_inputs); + assert_eq!(dpad.to_raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index e03f77d2..3a54bb86 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -7,11 +7,12 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::axislike::DualAxisData; use crate::input_processing::{ - AxisProcessor, DualAxisProcessor, WithAxisProcessorExt, WithDualAxisProcessorExt, + AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt, + WithDualAxisProcessingPipelineExt, }; use crate::input_streams::InputStreams; -use crate::prelude::raw_inputs::RawInputs; -use crate::user_input::{InputKind, UserInput}; +use crate::raw_inputs::RawInputs; +use crate::user_input::{InputChord, InputKind, UserInput}; /// A key or combination of keys used for capturing user input from the keyboard. #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] @@ -49,14 +50,14 @@ impl UserInput for KeyboardKey { /// Creates a [`RawInputs`] from the [`KeyCode`]s used by this [`KeyboardKey`]. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_keycodes(self.keycodes()) } /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { self.keycodes() .iter() .map(|keycode| Box::new(*keycode) as Box) @@ -120,7 +121,7 @@ impl UserInput for KeyCode { /// Creates a [`RawInputs`] from the key directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_keycodes([*self]) } @@ -128,7 +129,7 @@ impl UserInput for KeyCode { /// as it represents a simple physical button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -208,6 +209,12 @@ impl ModifierKey { ModifierKey::Super => KeyCode::SuperRight, } } + + /// Create an [`InputChord`] that includes this [`ModifierKey`] and the given `input`. + #[inline] + pub fn with(&self, other: impl UserInput) -> InputChord { + InputChord::from_single(*self).with(other) + } } #[serde_typetag] @@ -220,14 +227,14 @@ impl UserInput for ModifierKey { /// Creates a [`RawInputs`] from two [`KeyCode`]s used by this [`ModifierKey`]. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_keycodes(self.keycodes()) } /// Returns the two [`KeyCode`]s used by this [`ModifierKey`]. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(self.left()), Box::new(self.right())] } @@ -355,9 +362,9 @@ impl UserInput for KeyboardVirtualAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from two [`KeyCode`]s used by this axis. + /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this axis. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { let keycodes = self .negative .keycodes() @@ -366,14 +373,16 @@ impl UserInput for KeyboardVirtualAxis { RawInputs::from_keycodes(keycodes) } - /// Returns the two [`KeyboardKey`]s used by this axis. + /// Returns all the [`KeyCode`]s used by this axis. #[must_use] #[inline] - fn destructure(&self) -> Vec> { - vec![ - Box::new(self.negative.clone()), - Box::new(self.positive.clone()), - ] + fn to_clashing_checker(&self) -> Vec> { + self.negative + .keycodes() + .into_iter() + .chain(self.positive.keycodes()) + .map(|keycode| Box::new(keycode) as Box) + .collect() } /// Checks if this axis has a non-zero value after processing by the associated processor. @@ -405,15 +414,15 @@ impl UserInput for KeyboardVirtualAxis { } } -impl WithAxisProcessorExt for KeyboardVirtualAxis { +impl WithAxisProcessingPipelineExt for KeyboardVirtualAxis { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = AxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -535,9 +544,9 @@ impl UserInput for KeyboardVirtualDPad { InputKind::DualAxis } - /// Creates a [`RawInputs`] from four [`KeyCode`]s used by this D-pad. + /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this D-pad. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { let keycodes = self .up .keycodes() @@ -548,16 +557,18 @@ impl UserInput for KeyboardVirtualDPad { RawInputs::from_keycodes(keycodes) } - /// Returns the four [`KeyboardKey`]s used by this D-pad. + /// Returns all the [`KeyCode`]s used by this D-pad. #[must_use] #[inline] - fn destructure(&self) -> Vec> { - vec![ - Box::new(self.up.clone()), - Box::new(self.down.clone()), - Box::new(self.left.clone()), - Box::new(self.right.clone()), - ] + fn to_clashing_checker(&self) -> Vec> { + self.up + .keycodes() + .into_iter() + .chain(self.down.keycodes()) + .chain(self.left.keycodes()) + .chain(self.right.keycodes()) + .map(|keycode| Box::new(keycode) as Box) + .collect() } /// Checks if this D-pad has a non-zero magnitude after processing by the associated processor. @@ -583,15 +594,15 @@ impl UserInput for KeyboardVirtualDPad { } } -impl WithDualAxisProcessorExt for KeyboardVirtualDPad { +impl WithDualAxisProcessingPipelineExt for KeyboardVirtualDPad { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = DualAxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -607,6 +618,7 @@ impl WithDualAxisProcessorExt for KeyboardVirtualDPad { mod tests { use super::*; use crate::input_mocking::MockInput; + use crate::raw_inputs::RawInputs; use bevy::input::InputPlugin; use bevy::prelude::*; @@ -640,34 +652,34 @@ mod tests { fn test_keyboard_input() { let up = KeyCode::ArrowUp; assert_eq!(up.kind(), InputKind::Button); - assert_eq!(up.raw_inputs(), RawInputs::from_keycodes([up])); + assert_eq!(up.to_raw_inputs(), RawInputs::from_keycodes([up])); let left = KeyCode::ArrowLeft; assert_eq!(left.kind(), InputKind::Button); - assert_eq!(left.raw_inputs(), RawInputs::from_keycodes([left])); + assert_eq!(left.to_raw_inputs(), RawInputs::from_keycodes([left])); let alt = ModifierKey::Alt; assert_eq!(alt.kind(), InputKind::Button); let alt_raw_inputs = RawInputs::from_keycodes([KeyCode::AltLeft, KeyCode::AltRight]); - assert_eq!(alt.raw_inputs(), alt_raw_inputs); + assert_eq!(alt.to_raw_inputs(), alt_raw_inputs); let physical_up = KeyboardKey::PhysicalKey(up); assert_eq!(physical_up.kind(), InputKind::Button); - assert_eq!(physical_up.raw_inputs(), RawInputs::from_keycodes([up])); + assert_eq!(physical_up.to_raw_inputs(), RawInputs::from_keycodes([up])); let physical_any_up_left = KeyboardKey::PhysicalKeyAny(vec![up, left]); assert_eq!(physical_any_up_left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_keycodes([up, left]); - assert_eq!(physical_any_up_left.raw_inputs(), raw_inputs); + assert_eq!(physical_any_up_left.to_raw_inputs(), raw_inputs); let keyboard_alt = KeyboardKey::ModifierKey(alt); assert_eq!(keyboard_alt.kind(), InputKind::Button); - assert_eq!(keyboard_alt.raw_inputs(), alt_raw_inputs); + assert_eq!(keyboard_alt.to_raw_inputs(), alt_raw_inputs); let arrow_y = KeyboardVirtualAxis::VERTICAL_ARROW_KEYS; assert_eq!(arrow_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_keycodes([KeyCode::ArrowDown, KeyCode::ArrowUp]); - assert_eq!(arrow_y.raw_inputs(), raw_inputs); + assert_eq!(arrow_y.to_raw_inputs(), raw_inputs); let arrows = KeyboardVirtualDPad::ARROW_KEYS; assert_eq!(arrows.kind(), InputKind::DualAxis); @@ -677,7 +689,7 @@ mod tests { KeyCode::ArrowLeft, KeyCode::ArrowRight, ]); - assert_eq!(arrows.raw_inputs(), raw_inputs); + assert_eq!(arrows.to_raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index c0ac1cfc..487a3139 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -31,7 +31,7 @@ //! //! ## Raw Input Events //! -//! [`UserInput`]s use the method [`UserInput::raw_inputs`] returning a [`RawInputs`] +//! [`UserInput`]s use the method [`UserInput::to_raw_inputs`] returning a [`RawInputs`] //! used for sending fake input events, see [input mocking](crate::input_mocking::MockInput) for details. //! //! ## Built-in Inputs @@ -95,8 +95,8 @@ use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; use crate::axislike::DualAxisData; use crate::input_streams::InputStreams; +use crate::raw_inputs::RawInputs; use crate::typetag::RegisterTypeTag; -use crate::user_input::raw_inputs::RawInputs; pub use self::chord::*; pub use self::gamepad::*; @@ -107,7 +107,6 @@ pub mod chord; pub mod gamepad; pub mod keyboard; pub mod mouse; -pub mod raw_inputs; /// Classifies [`UserInput`]s based on their behavior (buttons, analog axes, etc.). #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] @@ -134,15 +133,6 @@ pub trait UserInput: /// Defines the kind of data that the input should provide. fn kind(&self) -> InputKind; - /// Returns the [`RawInputs`] that make up the input. - fn raw_inputs(&self) -> RawInputs; - - /// Breaks down this input until it reaches its most basic [`UserInput`]s. - /// - /// For inputs that represent a simple, atomic control, - /// this method should always return a list that only contains a boxed copy of the input itself. - fn destructure(&self) -> Vec>; - /// Checks if the input is currently active. fn pressed(&self, input_streams: &InputStreams) -> bool; @@ -157,6 +147,15 @@ pub trait UserInput: /// For inputs that don't represent dual-axis input, there is no need to override this method. /// The default implementation will always return [`None`]. fn axis_pair(&self, _input_streams: &InputStreams) -> Option; + + /// Returns the [`RawInputs`] that make up the input. + fn to_raw_inputs(&self) -> RawInputs; + + /// Breaks down this input until it reaches its most basic [`UserInput`]s. + /// + /// For inputs that represent a simple, atomic control, + /// this method should always return a list that only contains a boxed copy of the input itself. + fn to_clashing_checker(&self) -> Vec>; } dyn_clone::clone_trait_object!(UserInput); diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index a8caf9e1..bb574a2f 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -8,7 +8,7 @@ use crate as leafwing_input_manager; use crate::axislike::{AxisInputMode, DualAxisData, DualAxisDirection, DualAxisType}; use crate::input_processing::*; use crate::input_streams::InputStreams; -use crate::user_input::raw_inputs::RawInputs; +use crate::raw_inputs::RawInputs; use crate::user_input::{InputKind, UserInput}; // Built-in support for Bevy's MouseButton @@ -22,7 +22,7 @@ impl UserInput for MouseButton { /// Creates a [`RawInputs`] from the button directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_buttons([*self]) } @@ -30,7 +30,7 @@ impl UserInput for MouseButton { /// as it represents a simple physical button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -104,7 +104,7 @@ impl UserInput for MouseMoveDirection { /// Creates a [`RawInputs`] from the direction directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_move_directions([*self]) } @@ -112,7 +112,7 @@ impl UserInput for MouseMoveDirection { /// as it represents a simple virtual button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -199,7 +199,7 @@ impl UserInput for MouseMoveAxis { /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_move_axes([self.axis]) } @@ -207,7 +207,7 @@ impl UserInput for MouseMoveAxis { /// as it represents a simple axis input. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(MouseMoveDirection(self.axis.negative())), Box::new(MouseMoveDirection(self.axis.positive())), @@ -240,15 +240,15 @@ impl UserInput for MouseMoveAxis { } } -impl WithAxisProcessorExt for MouseMoveAxis { +impl WithAxisProcessingPipelineExt for MouseMoveAxis { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = AxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -298,14 +298,14 @@ impl UserInput for MouseMove { /// Creates a [`RawInputs`] from two [`DualAxisType`] used by the input. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_move_axes(DualAxisType::axes()) } /// Returns two raw, unprocessed [`MouseMoveAxis`] instances to represent the movement. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(MouseMoveDirection::UP), Box::new(MouseMoveDirection::DOWN), @@ -345,15 +345,15 @@ impl UserInput for MouseMove { } } -impl WithDualAxisProcessorExt for MouseMove { +impl WithDualAxisProcessingPipelineExt for MouseMove { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = DualAxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -408,7 +408,7 @@ impl UserInput for MouseScrollDirection { /// Creates a [`RawInputs`] from the direction directly. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_scroll_directions([*self]) } @@ -416,7 +416,7 @@ impl UserInput for MouseScrollDirection { /// as it represents a simple virtual button. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![Box::new(*self)] } @@ -503,14 +503,14 @@ impl UserInput for MouseScrollAxis { /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_scroll_axes([self.axis]) } /// Returns both positive and negative [`MouseScrollDirection`]s to represent the movement. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(MouseScrollDirection(self.axis.negative())), Box::new(MouseScrollDirection(self.axis.positive())), @@ -543,15 +543,15 @@ impl UserInput for MouseScrollAxis { } } -impl WithAxisProcessorExt for MouseScrollAxis { +impl WithAxisProcessingPipelineExt for MouseScrollAxis { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = AxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -601,14 +601,14 @@ impl UserInput for MouseScroll { /// Creates a [`RawInputs`] from two [`DualAxisType`] used by the input. #[inline] - fn raw_inputs(&self) -> RawInputs { + fn to_raw_inputs(&self) -> RawInputs { RawInputs::from_mouse_scroll_axes(DualAxisType::axes()) } /// Returns two raw, unprocessed [`MouseScrollAxis`] instances to represent the movement. #[must_use] #[inline] - fn destructure(&self) -> Vec> { + fn to_clashing_checker(&self) -> Vec> { vec![ Box::new(MouseScrollDirection::UP), Box::new(MouseScrollDirection::DOWN), @@ -648,15 +648,15 @@ impl UserInput for MouseScroll { } } -impl WithDualAxisProcessorExt for MouseScroll { +impl WithDualAxisProcessingPipelineExt for MouseScroll { #[inline] - fn no_processor(mut self) -> Self { + fn reset_processing_pipeline(mut self) -> Self { self.processor = DualAxisProcessor::None; self } #[inline] - fn replace_processor(mut self, processor: impl Into) -> Self { + fn replace_processing_pipeline(mut self, processor: impl Into) -> Self { self.processor = processor.into(); self } @@ -705,15 +705,21 @@ mod tests { fn test_mouse_button() { let left = MouseButton::Left; assert_eq!(left.kind(), InputKind::Button); - assert_eq!(left.raw_inputs(), RawInputs::from_mouse_buttons([left])); + assert_eq!(left.to_raw_inputs(), RawInputs::from_mouse_buttons([left])); let middle = MouseButton::Middle; assert_eq!(middle.kind(), InputKind::Button); - assert_eq!(middle.raw_inputs(), RawInputs::from_mouse_buttons([middle])); + assert_eq!( + middle.to_raw_inputs(), + RawInputs::from_mouse_buttons([middle]) + ); let right = MouseButton::Right; assert_eq!(right.kind(), InputKind::Button); - assert_eq!(right.raw_inputs(), RawInputs::from_mouse_buttons([right])); + assert_eq!( + right.to_raw_inputs(), + RawInputs::from_mouse_buttons([right]) + ); // No inputs let mut app = test_app(); @@ -756,17 +762,17 @@ mod tests { let mouse_move_up = MouseMoveDirection::UP; assert_eq!(mouse_move_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_mouse_move_directions([mouse_move_up]); - assert_eq!(mouse_move_up.raw_inputs(), raw_inputs); + assert_eq!(mouse_move_up.to_raw_inputs(), raw_inputs); let mouse_move_y = MouseMoveAxis::Y; assert_eq!(mouse_move_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::Y]); - assert_eq!(mouse_move_y.raw_inputs(), raw_inputs); + assert_eq!(mouse_move_y.to_raw_inputs(), raw_inputs); let mouse_move = MouseMove::RAW; assert_eq!(mouse_move.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::X, DualAxisType::Y]); - assert_eq!(mouse_move.raw_inputs(), raw_inputs); + assert_eq!(mouse_move.to_raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); @@ -833,17 +839,17 @@ mod tests { let mouse_scroll_up = MouseScrollDirection::UP; assert_eq!(mouse_scroll_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_mouse_scroll_directions([mouse_scroll_up]); - assert_eq!(mouse_scroll_up.raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll_up.to_raw_inputs(), raw_inputs); let mouse_scroll_y = MouseScrollAxis::Y; assert_eq!(mouse_scroll_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::Y]); - assert_eq!(mouse_scroll_y.raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll_y.to_raw_inputs(), raw_inputs); let mouse_scroll = MouseScroll::RAW; assert_eq!(mouse_scroll.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::X, DualAxisType::Y]); - assert_eq!(mouse_scroll.raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll.to_raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 39104f8e..5f54cfd4 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -110,7 +110,7 @@ fn mouse_move_buttonlike() { let input_map = app.world.resource::>(); // Get the first associated input let input = input_map.get(action).unwrap().first().unwrap().clone(); - let direction = Reflect::as_any(&input) + let direction = Reflect::as_any(input.as_ref()) .downcast_ref::() .unwrap(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index 1e660c22..7cd38abf 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -111,8 +111,8 @@ fn mouse_scroll_buttonlike() { let input_map = app.world.resource::>(); // Get the first associated input let input = input_map.get(action).unwrap().first().unwrap().clone(); - let direction = Reflect::as_any(&input) - .downcast_ref::() + let direction = Reflect::as_any(input.as_ref()) + .downcast_ref::() .unwrap(); app.press_input(*direction); From 41103091897d4cf2d2c6258d8d1d697f7ed74327 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 20:26:07 +0800 Subject: [PATCH 11/20] Fix clashing inputs --- src/clashing_inputs.rs | 138 +++++++++++++++------ src/input_mocking.rs | 14 +-- src/raw_inputs.rs | 2 +- src/user_input/chord.rs | 51 ++++---- src/user_input/gamepad.rs | 217 ++++++++++++++++---------------- src/user_input/keyboard.rs | 209 ++++++++++++++++--------------- src/user_input/mod.rs | 15 +-- src/user_input/mouse.rs | 246 ++++++++++++++++++------------------- 8 files changed, 478 insertions(+), 414 deletions(-) diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 073736fc..22d052b4 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -46,25 +46,88 @@ impl ClashStrategy { } } -/// Checks if the given two [`UserInput`]s clash with each other. -#[must_use] -#[inline] -fn check_clashed(lhs: &dyn UserInput, rhs: &dyn UserInput) -> bool { +/// The basic inputs that make up a [`UserInput`]. +#[derive(Debug, Clone)] +pub enum BasicInputs { + /// The input consists of a single, fundamental [`UserInput`]. + /// In most cases, the input simply holds itself. + Single(Box), + + /// The input consists of multiple independent [`UserInput`]s. + Composite(Vec>), + + /// The input represents one or more independent [`UserInput`] types. + Group(Vec>), +} + +impl BasicInputs { + /// Returns a list of the underlying [`UserInput`]s. #[inline] - fn clash(list_a: &[Box], list_b: &[Box]) -> bool { - // Checks if the length is greater than one, - // as simple, atomic input are able to trigger multiple actions. - list_a.len() > 1 && list_b.iter().all(|a| list_a.contains(a)) + pub fn inputs(&self) -> Vec> { + match self.clone() { + Self::Single(input) => vec![input], + Self::Composite(inputs) => inputs, + Self::Group(inputs) => inputs, + } } - let lhs_inners = lhs.to_clashing_checker(); - let rhs_inners = rhs.to_clashing_checker(); - - let res = clash(&lhs_inners, &rhs_inners) || clash(&rhs_inners, &lhs_inners); - - dbg!(lhs_inners, rhs_inners, res); + /// Returns the number of the underlying [`UserInput`]s. + #[allow(clippy::len_without_is_empty)] + #[inline] + pub fn len(&self) -> usize { + match self { + Self::Single(_) => 1, + Self::Composite(_) => 1, + Self::Group(inputs) => inputs.len(), + } + } - res + /// Checks if the given two [`BasicInputs`] clash with each other. + #[inline] + pub fn clashed(&self, other: &BasicInputs) -> bool { + match (self, other) { + (Self::Single(_), Self::Single(_)) => false, + (Self::Single(self_single), Self::Group(other_group)) => { + other_group.len() > 1 && other_group.contains(self_single) + } + (Self::Group(self_group), Self::Single(other_single)) => { + self_group.len() > 1 && self_group.contains(other_single) + } + (Self::Single(self_single), Self::Composite(other_composite)) => { + other_composite.contains(self_single) + } + (Self::Composite(self_composite), Self::Single(other_single)) => { + self_composite.contains(other_single) + } + (Self::Composite(self_composite), Self::Group(other_group)) => { + other_group.len() > 1 + && other_group + .iter() + .any(|input| self_composite.contains(input)) + } + (Self::Group(self_group), Self::Composite(other_composite)) => { + self_group.len() > 1 + && self_group + .iter() + .any(|input| other_composite.contains(input)) + } + (Self::Group(self_group), Self::Group(other_group)) => { + self_group.len() > 1 + && other_group.len() > 1 + && self_group != other_group + && (self_group.iter().all(|input| other_group.contains(input)) + || other_group.iter().all(|input| self_group.contains(input))) + } + (Self::Composite(self_composite), Self::Composite(other_composite)) => { + other_composite + .iter() + .any(|input| self_composite.contains(input)) + || self_composite + .iter() + .any(|input| other_composite.contains(input)) + } + } + } } impl InputMap { @@ -137,7 +200,7 @@ impl InputMap { for input_a in self.get(action_a)? { for input_b in self.get(action_b)? { - if check_clashed(input_a.as_ref(), input_b.as_ref()) { + if input_a.basic_inputs().clashed(&input_b.basic_inputs()) { clash.inputs_a.push(input_a.clone()); clash.inputs_b.push(input_b.clone()); } @@ -191,8 +254,8 @@ fn check_clash(clash: &Clash, input_streams: &InputStreams) -> .iter() .filter(|&input| input.pressed(input_streams)) { - // If a clash was detected, - if check_clashed(input_a.as_ref(), input_b.as_ref()) { + // If a clash was detected + if input_a.basic_inputs().clashed(&input_b.basic_inputs()) { actual_clash.inputs_a.push(input_a.clone()); actual_clash.inputs_b.push(input_b.clone()); } @@ -211,18 +274,18 @@ fn resolve_clash( input_streams: &InputStreams, ) -> Option { // Figure out why the actions are pressed - let reasons_a_is_pressed: Vec> = clash + let reasons_a_is_pressed: Vec<&dyn UserInput> = clash .inputs_a .iter() - .filter(|&input| input.pressed(input_streams)) - .cloned() + .filter(|input| input.pressed(input_streams)) + .map(|input| input.as_ref()) .collect(); - let reasons_b_is_pressed: Vec> = clash + let reasons_b_is_pressed: Vec<&dyn UserInput> = clash .inputs_b .iter() - .filter(|&input| input.pressed(input_streams)) - .cloned() + .filter(|input| input.pressed(input_streams)) + .map(|input| input.as_ref()) .collect(); // Clashes are spurious if the actions are pressed for any non-clashing reason @@ -230,7 +293,7 @@ fn resolve_clash( for reason_b in reasons_b_is_pressed.iter() { // If there is at least one non-clashing reason why these buttons should both be pressed, // we can avoid resolving the clash completely - if !check_clashed(reason_a.as_ref(), reason_b.as_ref()) { + if !reason_a.basic_inputs().clashed(&reason_b.basic_inputs()) { return None; } } @@ -244,13 +307,13 @@ fn resolve_clash( ClashStrategy::PrioritizeLongest => { let longest_a: usize = reasons_a_is_pressed .iter() - .map(|input| input.to_clashing_checker().len()) + .map(|input| input.basic_inputs().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); let longest_b: usize = reasons_b_is_pressed .iter() - .map(|input| input.to_clashing_checker().len()) + .map(|input| input.basic_inputs().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); @@ -314,10 +377,8 @@ mod tests { input_map } - fn test_input_clash(input_a: A, input_b: B) -> bool { - let input_a: Box = Box::new(input_a); - let input_b: Box = Box::new(input_b); - check_clashed(input_a.as_ref(), input_b.as_ref()) + fn test_input_clash(input_a: impl UserInput, input_b: impl UserInput) -> bool { + input_a.basic_inputs().clashed(&input_b.basic_inputs()) } mod basic_functionality { @@ -412,13 +473,11 @@ mod tests { app.press_input(Digit2); app.update(); - let input_streams = InputStreams::from_world(&app.world, None); - assert_eq!( resolve_clash( &simple_clash, ClashStrategy::PrioritizeLongest, - &input_streams, + &InputStreams::from_world(&app.world, None), ), Some(One) ); @@ -428,7 +487,7 @@ mod tests { resolve_clash( &reversed_clash, ClashStrategy::PrioritizeLongest, - &input_streams, + &InputStreams::from_world(&app.world, None), ), Some(One) ); @@ -498,11 +557,10 @@ mod tests { action_data.insert(CtrlUp, action_datum.clone()); action_data.insert(MoveDPad, action_datum.clone()); - input_map.handle_clashes( - &mut action_data, - &InputStreams::from_world(&app.world, None), - ClashStrategy::PrioritizeLongest, - ); + let streams = InputStreams::from_world(&app.world, None); + println!("World: {streams:?}"); + + input_map.handle_clashes(&mut action_data, &streams, ClashStrategy::PrioritizeLongest); let mut expected = HashMap::new(); expected.insert(CtrlUp, action_datum); diff --git a/src/input_mocking.rs b/src/input_mocking.rs index 9f784e88..cee6da56 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -75,7 +75,7 @@ use crate::user_input::*; /// ``` pub trait MockInput { /// Simulates an activated event for the given `input`, - /// pressing all buttons and keys in the [`RawInputs`](raw_inputs::RawInputs) of the `input`. + /// pressing all buttons and keys in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. @@ -100,7 +100,7 @@ pub trait MockInput { fn press_input(&mut self, input: impl UserInput); /// Simulates an activated event for the given `input`, using the specified `gamepad`, - /// pressing all buttons and keys in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. + /// pressing all buttons and keys in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. @@ -120,7 +120,7 @@ pub trait MockInput { fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option); /// Simulates axis value changed events for the given `input`. - /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. + /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// Missing axis values default to `0.0`. /// /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs @@ -140,7 +140,7 @@ pub trait MockInput { fn send_axis_values(&mut self, input: impl UserInput, values: impl IntoIterator); /// Simulates axis value changed events for the given `input`, using the specified `gamepad`. - /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::user_input::raw_inputs::RawInputs) of the `input`. + /// Each value in the `values` iterator corresponds to an axis in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// Missing axis values default to `0.0`. /// /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs @@ -218,7 +218,7 @@ impl MockInput for MutableInputStreams<'_> { } fn press_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { - let raw_inputs = input.to_raw_inputs(); + let raw_inputs = input.raw_inputs(); // Press KeyCode for keycode in raw_inputs.keycodes.iter() { @@ -269,7 +269,7 @@ impl MockInput for MutableInputStreams<'_> { values: impl IntoIterator, gamepad: Option, ) { - let raw_inputs = input.to_raw_inputs(); + let raw_inputs = input.raw_inputs(); let mut value_iter = values.into_iter(); if let Some(gamepad) = gamepad { @@ -297,7 +297,7 @@ impl MockInput for MutableInputStreams<'_> { } fn release_input_as_gamepad(&mut self, input: impl UserInput, gamepad: Option) { - let raw_inputs = input.to_raw_inputs(); + let raw_inputs = input.raw_inputs(); // Release KeyCode for keycode in raw_inputs.keycodes.iter() { diff --git a/src/raw_inputs.rs b/src/raw_inputs.rs index c2aef77c..9d699a41 100644 --- a/src/raw_inputs.rs +++ b/src/raw_inputs.rs @@ -10,7 +10,7 @@ use crate::user_input::{GamepadControlDirection, MouseMoveDirection, MouseScroll /// The basic input events that make up a [`UserInput`](crate::user_input::UserInput). /// -/// Typically obtained by calling [`UserInput::raw_inputs`](crate::user_input::UserInput::to_raw_inputs). +/// Typically obtained by calling [`UserInput::raw_inputs`](crate::user_input::UserInput::raw_inputs). #[derive(Default, Debug, Clone, PartialEq)] #[must_use] pub struct RawInputs { diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index 5cee94b6..6c02e6df 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -5,6 +5,7 @@ use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; +use crate::clashing_inputs::BasicInputs; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; use crate::user_input::{DualAxisData, InputKind, UserInput}; @@ -102,24 +103,6 @@ impl UserInput for InputChord { InputKind::Button } - /// Returns the [`RawInputs`] that combines the raw input events of all inner inputs. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - self.0.iter().fold(RawInputs::default(), |inputs, next| { - inputs.merge_input(&next.to_raw_inputs()) - }) - } - - /// Retrieves a list of simple, atomic [`UserInput`]s that compose the chord. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - self.0 - .iter() - .flat_map(|input| input.to_clashing_checker()) - .collect() - } - /// Checks if all the inner inputs within the chord are active simultaneously. #[must_use] #[inline] @@ -170,6 +153,26 @@ impl UserInput for InputChord { .flat_map(|input| input.axis_pair(input_streams)) .next() } + + /// Retrieves a list of simple, atomic [`UserInput`]s that compose the chord. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + let inputs = self + .0 + .iter() + .flat_map(|input| input.basic_inputs().inputs()) + .collect(); + BasicInputs::Group(inputs) + } + + /// Returns the [`RawInputs`] that combines the raw input events of all inner inputs. + #[inline] + fn raw_inputs(&self) -> RawInputs { + self.0.iter().fold(RawInputs::default(), |inputs, next| { + inputs.merge_input(&next.raw_inputs()) + }) + } } impl FromIterator for InputChord { @@ -259,7 +262,7 @@ mod tests { assert_eq!(chord.0, expected_inners); let expected_raw_inputs = RawInputs::from_keycodes(required_keys); - assert_eq!(chord.to_raw_inputs(), expected_raw_inputs); + assert_eq!(chord.raw_inputs(), expected_raw_inputs); // No keys pressed, resulting in a released chord with a value of zero. let mut app = test_app(); @@ -321,11 +324,11 @@ mod tests { assert_eq!(chord.0, expected_inners); let expected_raw_inputs = RawInputs::from_keycodes(required_keys) - .merge_input(&MouseScrollAxis::X.to_raw_inputs()) - .merge_input(&MouseScrollAxis::Y.to_raw_inputs()) - .merge_input(&GamepadStick::LEFT.to_raw_inputs()) - .merge_input(&GamepadStick::RIGHT.to_raw_inputs()); - assert_eq!(chord.to_raw_inputs(), expected_raw_inputs); + .merge_input(&MouseScrollAxis::X.raw_inputs()) + .merge_input(&MouseScrollAxis::Y.raw_inputs()) + .merge_input(&GamepadStick::LEFT.raw_inputs()) + .merge_input(&GamepadStick::RIGHT.raw_inputs()); + assert_eq!(chord.raw_inputs(), expected_raw_inputs); // No input events, resulting in a released chord with values of zeros. let zeros = Some(DualAxisData::default()); diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index 71b08673..cb281165 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::axislike::{AxisDirection, AxisInputMode, DualAxisData}; +use crate::clashing_inputs::BasicInputs; use crate::input_processing::{ AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt, WithDualAxisProcessingPipelineExt, @@ -101,20 +102,6 @@ impl UserInput for GamepadControlDirection { InputKind::Button } - /// Creates a [`RawInputs`] from the direction directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_control_directions([*self]) - } - - /// Returns a list that only contains the [`GamepadControlDirection`] itself, - /// as it represents a simple virtual button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if there is any recent stick movement along the specified direction. /// /// When a [`Gamepad`] is specified, only checks the movement on the specified gamepad. @@ -143,6 +130,20 @@ impl UserInput for GamepadControlDirection { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a [`BasicInputs`] that only contains the [`GamepadControlDirection`] itself, + /// as it represents a simple virtual button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the direction directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_control_directions([*self]) + } } /// Captures values from a specified [`GamepadAxisType`]. @@ -224,22 +225,6 @@ impl UserInput for GamepadControlAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from the [`GamepadAxisType`] used by the axis. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_axes([self.axis]) - } - - /// Returns both positive and negative [`GamepadControlDirection`]s to represent the movement. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(GamepadControlDirection::negative(self.axis)), - Box::new(GamepadControlDirection::positive(self.axis)), - ] - } - /// Checks if this axis has a non-zero value. /// /// When a [`Gamepad`] is specified, only checks if the axis is active on the specified gamepad. @@ -268,6 +253,22 @@ impl UserInput for GamepadControlAxis { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns both positive and negative [`GamepadControlDirection`]s to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(GamepadControlDirection::negative(self.axis)), + Box::new(GamepadControlDirection::positive(self.axis)), + ]) + } + + /// Creates a [`RawInputs`] from the [`GamepadAxisType`] used by the axis. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_axes([self.axis]) + } } impl WithAxisProcessingPipelineExt for GamepadControlAxis { @@ -374,24 +375,6 @@ impl UserInput for GamepadStick { InputKind::DualAxis } - /// Creates a [`RawInputs`] from two [`GamepadAxisType`]s used by the stick. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_axes([self.x, self.y]) - } - - /// Returns four [`GamepadControlDirection`]s to represent the movement. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(GamepadControlDirection::negative(self.x)), - Box::new(GamepadControlDirection::positive(self.x)), - Box::new(GamepadControlDirection::negative(self.y)), - Box::new(GamepadControlDirection::positive(self.y)), - ] - } - /// Checks if this stick has a non-zero magnitude. /// /// When a [`Gamepad`] is specified, only checks if the stick is active on the specified gamepad. @@ -423,6 +406,24 @@ impl UserInput for GamepadStick { let value = self.processed_value(input_streams); Some(DualAxisData::from_xy(value)) } + + /// Returns four [`GamepadControlDirection`]s to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(GamepadControlDirection::negative(self.x)), + Box::new(GamepadControlDirection::positive(self.x)), + Box::new(GamepadControlDirection::negative(self.y)), + Box::new(GamepadControlDirection::positive(self.y)), + ]) + } + + /// Creates a [`RawInputs`] from two [`GamepadAxisType`]s used by the stick. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_axes([self.x, self.y]) + } } impl WithDualAxisProcessingPipelineExt for GamepadStick { @@ -493,20 +494,6 @@ impl UserInput for GamepadButtonType { InputKind::Button } - /// Creates a [`RawInputs`] from the button directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_buttons([*self]) - } - - /// Returns a list that only contains the [`GamepadButtonType`] itself, - /// as it represents a simple physical button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if the specified button is currently pressed down. /// /// When a [`Gamepad`] is specified, only checks if the button is pressed on the specified gamepad. @@ -544,6 +531,20 @@ impl UserInput for GamepadButtonType { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Creates a [`BasicInputs`] that only contains the [`GamepadButtonType`] itself, + /// as it represents a simple physical button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the button directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_buttons([*self]) + } } /// A virtual single-axis control constructed from two [`GamepadButtonType`]s. @@ -613,19 +614,6 @@ impl UserInput for GamepadVirtualAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from two [`GamepadButtonType`]s used by this axis. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_buttons([self.negative, self.positive]) - } - - /// Returns the two [`GamepadButtonType`]s used by this axis. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(self.negative), Box::new(self.positive)] - } - /// Checks if this axis has a non-zero value after processing by the associated processor. /// /// When a [`Gamepad`] is specified, only checks if the buttons are pressed on the specified gamepad. @@ -661,6 +649,19 @@ impl UserInput for GamepadVirtualAxis { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns the two [`GamepadButtonType`]s used by this axis. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![Box::new(self.negative), Box::new(self.positive)]) + } + + /// Creates a [`RawInputs`] from two [`GamepadButtonType`]s used by this axis. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_buttons([self.negative, self.positive]) + } } impl WithAxisProcessingPipelineExt for GamepadVirtualAxis { @@ -782,24 +783,6 @@ impl UserInput for GamepadVirtualDPad { InputKind::DualAxis } - /// Creates a [`RawInputs`] from four [`GamepadButtonType`]s used by this D-pad. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_gamepad_buttons([self.up, self.down, self.left, self.right]) - } - - /// Returns the four [`GamepadButtonType`]s used by this D-pad. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(self.up), - Box::new(self.down), - Box::new(self.left), - Box::new(self.right), - ] - } - /// Checks if this D-pad has a non-zero magnitude after processing by the associated processor. /// /// When a [`Gamepad`] is specified, only checks if the button is pressed on the specified gamepad. @@ -830,6 +813,24 @@ impl UserInput for GamepadVirtualDPad { let value = self.processed_value(input_streams); Some(DualAxisData::from_xy(value)) } + + /// Returns the four [`GamepadButtonType`]s used by this D-pad. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(self.up), + Box::new(self.down), + Box::new(self.left), + Box::new(self.right), + ]) + } + + /// Creates a [`RawInputs`] from four [`GamepadButtonType`]s used by this D-pad. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_gamepad_buttons([self.up, self.down, self.left, self.right]) + } } impl WithDualAxisProcessingPipelineExt for GamepadVirtualDPad { @@ -910,44 +911,44 @@ mod tests { let left_up = GamepadControlDirection::LEFT_UP; assert_eq!(left_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); - assert_eq!(left_up.to_raw_inputs(), raw_inputs); + assert_eq!(left_up.raw_inputs(), raw_inputs); // The opposite of left up let left_down = GamepadControlDirection::LEFT_DOWN; assert_eq!(left_down.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); - assert_eq!(left_up.to_raw_inputs(), raw_inputs); + assert_eq!(left_up.raw_inputs(), raw_inputs); let left_x = GamepadControlAxis::LEFT_X; assert_eq!(left_x.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_x.axis]); - assert_eq!(left_x.to_raw_inputs(), raw_inputs); + assert_eq!(left_x.raw_inputs(), raw_inputs); let left_y = GamepadControlAxis::LEFT_Y; assert_eq!(left_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_y.axis]); - assert_eq!(left_y.to_raw_inputs(), raw_inputs); + assert_eq!(left_y.raw_inputs(), raw_inputs); let left = GamepadStick::LEFT; assert_eq!(left.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([left.x, left.y]); - assert_eq!(left.to_raw_inputs(), raw_inputs); + assert_eq!(left.raw_inputs(), raw_inputs); // Up; but for the other stick let right_up = GamepadControlDirection::RIGHT_DOWN; assert_eq!(right_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([right_up]); - assert_eq!(right_up.to_raw_inputs(), raw_inputs); + assert_eq!(right_up.raw_inputs(), raw_inputs); let right_y = GamepadControlAxis::RIGHT_Y; assert_eq!(right_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); - assert_eq!(right_y.to_raw_inputs(), raw_inputs); + assert_eq!(right_y.raw_inputs(), raw_inputs); let right = GamepadStick::RIGHT; assert_eq!(right.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); - assert_eq!(right_y.to_raw_inputs(), raw_inputs); + assert_eq!(right_y.raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); @@ -1015,37 +1016,37 @@ mod tests { let up = GamepadButtonType::DPadUp; assert_eq!(up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([up]); - assert_eq!(up.to_raw_inputs(), raw_inputs); + assert_eq!(up.raw_inputs(), raw_inputs); let left = GamepadButtonType::DPadLeft; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([left]); - assert_eq!(left.to_raw_inputs(), raw_inputs); + assert_eq!(left.raw_inputs(), raw_inputs); let down = GamepadButtonType::DPadDown; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([down]); - assert_eq!(down.to_raw_inputs(), raw_inputs); + assert_eq!(down.raw_inputs(), raw_inputs); let right = GamepadButtonType::DPadRight; assert_eq!(left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([right]); - assert_eq!(right.to_raw_inputs(), raw_inputs); + assert_eq!(right.raw_inputs(), raw_inputs); let x_axis = GamepadVirtualAxis::DPAD_X; assert_eq!(x_axis.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([left, right]); - assert_eq!(x_axis.to_raw_inputs(), raw_inputs); + assert_eq!(x_axis.raw_inputs(), raw_inputs); let y_axis = GamepadVirtualAxis::DPAD_Y; assert_eq!(y_axis.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([down, up]); - assert_eq!(y_axis.to_raw_inputs(), raw_inputs); + assert_eq!(y_axis.raw_inputs(), raw_inputs); let dpad = GamepadVirtualDPad::DPAD; assert_eq!(dpad.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_buttons([up, down, left, right]); - assert_eq!(dpad.to_raw_inputs(), raw_inputs); + assert_eq!(dpad.raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index 3a54bb86..e7115637 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::axislike::DualAxisData; +use crate::clashing_inputs::BasicInputs; use crate::input_processing::{ AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt, WithDualAxisProcessingPipelineExt, @@ -48,22 +49,6 @@ impl UserInput for KeyboardKey { InputKind::Button } - /// Creates a [`RawInputs`] from the [`KeyCode`]s used by this [`KeyboardKey`]. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_keycodes(self.keycodes()) - } - - /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - self.keycodes() - .iter() - .map(|keycode| Box::new(*keycode) as Box) - .collect() - } - /// Checks if the specified [`KeyboardKey`] is currently pressed down. #[must_use] #[inline] @@ -87,6 +72,24 @@ impl UserInput for KeyboardKey { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + let keycodes = self + .keycodes() + .iter() + .map(|keycode| Box::new(*keycode) as Box) + .collect(); + BasicInputs::Group(keycodes) + } + + /// Creates a [`RawInputs`] from the [`KeyCode`]s used by this [`KeyboardKey`]. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_keycodes(self.keycodes()) + } } impl From for KeyboardKey { @@ -119,20 +122,6 @@ impl UserInput for KeyCode { InputKind::Button } - /// Creates a [`RawInputs`] from the key directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_keycodes([*self]) - } - - /// Returns a list that only contains the [`KeyCode`] itself, - /// as it represents a simple physical button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if the specified key is currently pressed down. #[must_use] #[inline] @@ -156,6 +145,20 @@ impl UserInput for KeyCode { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a [`BasicInputs`] that only contains the [`KeyCode`] itself, + /// as it represents a simple physical button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the key directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_keycodes([*self]) + } } /// Keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). @@ -225,19 +228,6 @@ impl UserInput for ModifierKey { InputKind::Button } - /// Creates a [`RawInputs`] from two [`KeyCode`]s used by this [`ModifierKey`]. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_keycodes(self.keycodes()) - } - - /// Returns the two [`KeyCode`]s used by this [`ModifierKey`]. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(self.left()), Box::new(self.right())] - } - /// Checks if the specified modifier key is currently pressed down. #[must_use] #[inline] @@ -261,6 +251,19 @@ impl UserInput for ModifierKey { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns the two [`KeyCode`]s used by this [`ModifierKey`]. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![Box::new(self.left()), Box::new(self.right())]) + } + + /// Creates a [`RawInputs`] from two [`KeyCode`]s used by this [`ModifierKey`]. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_keycodes(self.keycodes()) + } } /// A virtual single-axis control constructed from two [`KeyboardKey`]s. @@ -362,29 +365,6 @@ impl UserInput for KeyboardVirtualAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this axis. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - let keycodes = self - .negative - .keycodes() - .into_iter() - .chain(self.positive.keycodes()); - RawInputs::from_keycodes(keycodes) - } - - /// Returns all the [`KeyCode`]s used by this axis. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - self.negative - .keycodes() - .into_iter() - .chain(self.positive.keycodes()) - .map(|keycode| Box::new(keycode) as Box) - .collect() - } - /// Checks if this axis has a non-zero value after processing by the associated processor. #[must_use] #[inline] @@ -412,6 +392,31 @@ impl UserInput for KeyboardVirtualAxis { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns all the [`KeyCode`]s used by this axis. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + let keycodes = self + .negative + .keycodes() + .into_iter() + .chain(self.positive.keycodes()) + .map(|keycode| Box::new(keycode) as Box) + .collect(); + BasicInputs::Composite(keycodes) + } + + /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this axis. + #[inline] + fn raw_inputs(&self) -> RawInputs { + let keycodes = self + .negative + .keycodes() + .into_iter() + .chain(self.positive.keycodes()); + RawInputs::from_keycodes(keycodes) + } } impl WithAxisProcessingPipelineExt for KeyboardVirtualAxis { @@ -544,33 +549,6 @@ impl UserInput for KeyboardVirtualDPad { InputKind::DualAxis } - /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this D-pad. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - let keycodes = self - .up - .keycodes() - .into_iter() - .chain(self.down.keycodes()) - .chain(self.left.keycodes()) - .chain(self.right.keycodes()); - RawInputs::from_keycodes(keycodes) - } - - /// Returns all the [`KeyCode`]s used by this D-pad. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - self.up - .keycodes() - .into_iter() - .chain(self.down.keycodes()) - .chain(self.left.keycodes()) - .chain(self.right.keycodes()) - .map(|keycode| Box::new(keycode) as Box) - .collect() - } - /// Checks if this D-pad has a non-zero magnitude after processing by the associated processor. #[must_use] #[inline] @@ -592,6 +570,35 @@ impl UserInput for KeyboardVirtualDPad { let value = self.processed_value(input_streams); Some(DualAxisData::from_xy(value)) } + + /// Returns all the [`KeyCode`]s used by this D-pad. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + let keycodes = self + .up + .keycodes() + .into_iter() + .chain(self.down.keycodes()) + .chain(self.left.keycodes()) + .chain(self.right.keycodes()) + .map(|keycode| Box::new(keycode) as Box) + .collect(); + BasicInputs::Composite(keycodes) + } + + /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this D-pad. + #[inline] + fn raw_inputs(&self) -> RawInputs { + let keycodes = self + .up + .keycodes() + .into_iter() + .chain(self.down.keycodes()) + .chain(self.left.keycodes()) + .chain(self.right.keycodes()); + RawInputs::from_keycodes(keycodes) + } } impl WithDualAxisProcessingPipelineExt for KeyboardVirtualDPad { @@ -652,34 +659,34 @@ mod tests { fn test_keyboard_input() { let up = KeyCode::ArrowUp; assert_eq!(up.kind(), InputKind::Button); - assert_eq!(up.to_raw_inputs(), RawInputs::from_keycodes([up])); + assert_eq!(up.raw_inputs(), RawInputs::from_keycodes([up])); let left = KeyCode::ArrowLeft; assert_eq!(left.kind(), InputKind::Button); - assert_eq!(left.to_raw_inputs(), RawInputs::from_keycodes([left])); + assert_eq!(left.raw_inputs(), RawInputs::from_keycodes([left])); let alt = ModifierKey::Alt; assert_eq!(alt.kind(), InputKind::Button); let alt_raw_inputs = RawInputs::from_keycodes([KeyCode::AltLeft, KeyCode::AltRight]); - assert_eq!(alt.to_raw_inputs(), alt_raw_inputs); + assert_eq!(alt.raw_inputs(), alt_raw_inputs); let physical_up = KeyboardKey::PhysicalKey(up); assert_eq!(physical_up.kind(), InputKind::Button); - assert_eq!(physical_up.to_raw_inputs(), RawInputs::from_keycodes([up])); + assert_eq!(physical_up.raw_inputs(), RawInputs::from_keycodes([up])); let physical_any_up_left = KeyboardKey::PhysicalKeyAny(vec![up, left]); assert_eq!(physical_any_up_left.kind(), InputKind::Button); let raw_inputs = RawInputs::from_keycodes([up, left]); - assert_eq!(physical_any_up_left.to_raw_inputs(), raw_inputs); + assert_eq!(physical_any_up_left.raw_inputs(), raw_inputs); let keyboard_alt = KeyboardKey::ModifierKey(alt); assert_eq!(keyboard_alt.kind(), InputKind::Button); - assert_eq!(keyboard_alt.to_raw_inputs(), alt_raw_inputs); + assert_eq!(keyboard_alt.raw_inputs(), alt_raw_inputs); let arrow_y = KeyboardVirtualAxis::VERTICAL_ARROW_KEYS; assert_eq!(arrow_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_keycodes([KeyCode::ArrowDown, KeyCode::ArrowUp]); - assert_eq!(arrow_y.to_raw_inputs(), raw_inputs); + assert_eq!(arrow_y.raw_inputs(), raw_inputs); let arrows = KeyboardVirtualDPad::ARROW_KEYS; assert_eq!(arrows.kind(), InputKind::DualAxis); @@ -689,7 +696,7 @@ mod tests { KeyCode::ArrowLeft, KeyCode::ArrowRight, ]); - assert_eq!(arrows.to_raw_inputs(), raw_inputs); + assert_eq!(arrows.raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index 487a3139..4ca62833 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -31,7 +31,7 @@ //! //! ## Raw Input Events //! -//! [`UserInput`]s use the method [`UserInput::to_raw_inputs`] returning a [`RawInputs`] +//! [`UserInput`]s use the method [`UserInput::raw_inputs`] returning a [`RawInputs`] //! used for sending fake input events, see [input mocking](crate::input_mocking::MockInput) for details. //! //! ## Built-in Inputs @@ -94,6 +94,7 @@ use serde_flexitos::ser::require_erased_serialize_impl; use serde_flexitos::{serialize_trait_object, MapRegistry, Registry}; use crate::axislike::DualAxisData; +use crate::clashing_inputs::BasicInputs; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; use crate::typetag::RegisterTypeTag; @@ -148,14 +149,14 @@ pub trait UserInput: /// The default implementation will always return [`None`]. fn axis_pair(&self, _input_streams: &InputStreams) -> Option; - /// Returns the [`RawInputs`] that make up the input. - fn to_raw_inputs(&self) -> RawInputs; - - /// Breaks down this input until it reaches its most basic [`UserInput`]s. + /// Returns the most basic inputs that make up this input. /// /// For inputs that represent a simple, atomic control, - /// this method should always return a list that only contains a boxed copy of the input itself. - fn to_clashing_checker(&self) -> Vec>; + /// this method should always return a [`BasicInputs::Single`] that only contains the input itself. + fn basic_inputs(&self) -> BasicInputs; + + /// Returns the raw input events that make up this input. + fn raw_inputs(&self) -> RawInputs; } dyn_clone::clone_trait_object!(UserInput); diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index bb574a2f..5264c4d9 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; use crate::axislike::{AxisInputMode, DualAxisData, DualAxisDirection, DualAxisType}; +use crate::clashing_inputs::BasicInputs; use crate::input_processing::*; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; @@ -20,20 +21,6 @@ impl UserInput for MouseButton { InputKind::Button } - /// Creates a [`RawInputs`] from the button directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_buttons([*self]) - } - - /// Returns a list that only contains the [`MouseButton`] itself, - /// as it represents a simple physical button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if the specified button is currently pressed down. #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -59,6 +46,20 @@ impl UserInput for MouseButton { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a [`BasicInputs`] that only contains the [`MouseButton`] itself, + /// as it represents a simple physical button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the button directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_buttons([*self]) + } } /// Retrieves the total mouse displacement. @@ -102,20 +103,6 @@ impl UserInput for MouseMoveDirection { InputKind::Button } - /// Creates a [`RawInputs`] from the direction directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_move_directions([*self]) - } - - /// Returns a list that only contains the [`MouseMoveDirection`] itself, - /// as it represents a simple virtual button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if there is any recent mouse movement along the specified direction. #[must_use] #[inline] @@ -138,6 +125,20 @@ impl UserInput for MouseMoveDirection { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a [`BasicInputs`] that only contains the [`MouseMoveDirection`] itself, + /// as it represents a simple virtual button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the direction directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_move_directions([*self]) + } } /// Captures ongoing mouse movement on a specific axis. @@ -197,23 +198,6 @@ impl UserInput for MouseMoveAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_move_axes([self.axis]) - } - - /// Returns the raw, unprocessed [`MouseMoveAxis`] for the same axis, - /// as it represents a simple axis input. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(MouseMoveDirection(self.axis.negative())), - Box::new(MouseMoveDirection(self.axis.positive())), - ] - } - /// Checks if there is any recent mouse movement along the specified axis. #[must_use] #[inline] @@ -238,6 +222,22 @@ impl UserInput for MouseMoveAxis { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns both negative and positive [`MouseMoveDirection`] to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(MouseMoveDirection(self.axis.negative())), + Box::new(MouseMoveDirection(self.axis.positive())), + ]) + } + + /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_move_axes([self.axis]) + } } impl WithAxisProcessingPipelineExt for MouseMoveAxis { @@ -296,24 +296,6 @@ impl UserInput for MouseMove { InputKind::DualAxis } - /// Creates a [`RawInputs`] from two [`DualAxisType`] used by the input. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_move_axes(DualAxisType::axes()) - } - - /// Returns two raw, unprocessed [`MouseMoveAxis`] instances to represent the movement. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(MouseMoveDirection::UP), - Box::new(MouseMoveDirection::DOWN), - Box::new(MouseMoveDirection::LEFT), - Box::new(MouseMoveDirection::RIGHT), - ] - } - /// Checks if there is any recent mouse movement. #[must_use] #[inline] @@ -343,6 +325,24 @@ impl UserInput for MouseMove { let value = self.processor.process(value); Some(DualAxisData::from_xy(value)) } + + /// Returns four [`MouseMoveDirection`]s to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(MouseMoveDirection::UP), + Box::new(MouseMoveDirection::DOWN), + Box::new(MouseMoveDirection::LEFT), + Box::new(MouseMoveDirection::RIGHT), + ]) + } + + /// Creates a [`RawInputs`] from two [`DualAxisType`]s used by the input. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_move_axes(DualAxisType::axes()) + } } impl WithDualAxisProcessingPipelineExt for MouseMove { @@ -406,20 +406,6 @@ impl UserInput for MouseScrollDirection { InputKind::Button } - /// Creates a [`RawInputs`] from the direction directly. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_scroll_directions([*self]) - } - - /// Returns a list that only contains the [`MouseScrollDirection`] itself, - /// as it represents a simple virtual button. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![Box::new(*self)] - } - /// Checks if there is any recent mouse wheel movement along the specified direction. #[must_use] #[inline] @@ -442,6 +428,20 @@ impl UserInput for MouseScrollDirection { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns a [`BasicInputs`] that only contains the [`MouseScrollDirection`] itself, + /// as it represents a simple virtual button. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Single(Box::new(*self)) + } + + /// Creates a [`RawInputs`] from the direction directly. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_scroll_directions([*self]) + } } /// Captures ongoing mouse wheel movement on a specific axis. @@ -501,22 +501,6 @@ impl UserInput for MouseScrollAxis { InputKind::Axis } - /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_scroll_axes([self.axis]) - } - - /// Returns both positive and negative [`MouseScrollDirection`]s to represent the movement. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(MouseScrollDirection(self.axis.negative())), - Box::new(MouseScrollDirection(self.axis.positive())), - ] - } - /// Checks if there is any recent mouse wheel movement along the specified axis. #[must_use] #[inline] @@ -541,6 +525,22 @@ impl UserInput for MouseScrollAxis { fn axis_pair(&self, _input_streams: &InputStreams) -> Option { None } + + /// Returns both positive and negative [`MouseScrollDirection`]s to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(MouseScrollDirection(self.axis.negative())), + Box::new(MouseScrollDirection(self.axis.positive())), + ]) + } + + /// Creates a [`RawInputs`] from the [`DualAxisType`] used by the axis. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_scroll_axes([self.axis]) + } } impl WithAxisProcessingPipelineExt for MouseScrollAxis { @@ -599,24 +599,6 @@ impl UserInput for MouseScroll { InputKind::DualAxis } - /// Creates a [`RawInputs`] from two [`DualAxisType`] used by the input. - #[inline] - fn to_raw_inputs(&self) -> RawInputs { - RawInputs::from_mouse_scroll_axes(DualAxisType::axes()) - } - - /// Returns two raw, unprocessed [`MouseScrollAxis`] instances to represent the movement. - #[must_use] - #[inline] - fn to_clashing_checker(&self) -> Vec> { - vec![ - Box::new(MouseScrollDirection::UP), - Box::new(MouseScrollDirection::DOWN), - Box::new(MouseScrollDirection::LEFT), - Box::new(MouseScrollDirection::RIGHT), - ] - } - /// Checks if there is any recent mouse wheel movement. #[must_use] #[inline] @@ -646,6 +628,24 @@ impl UserInput for MouseScroll { let value = self.processor.process(value); Some(DualAxisData::from_xy(value)) } + + /// Returns four [`MouseScrollDirection`]s to represent the movement. + #[must_use] + #[inline] + fn basic_inputs(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(MouseScrollDirection::UP), + Box::new(MouseScrollDirection::DOWN), + Box::new(MouseScrollDirection::LEFT), + Box::new(MouseScrollDirection::RIGHT), + ]) + } + + /// Creates a [`RawInputs`] from two [`DualAxisType`] used by the input. + #[inline] + fn raw_inputs(&self) -> RawInputs { + RawInputs::from_mouse_scroll_axes(DualAxisType::axes()) + } } impl WithDualAxisProcessingPipelineExt for MouseScroll { @@ -705,21 +705,15 @@ mod tests { fn test_mouse_button() { let left = MouseButton::Left; assert_eq!(left.kind(), InputKind::Button); - assert_eq!(left.to_raw_inputs(), RawInputs::from_mouse_buttons([left])); + assert_eq!(left.raw_inputs(), RawInputs::from_mouse_buttons([left])); let middle = MouseButton::Middle; assert_eq!(middle.kind(), InputKind::Button); - assert_eq!( - middle.to_raw_inputs(), - RawInputs::from_mouse_buttons([middle]) - ); + assert_eq!(middle.raw_inputs(), RawInputs::from_mouse_buttons([middle])); let right = MouseButton::Right; assert_eq!(right.kind(), InputKind::Button); - assert_eq!( - right.to_raw_inputs(), - RawInputs::from_mouse_buttons([right]) - ); + assert_eq!(right.raw_inputs(), RawInputs::from_mouse_buttons([right])); // No inputs let mut app = test_app(); @@ -762,17 +756,17 @@ mod tests { let mouse_move_up = MouseMoveDirection::UP; assert_eq!(mouse_move_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_mouse_move_directions([mouse_move_up]); - assert_eq!(mouse_move_up.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_move_up.raw_inputs(), raw_inputs); let mouse_move_y = MouseMoveAxis::Y; assert_eq!(mouse_move_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::Y]); - assert_eq!(mouse_move_y.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_move_y.raw_inputs(), raw_inputs); let mouse_move = MouseMove::RAW; assert_eq!(mouse_move.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::X, DualAxisType::Y]); - assert_eq!(mouse_move.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_move.raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); @@ -839,17 +833,17 @@ mod tests { let mouse_scroll_up = MouseScrollDirection::UP; assert_eq!(mouse_scroll_up.kind(), InputKind::Button); let raw_inputs = RawInputs::from_mouse_scroll_directions([mouse_scroll_up]); - assert_eq!(mouse_scroll_up.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll_up.raw_inputs(), raw_inputs); let mouse_scroll_y = MouseScrollAxis::Y; assert_eq!(mouse_scroll_y.kind(), InputKind::Axis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::Y]); - assert_eq!(mouse_scroll_y.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll_y.raw_inputs(), raw_inputs); let mouse_scroll = MouseScroll::RAW; assert_eq!(mouse_scroll.kind(), InputKind::DualAxis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::X, DualAxisType::Y]); - assert_eq!(mouse_scroll.to_raw_inputs(), raw_inputs); + assert_eq!(mouse_scroll.raw_inputs(), raw_inputs); // No inputs let zeros = Some(DualAxisData::ZERO); From 3fc96b4515aceb2216619b7c2d7cc5b26e6f85d1 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 22:06:13 +0800 Subject: [PATCH 12/20] Typo --- src/clashing_inputs.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 22d052b4..310e6741 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -71,7 +71,7 @@ impl BasicInputs { } } - /// Returns the number of the underlying [`UserInput`]s. + /// Returns the number of the logical [`UserInput`]s that make up the input. #[allow(clippy::len_without_is_empty)] #[inline] pub fn len(&self) -> usize { @@ -557,10 +557,11 @@ mod tests { action_data.insert(CtrlUp, action_datum.clone()); action_data.insert(MoveDPad, action_datum.clone()); - let streams = InputStreams::from_world(&app.world, None); - println!("World: {streams:?}"); - - input_map.handle_clashes(&mut action_data, &streams, ClashStrategy::PrioritizeLongest); + input_map.handle_clashes( + &mut action_data, + &InputStreams::from_world(&app.world, None), + ClashStrategy::PrioritizeLongest, + ); let mut expected = HashMap::new(); expected.insert(CtrlUp, action_datum); From e51967e190e8192398af38d4c3e4baf9290280ab Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 22:29:58 +0800 Subject: [PATCH 13/20] Docs --- RELEASES.md | 4 +++- src/clashing_inputs.rs | 1 + src/user_input/chord.rs | 1 - src/user_input/gamepad.rs | 6 ------ src/user_input/keyboard.rs | 5 ----- src/user_input/mod.rs | 5 +++++ src/user_input/mouse.rs | 7 ------- 7 files changed, 9 insertions(+), 20 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 69897198..975c3b72 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,7 +11,9 @@ - applied clashing check to continuous mouse inputs, for example: - `MouseScrollAxis::Y` will clash with `MouseScrollDirection::UP` and `MouseScrollDirection::DOWN`. - `MouseMove` will clash with all the two axes and the four directions. -- removed `InputMap::insert_chord` and `InputMap::insert_modified` methods since they’re a little bit +- removed `InputMap::insert_chord` and `InputMap::insert_modified` due to their limited applicability within the type system. + - the new `InputChord` contructors and builders allow you to define chords with guaranteed type safety. + - the new `ModifierKey::with` method simplifies the creation of input chords that include the modifier and your desired input. - removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). - renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. - removed `DeadZoneShape` in favor of new dead zone processors (see 'Enhancements: Input Processors' for details). diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 310e6741..b19b4ec7 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -48,6 +48,7 @@ impl ClashStrategy { /// The basic inputs that make up a [`UserInput`]. #[derive(Debug, Clone)] +#[must_use] pub enum BasicInputs { /// The input consists of a single, fundamental [`UserInput`]. /// In most cases, the input simply holds itself. diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index 6c02e6df..bb3c49c8 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -155,7 +155,6 @@ impl UserInput for InputChord { } /// Retrieves a list of simple, atomic [`UserInput`]s that compose the chord. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { let inputs = self diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index cb281165..9f63527a 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -133,7 +133,6 @@ impl UserInput for GamepadControlDirection { /// Returns a [`BasicInputs`] that only contains the [`GamepadControlDirection`] itself, /// as it represents a simple virtual button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -255,7 +254,6 @@ impl UserInput for GamepadControlAxis { } /// Returns both positive and negative [`GamepadControlDirection`]s to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ @@ -408,7 +406,6 @@ impl UserInput for GamepadStick { } /// Returns four [`GamepadControlDirection`]s to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ @@ -534,7 +531,6 @@ impl UserInput for GamepadButtonType { /// Creates a [`BasicInputs`] that only contains the [`GamepadButtonType`] itself, /// as it represents a simple physical button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -651,7 +647,6 @@ impl UserInput for GamepadVirtualAxis { } /// Returns the two [`GamepadButtonType`]s used by this axis. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![Box::new(self.negative), Box::new(self.positive)]) @@ -815,7 +810,6 @@ impl UserInput for GamepadVirtualDPad { } /// Returns the four [`GamepadButtonType`]s used by this D-pad. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index e7115637..57e3a9f6 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -74,7 +74,6 @@ impl UserInput for KeyboardKey { } /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { let keycodes = self @@ -148,7 +147,6 @@ impl UserInput for KeyCode { /// Returns a [`BasicInputs`] that only contains the [`KeyCode`] itself, /// as it represents a simple physical button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -253,7 +251,6 @@ impl UserInput for ModifierKey { } /// Returns the two [`KeyCode`]s used by this [`ModifierKey`]. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![Box::new(self.left()), Box::new(self.right())]) @@ -394,7 +391,6 @@ impl UserInput for KeyboardVirtualAxis { } /// Returns all the [`KeyCode`]s used by this axis. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { let keycodes = self @@ -572,7 +568,6 @@ impl UserInput for KeyboardVirtualDPad { } /// Returns all the [`KeyCode`]s used by this D-pad. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { let keycodes = self diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index 4ca62833..cd4ea6e7 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -29,6 +29,11 @@ //! These inputs provide separate X and Y values typically ranging from `-1.0` to `1.0`. //! Non-zero values are considered active. //! +//! ## Basic Inputs +//! +//! [`UserInput`]s use the method [`UserInput::basic_inputs`] returning a [`BasicInputs`] +//! used for clashing detection, see [clashing input check](crate::clashing_inputs) for details. +//! //! ## Raw Input Events //! //! [`UserInput`]s use the method [`UserInput::raw_inputs`] returning a [`RawInputs`] diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index 5264c4d9..81bb2135 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -49,7 +49,6 @@ impl UserInput for MouseButton { /// Returns a [`BasicInputs`] that only contains the [`MouseButton`] itself, /// as it represents a simple physical button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -128,7 +127,6 @@ impl UserInput for MouseMoveDirection { /// Returns a [`BasicInputs`] that only contains the [`MouseMoveDirection`] itself, /// as it represents a simple virtual button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -224,7 +222,6 @@ impl UserInput for MouseMoveAxis { } /// Returns both negative and positive [`MouseMoveDirection`] to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ @@ -327,7 +324,6 @@ impl UserInput for MouseMove { } /// Returns four [`MouseMoveDirection`]s to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ @@ -431,7 +427,6 @@ impl UserInput for MouseScrollDirection { /// Returns a [`BasicInputs`] that only contains the [`MouseScrollDirection`] itself, /// as it represents a simple virtual button. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Single(Box::new(*self)) @@ -527,7 +522,6 @@ impl UserInput for MouseScrollAxis { } /// Returns both positive and negative [`MouseScrollDirection`]s to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ @@ -630,7 +624,6 @@ impl UserInput for MouseScroll { } /// Returns four [`MouseScrollDirection`]s to represent the movement. - #[must_use] #[inline] fn basic_inputs(&self) -> BasicInputs { BasicInputs::Composite(vec![ From db95418a623e67474a5805d4a6bb14978c717f97 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 22:32:20 +0800 Subject: [PATCH 14/20] Rename --- src/clashing_inputs.rs | 16 ++++++++-------- src/user_input/gamepad.rs | 4 ++-- src/user_input/keyboard.rs | 2 +- src/user_input/mod.rs | 2 +- src/user_input/mouse.rs | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index b19b4ec7..a04d8772 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -52,7 +52,7 @@ impl ClashStrategy { pub enum BasicInputs { /// The input consists of a single, fundamental [`UserInput`]. /// In most cases, the input simply holds itself. - Single(Box), + Simple(Box), /// The input consists of multiple independent [`UserInput`]s. Composite(Vec>), @@ -66,7 +66,7 @@ impl BasicInputs { #[inline] pub fn inputs(&self) -> Vec> { match self.clone() { - Self::Single(input) => vec![input], + Self::Simple(input) => vec![input], Self::Composite(inputs) => inputs, Self::Group(inputs) => inputs, } @@ -77,7 +77,7 @@ impl BasicInputs { #[inline] pub fn len(&self) -> usize { match self { - Self::Single(_) => 1, + Self::Simple(_) => 1, Self::Composite(_) => 1, Self::Group(inputs) => inputs.len(), } @@ -87,17 +87,17 @@ impl BasicInputs { #[inline] pub fn clashed(&self, other: &BasicInputs) -> bool { match (self, other) { - (Self::Single(_), Self::Single(_)) => false, - (Self::Single(self_single), Self::Group(other_group)) => { + (Self::Simple(_), Self::Simple(_)) => false, + (Self::Simple(self_single), Self::Group(other_group)) => { other_group.len() > 1 && other_group.contains(self_single) } - (Self::Group(self_group), Self::Single(other_single)) => { + (Self::Group(self_group), Self::Simple(other_single)) => { self_group.len() > 1 && self_group.contains(other_single) } - (Self::Single(self_single), Self::Composite(other_composite)) => { + (Self::Simple(self_single), Self::Composite(other_composite)) => { other_composite.contains(self_single) } - (Self::Composite(self_composite), Self::Single(other_single)) => { + (Self::Composite(self_composite), Self::Simple(other_single)) => { self_composite.contains(other_single) } (Self::Composite(self_composite), Self::Group(other_group)) => { diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index 9f63527a..d9ed4ae5 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -135,7 +135,7 @@ impl UserInput for GamepadControlDirection { /// as it represents a simple virtual button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the direction directly. @@ -533,7 +533,7 @@ impl UserInput for GamepadButtonType { /// as it represents a simple physical button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the button directly. diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index 57e3a9f6..96d80ee8 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -149,7 +149,7 @@ impl UserInput for KeyCode { /// as it represents a simple physical button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the key directly. diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index cd4ea6e7..8044c3de 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -157,7 +157,7 @@ pub trait UserInput: /// Returns the most basic inputs that make up this input. /// /// For inputs that represent a simple, atomic control, - /// this method should always return a [`BasicInputs::Single`] that only contains the input itself. + /// this method should always return a [`BasicInputs::Simple`] that only contains the input itself. fn basic_inputs(&self) -> BasicInputs; /// Returns the raw input events that make up this input. diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index 81bb2135..f8e197cd 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -51,7 +51,7 @@ impl UserInput for MouseButton { /// as it represents a simple physical button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the button directly. @@ -129,7 +129,7 @@ impl UserInput for MouseMoveDirection { /// as it represents a simple virtual button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the direction directly. @@ -429,7 +429,7 @@ impl UserInput for MouseScrollDirection { /// as it represents a simple virtual button. #[inline] fn basic_inputs(&self) -> BasicInputs { - BasicInputs::Single(Box::new(*self)) + BasicInputs::Simple(Box::new(*self)) } /// Creates a [`RawInputs`] from the direction directly. From 94c12c556bd4246dccd2602113a0314562d32bbd Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sat, 4 May 2024 23:05:30 +0800 Subject: [PATCH 15/20] Docs --- src/plugin.rs | 21 ++++++++++++- src/user_input/mod.rs | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/plugin.rs b/src/plugin.rs index 881edfb3..92fd61e5 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; use bevy::app::{App, Plugin}; use bevy::ecs::prelude::*; use bevy::input::{ButtonState, InputSystem}; -use bevy::prelude::{PostUpdate, PreUpdate}; +use bevy::prelude::{GamepadButtonType, KeyCode, PostUpdate, PreUpdate}; use bevy::reflect::TypePath; #[cfg(feature = "ui")] @@ -19,6 +19,7 @@ use crate::clashing_inputs::ClashStrategy; use crate::input_map::InputMap; use crate::input_processing::*; use crate::timing::Timing; +use crate::user_input::*; use crate::Actionlike; /// A [`Plugin`] that collects [`ButtonInput`](bevy::input::ButtonInput) from disparate sources, @@ -165,6 +166,24 @@ impl Plugin for InputManagerPlugin { .register_type::() .register_type::() .register_type::() + // Inputs + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() + .register_user_input::() // Processors .register_type::() .register_type::() diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index 8044c3de..d235cc77 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -133,6 +133,76 @@ pub enum InputKind { /// A trait for defining the behavior expected from different user input sources. /// /// Implementers of this trait should provide methods for accessing and processing user input data. +/// +/// # Examples +/// +/// ```rust +/// use std::hash::{Hash, Hasher}; +/// use bevy::prelude::*; +/// use bevy::utils::FloatOrd; +/// use serde::{Deserialize, Serialize}; +/// use leafwing_input_manager::prelude::*; +/// use leafwing_input_manager::input_streams::InputStreams; +/// use leafwing_input_manager::axislike::{DualAxisType, DualAxisData}; +/// use leafwing_input_manager::raw_inputs::RawInputs; +/// use leafwing_input_manager::clashing_inputs::BasicInputs; +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +/// pub struct MouseScrollAlwaysFiveOnYAxis; +/// +/// // Add this attribute for ensuring proper serialization and deserialization. +/// #[serde_typetag] +/// impl UserInput for MouseScrollAlwaysFiveOnYAxis { +/// fn kind(&self) -> InputKind { +/// // Returns the kind of input this represents. +/// // +/// // In this case, it represents an axial input. +/// InputKind::Axis +/// } +/// +/// fn pressed(&self, input_streams: &InputStreams) -> bool { +/// // Checks if the input is currently active. +/// // +/// // Since this virtual mouse scroll always outputs a value, +/// // it will always return `true`. +/// true +/// } +/// +/// fn value(&self, input_streams: &InputStreams) -> f32 { +/// // Gets the current value of the input as an `f32`. +/// // +/// // This input always represents a scroll of `5.0` on the Y-axis. +/// 5.0 +/// } +/// +/// fn axis_pair(&self, input_streams: &InputStreams) -> Option { +/// // Gets the values of this input along the X and Y axes (if applicable). +/// // +/// // This input only represents movement on the Y-axis, +/// // so it returns `None`. +/// None +/// } +/// +/// fn basic_inputs(&self) -> BasicInputs { +/// // Gets the most basic form of this input for clashing input detection. +/// // +/// // This input is a simple, atomic unit, +/// // so it is returned as a `BasicInputs::Simple`. +/// BasicInputs::Simple(Box::new(*self)) +/// } +/// +/// fn raw_inputs(&self) -> RawInputs { +/// // Defines the raw input events used for simulating this input. +/// // +/// // This input simulates a mouse scroll event on the Y-axis. +/// RawInputs::from_mouse_scroll_axes([DualAxisType::Y]) +/// } +/// } +/// +/// // Remember to register your input - it will ensure everything works smoothly! +/// let mut app = App::new(); +/// app.register_user_input::(); +/// ``` pub trait UserInput: Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize { From 61ab859b5d48a76b1874590aa3c5481e5ae7836a Mon Sep 17 00:00:00 2001 From: Shute052 Date: Tue, 7 May 2024 13:19:57 +0800 Subject: [PATCH 16/20] Docs --- README.md | 2 +- RELEASES.md | 45 ++- examples/input_processing.rs | 2 +- examples/mouse_motion.rs | 2 +- examples/mouse_wheel.rs | 4 +- src/axislike.rs | 69 ---- src/clashing_inputs.rs | 12 +- src/input_map.rs | 20 +- src/input_mocking.rs | 238 ++++++++--- src/input_processing/dual_axis/mod.rs | 38 ++ src/input_processing/mod.rs | 8 + src/input_processing/single_axis/mod.rs | 35 ++ src/plugin.rs | 1 - src/user_input/chord.rs | 81 +++- src/user_input/gamepad.rs | 500 +++++++++++++----------- src/user_input/keyboard.rs | 424 ++++++++------------ src/user_input/mod.rs | 35 +- src/user_input/mouse.rs | 415 ++++++++++++-------- tests/mouse_motion.rs | 17 +- tests/mouse_wheel.rs | 13 +- 20 files changed, 1096 insertions(+), 865 deletions(-) diff --git a/README.md b/README.md index 2280b8cb..e3937729 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ and a single input can result in multiple actions being triggered, which can be - Ergonomic insertion API that seamlessly blends multiple input types for you - Can't decide between `input_map.insert(Action::Jump, KeyCode::Space)` and `input_map.insert(Action::Jump, GamepadButtonType::South)`? Have both! - Full support for arbitrary button combinations: chord your heart out. - - `input_map.insert(Action::Console, InputChord::multiple([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))` + - `input_map.insert(Action::Console, InputChord::from_multiple([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))` - Sophisticated input disambiguation with the `ClashStrategy` enum: stop triggering individual buttons when you meant to press a chord! - Create an arbitrary number of strongly typed disjoint action sets by adding multiple copies of this plugin: decouple your camera and player state - Local multiplayer support: freely bind keys to distinct entities, rather than worrying about singular global state diff --git a/RELEASES.md b/RELEASES.md index 975c3b72..8e2be9ae 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,18 +11,16 @@ - applied clashing check to continuous mouse inputs, for example: - `MouseScrollAxis::Y` will clash with `MouseScrollDirection::UP` and `MouseScrollDirection::DOWN`. - `MouseMove` will clash with all the two axes and the four directions. +- refactored the method signatures of `InputMap` to fit the new input types. - removed `InputMap::insert_chord` and `InputMap::insert_modified` due to their limited applicability within the type system. - the new `InputChord` contructors and builders allow you to define chords with guaranteed type safety. - the new `ModifierKey::with` method simplifies the creation of input chords that include the modifier and your desired input. - removed `InputMap::build` method in favor of new fluent builder pattern (see 'Usability: InputMap' for details). - renamed `InputMap::which_pressed` method to `process_actions` to better reflect its current functionality for clarity. - removed `DeadZoneShape` in favor of new dead zone processors (see 'Enhancements: Input Processors' for details). -- refactored the method signatures of `InputMap` to fit the new input types. - refactored the fields and methods of `RawInputs` to fit the new input types. - removed `Direction` type in favor of `bevy::math::primitives::Direction2d`. -- split `MockInput::send_input` method to two methods: - - `fn press_input(&self, input: impl UserInput)` for focusing on simulating button and key presses. - - `fn send_axis_values(&self, input: impl UserInput, values: impl IntoIterator)` for sending value changed events to each axis of the input. +- removed `MockInput::send_input` methods, in favor of new input mocking APIs (see 'Usability: MockInput' for details). - made the dependency on bevy's `bevy_gilrs` feature optional. - it is still enabled by leafwing-input-manager's default features. - if you're using leafwing-input-manager with `default_features = false`, you can readd it by adding `bevy/bevy_gilrs` as a dependency. @@ -43,9 +41,8 @@ - added `UserInput` impls for keyboard inputs: - implemented `UserInput` for Bevy’s `KeyCode` directly. - implemented `UserInput` for `ModifierKey`. - - added `KeyboardKey` enum, including individual `KeyCode`s, `ModifierKey`s, and checks for any key press. - - added `KeyboardVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `KeyboardKey`s. - - added `KeyboardVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `KeyboardKey`s. + - added `KeyboardVirtualAxis`, similar to the old `UserInput::VirtualAxis` using two `KeyCode`s. + - added `KeyboardVirtualDPad`, similar to the old `UserInput::VirtualDPad` using four `KeyCode`s. - added `UserInput` impls for mouse inputs: - implemented `UserInput` for movement-related inputs. - `MouseMove`: Continuous or discrete movement events of the mouse both X and Y axes. @@ -65,8 +62,8 @@ - `MouseScrollAxis::X` and `MouseScrollAxis::Y` for continuous mouse wheel movement. - the old `DualAxis` is now: - `GamepadStick` for gamepad sticks. - - `MouseMove::RAW` for continuous mouse movement. - - `MouseScroll::RAW` for continuous mouse wheel movement. + - `MouseMove::default()` for continuous mouse movement. + - `MouseScroll::default()` for continuous mouse wheel movement. - the old `Modifier` is now `ModifierKey`. - the old `MouseMotionDirection` is now `MouseMoveDirection`. - the old `MouseWheelDirection` is now `MouseScrollDirection`. @@ -74,13 +71,13 @@ - the old `UserInput::VirtualAxis` is now: - `GamepadVirtualAxis` for four gamepad buttons. - `KeyboardVirtualAxis` for four keys. - - `MouseMoveAxis::X_DIGITAL` and `MouseMoveAxis::X_DIGITAL` for discrete mouse movement. - - `MouseScrollAxis::X_DIGITAL` and `MouseScrollAxis::Y_DIGITAL` for discrete mouse wheel movement. + - `MouseMoveAxis::X.digital()` and `MouseMoveAxis::Y.digital()` for discrete mouse movement. + - `MouseScrollAxis::X.digital()` and `MouseScrollAxis::Y.digital()` for discrete mouse wheel movement. - the old `UserInput::VirtualDPad` is now: - `GamepadVirtualDPad` for four gamepad buttons. - `KeyboardVirtualDPad` for four keys. - - `MouseMove::DIGITAL` for discrete mouse movement. - - `MouseScroll::DIGITAL` for discrete mouse wheel movement. + - `MouseMove::default().digital()` for discrete mouse movement. + - `MouseScroll::default().digital()` for discrete mouse wheel movement. #### Input Processors @@ -100,6 +97,9 @@ Input processors allow you to create custom logic for axis-like input manipulati - you can also create them by these methods: - `AxisProcessor::pipeline` or `AxisProcessor::with_processor` for `AxisProcessor::Pipeline`. - `DualAxisProcessor::pipeline` or `DualAxisProcessor::with_processor` for `DualAxisProcessor::Pipeline`. + - Digital Conversion: Similar to `signum` but returning `0.0` for zero values: + - `AxisProcessor::Digital`: Single-axis digital conversion. + - `DualAxisProcessor::Digital`: Dual-axis digital conversion. - Inversion: Reverses control (positive becomes negative, etc.) - `AxisProcessor::Inverted`: Single-axis inversion. - `DualAxisInverted`: Dual-axis inversion, implemented `Into`. @@ -128,14 +128,29 @@ Input processors allow you to create custom logic for axis-like input manipulati - added new fluent builders for creating a new `InputMap` with short configurations: - `fn with(mut self, action: A, input: impl UserInput)`. - - `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. - - `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. + - `fn with_one_to_many(mut self, action: A, inputs: impl IntoIterator)`. + - `fn with_multiple(mut self, bindings: impl IntoIterator) -> Self`. - `fn with_gamepad(mut self, gamepad: Gamepad) -> Self`. - added new iterators over `InputMap`: - `actions(&self) -> impl Iterator` for iterating over all registered actions. - `bindings(&self) -> impl Iterator` for iterating over all registered action-input bindings. +### MockInput + +- added new methods for the `MockInput` trait. + - `fn press_input(&self, input: impl UserInput)` for simulating button and key presses. + - `fn send_axis_values(&self, input: impl UserInput, values: impl IntoIterator)` for sending value changed events to each axis represented by the input. + - as well as methods for a specific gamepad. +- implemented the methods for `MutableInputStreams`, `World`, and `App`. + +### QueryInput + +- added new methods for the `QueryInput` trait. + - `fn read_axis_values(&self, input: impl UserInput) -> Vec` to read the values on all axes represented by an input. + - as well as methods for a specific gamepad. +- implemented the methods for `InputStreams`, `World`, and `App`. + ### Bugs - fixed a bug in `InputStreams::button_pressed()` where unrelated gamepads were not filtered out when an `associated_gamepad` is defined. diff --git a/examples/input_processing.rs b/examples/input_processing.rs index b1916810..f1256078 100644 --- a/examples/input_processing.rs +++ b/examples/input_processing.rs @@ -45,7 +45,7 @@ fn spawn_player(mut commands: Commands) { .with( Action::LookAround, // You can also use a sequence of processors as the processing pipeline. - MouseMove::RAW.with_processor(DualAxisProcessor::pipeline([ + MouseMove::default().with_processor(DualAxisProcessor::pipeline([ // The first processor is a circular deadzone. CircleDeadZone::new(0.1).into(), // The next processor doubles inputs normalized by the deadzone. diff --git a/examples/mouse_motion.rs b/examples/mouse_motion.rs index 2468efc2..08218d0e 100644 --- a/examples/mouse_motion.rs +++ b/examples/mouse_motion.rs @@ -20,7 +20,7 @@ fn setup(mut commands: Commands) { // This will capture the total continuous value, for direct use. // Note that you can also use discrete gesture-like motion, // via the `MouseMoveDirection` enum. - (CameraMovement::Pan, MouseMove::RAW), + (CameraMovement::Pan, MouseMove::default()), ]); commands .spawn(Camera2dBundle::default()) diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index 0a6cb2a8..5b8a2b0e 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -27,9 +27,9 @@ fn setup(mut commands: Commands) { .with(CameraMovement::PanLeft, MouseScrollDirection::LEFT) .with(CameraMovement::PanRight, MouseScrollDirection::RIGHT) // Alternatively, you could model them as a continuous dual-axis input - .with(CameraMovement::Pan, MouseScroll::RAW) + .with(CameraMovement::Pan, MouseScroll::default()) // Or even a digital dual-axis input! - .with(CameraMovement::Pan, MouseScroll::DIGITAL); + .with(CameraMovement::Pan, MouseScroll::default().digital()); commands .spawn(Camera2dBundle::default()) .insert(InputManagerBundle::with_map(input_map)); diff --git a/src/axislike.rs b/src/axislike.rs index d7f03e29..89f6daec 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -5,75 +5,6 @@ use serde::{Deserialize, Serialize}; use crate::orientation::Rotation; -/// Different ways that user input is represented on an axis. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, Reflect)] -#[must_use] -pub enum AxisInputMode { - /// Continuous input values, typically range from a negative maximum (e.g., `-1.0`) - /// to a positive maximum (e.g., `1.0`), allowing for smooth and precise control. - #[default] - Analog, - - /// Discrete input values, using three distinct values to represent the states: - /// `-1.0` for active in negative direction, `0.0` for inactive, and `1.0` for active in positive direction. - Digital, -} - -impl AxisInputMode { - /// Converts the given `f32` value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0`, leaving others as is. - #[must_use] - #[inline] - pub fn axis_value(&self, value: f32) -> f32 { - match self { - Self::Analog => value, - Self::Digital => { - if value < 0.0 { - -1.0 - } else if value > 0.0 { - 1.0 - } else { - value - } - } - } - } - - /// Converts the given [`Vec2`] value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: Maps negative values to `-1.0` and positive values to `1.0` along each axis, leaving others as is. - #[must_use] - #[inline] - pub fn dual_axis_value(&self, value: Vec2) -> Vec2 { - match self { - Self::Analog => value, - Self::Digital => Vec2::new(self.axis_value(value.x), self.axis_value(value.y)), - } - } - - /// Computes the magnitude of given [`Vec2`] value based on the current [`AxisInputMode`]. - /// - /// # Returns - /// - /// - [`AxisInputMode::Analog`]: Leaves values as is. - /// - [`AxisInputMode::Digital`]: `1.0` for non-zero values, `0.0` for others. - #[must_use] - #[inline] - pub fn dual_axis_magnitude(&self, value: Vec2) -> f32 { - match self { - Self::Analog => value.length(), - Self::Digital => f32::from(value != Vec2::ZERO), - } - } -} - /// The directions for single-axis inputs. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index a04d8772..42a5e557 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -201,7 +201,7 @@ impl InputMap { for input_a in self.get(action_a)? { for input_b in self.get(action_b)? { - if input_a.basic_inputs().clashed(&input_b.basic_inputs()) { + if input_a.decompose().clashed(&input_b.decompose()) { clash.inputs_a.push(input_a.clone()); clash.inputs_b.push(input_b.clone()); } @@ -256,7 +256,7 @@ fn check_clash(clash: &Clash, input_streams: &InputStreams) -> .filter(|&input| input.pressed(input_streams)) { // If a clash was detected - if input_a.basic_inputs().clashed(&input_b.basic_inputs()) { + if input_a.decompose().clashed(&input_b.decompose()) { actual_clash.inputs_a.push(input_a.clone()); actual_clash.inputs_b.push(input_b.clone()); } @@ -294,7 +294,7 @@ fn resolve_clash( for reason_b in reasons_b_is_pressed.iter() { // If there is at least one non-clashing reason why these buttons should both be pressed, // we can avoid resolving the clash completely - if !reason_a.basic_inputs().clashed(&reason_b.basic_inputs()) { + if !reason_a.decompose().clashed(&reason_b.decompose()) { return None; } } @@ -308,13 +308,13 @@ fn resolve_clash( ClashStrategy::PrioritizeLongest => { let longest_a: usize = reasons_a_is_pressed .iter() - .map(|input| input.basic_inputs().len()) + .map(|input| input.decompose().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); let longest_b: usize = reasons_b_is_pressed .iter() - .map(|input| input.basic_inputs().len()) + .map(|input| input.decompose().len()) .reduce(|a, b| a.max(b)) .unwrap_or_default(); @@ -379,7 +379,7 @@ mod tests { } fn test_input_clash(input_a: impl UserInput, input_b: impl UserInput) -> bool { - input_a.basic_inputs().clashed(&input_b.basic_inputs()) + input_a.decompose().clashed(&input_b.decompose()) } mod basic_functionality { diff --git a/src/input_map.rs b/src/input_map.rs index d4c02b48..31a6a47b 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -46,7 +46,7 @@ use crate::Actionlike; /// ```rust /// use bevy::prelude::*; /// use leafwing_input_manager::prelude::*; -/// use leafwing_input_manager::user_input::InputKind; +/// use leafwing_input_manager::user_input::InputControlKind; /// /// // Define your actions. /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Reflect)] @@ -109,7 +109,7 @@ impl InputMap { /// This method ensures idempotence, meaning that adding the same input /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn new(bindings: impl IntoIterator) -> Self { + pub fn new(bindings: impl IntoIterator) -> Self { bindings .into_iter() .fold(Self::default(), |map, (action, input)| { @@ -134,10 +134,10 @@ impl InputMap { /// This method ensures idempotence, meaning that adding the same input /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn with_one_to_many( + pub fn with_one_to_many( mut self, action: A, - inputs: impl IntoIterator, + inputs: impl IntoIterator, ) -> Self { self.insert_one_to_many(action, inputs); self @@ -149,9 +149,9 @@ impl InputMap { /// This method ensures idempotence, meaning that adding the same input /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn with_multiple( + pub fn with_multiple( mut self, - bindings: impl IntoIterator, + bindings: impl IntoIterator, ) -> Self { self.insert_multiple(bindings); self @@ -195,10 +195,10 @@ impl InputMap { /// This method ensures idempotence, meaning that adding the same input /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn insert_one_to_many( + pub fn insert_one_to_many( &mut self, action: A, - inputs: impl IntoIterator, + inputs: impl IntoIterator, ) -> &mut Self { let inputs = inputs .into_iter() @@ -221,9 +221,9 @@ impl InputMap { /// This method ensures idempotence, meaning that adding the same input /// for the same action multiple times will only result in a single binding being created. #[inline(always)] - pub fn insert_multiple( + pub fn insert_multiple( &mut self, - bindings: impl IntoIterator, + bindings: impl IntoIterator, ) -> &mut Self { for (action, input) in bindings.into_iter() { self.insert(action, input); diff --git a/src/input_mocking.rs b/src/input_mocking.rs index cee6da56..5c3650f8 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -47,8 +47,7 @@ use crate::user_input::*; /// /// let mut app = App::new(); /// -/// // This functionality requires Bevy's InputPlugin to be present in your plugin set. -/// // If you included the `DefaultPlugins`, then InputPlugin is already included. +/// // This functionality requires Bevy's InputPlugin (included with DefaultPlugins) /// app.add_plugins(InputPlugin); /// /// // Press a key press directly. @@ -62,7 +61,7 @@ use crate::user_input::*; /// app.send_axis_values(MouseScrollAxis::Y, [5.0]); /// /// // Send values to two axes. -/// app.send_axis_values(MouseMove::RAW, [5.0, 8.0]); +/// app.send_axis_values(MouseMove::default(), [5.0, 8.0]); /// /// // Release or deactivate an input. /// app.release_input(KeyCode::KeyR); @@ -78,7 +77,7 @@ pub trait MockInput { /// pressing all buttons and keys in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, - /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. + /// like [`KeyCode`]s, [`ModifierKey`]s, and [`InputChord`]s. /// Axial inputs (e.g., analog thumb sticks) aren't affected. /// Use [`Self::send_axis_values`] for those. /// @@ -103,7 +102,7 @@ pub trait MockInput { /// pressing all buttons and keys in the [`RawInputs`](crate::raw_inputs::RawInputs) of the `input`. /// /// To avoid confusing adjustments, it is best to stick with straightforward button-like inputs, - /// like [`KeyCode`]s, [`KeyboardKey`]s, and [`InputChord`]s. + /// like [`KeyCode`]s, [`ModifierKey`]s, and [`InputChord`]s. /// Axial inputs (e.g., analog thumb sticks) aren't affected. /// Use [`Self::send_axis_values_as_gamepad`] for those. /// @@ -124,7 +123,7 @@ pub trait MockInput { /// Missing axis values default to `0.0`. /// /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs - /// like [`MouseScrollAxis::Y`], [`MouseMove::RAW`] and [`GamepadStick::LEFT`]. + /// like [`MouseScrollAxis::Y`], [`MouseMove`] and [`GamepadStick::LEFT`]. /// Non-axial inputs (e.g., keys and buttons) aren't affected; /// the current value will be retained for the next encountered axis. /// Use [`Self::press_input`] for those. @@ -144,7 +143,7 @@ pub trait MockInput { /// Missing axis values default to `0.0`. /// /// To avoid confusing adjustments, it is best to stick with straightforward axis-like inputs - /// like [`MouseScrollAxis::Y`], [`MouseMove::RAW`] and [`GamepadStick::LEFT`]. + /// like [`MouseScrollAxis::Y`], [`MouseMove`] and [`GamepadStick::LEFT`]. /// Non-axial inputs (e.g., keys and buttons) aren't affected; /// the current value will be retained for the next encountered axis. /// Use [`Self::press_input_as_gamepad`] for those. @@ -180,22 +179,69 @@ pub trait MockInput { fn reset_inputs(&mut self); } -/// Query [`ButtonInput`] state directly for testing purposes. +/// Query input state directly for testing purposes. /// /// In game code, you should (almost) always be using [`ActionState`](crate::action_state::ActionState) /// methods instead. +/// +/// # Examples +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::input_mocking::QueryInput; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// +/// // This functionality requires Bevy's InputPlugin (included with DefaultPlugins) +/// app.add_plugins(InputPlugin); +/// +/// // Check if a key is currently pressed down. +/// let pressed = app.pressed(KeyCode::KeyB); +/// +/// // Read the current vertical mouse scroll value. +/// let value = app.read_axis_values(MouseScrollAxis::Y); +/// +/// // Read the current changes in relative mouse X and Y coordinates. +/// let values = app.read_axis_values(MouseMove::default()); +/// let x = values[0]; +/// let y = values[1]; +/// ``` pub trait QueryInput { - /// Checks if the `input` is currently pressed. + /// Checks if the `input` is currently pressed or active. /// - /// This method is intended as a convenience for testing; check the [`ButtonInput`] resource directly, - /// or use an [`InputMap`](crate::input_map::InputMap) in real code. + /// This method is intended as a convenience for testing; + /// use an [`InputMap`](crate::input_map::InputMap) in real code. fn pressed(&self, input: impl UserInput) -> bool; - /// Checks if the `input` is currently pressed the specified [`Gamepad`]. + /// Checks if the `input` is currently pressed or active on the specified [`Gamepad`]. /// - /// This method is intended as a convenience for testing; check the [`ButtonInput`] resource directly, - /// or use an [`InputMap`](crate::input_map::InputMap) in real code. + /// This method is intended as a convenience for testing; + /// use an [`InputMap`](crate::input_map::InputMap) in real code. fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool; + + /// Retrieves the values on all axes represented by the `input`. + /// + /// Binary inputs (e.g., keys and buttons) are treated like single-axis inputs, + /// typically returning a value between `0.0` (not pressed) and `1.0` (fully pressed). + /// + /// This method is intended as a convenience for testing; + /// use an [`InputMap`](crate::input_map::InputMap) in real code. + fn read_axis_values(&self, input: impl UserInput) -> Vec; + + /// Retrieves the values on all axes represented by the `input` on the specified [`Gamepad`]. + /// + /// Binary inputs (e.g., keys and buttons) are treated like single-axis inputs, + /// typically returning a value between `0.0` (not pressed) and `1.0` (fully pressed). + /// + /// This method is intended as a convenience for testing; + /// use an [`InputMap`](crate::input_map::InputMap) in real code. + fn read_axis_values_on_gamepad( + &self, + input: impl UserInput, + gamepad: Option, + ) -> Vec; } /// Send fake UI interaction for testing purposes. @@ -245,7 +291,7 @@ impl MockInput for MutableInputStreams<'_> { self.send_gamepad_axis_value( gamepad, &direction.axis, - direction.direction.full_active_value(), + direction.side.full_active_value(), ); } @@ -402,15 +448,37 @@ impl MutableInputStreams<'_> { } impl QueryInput for InputStreams<'_> { + #[inline] fn pressed(&self, input: impl UserInput) -> bool { input.pressed(self) } + #[inline] fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { let mut input_streams = self.clone(); input_streams.associated_gamepad = gamepad; - input.pressed(&input_streams) + input_streams.pressed(input) + } + + #[inline] + fn read_axis_values(&self, input: impl UserInput) -> Vec { + if let Some(data) = input.axis_pair(self) { + return vec![data.x(), data.y()]; + } + + vec![input.value(self)] + } + + fn read_axis_values_on_gamepad( + &self, + input: impl UserInput, + gamepad: Option, + ) -> Vec { + let mut input_streams = self.clone(); + input_streams.associated_gamepad = gamepad; + + input_streams.read_axis_values(input) } } @@ -509,7 +577,21 @@ impl QueryInput for World { fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { let input_streams = InputStreams::from_world(self, gamepad); - input.pressed(&input_streams) + input_streams.pressed(input) + } + + fn read_axis_values(&self, input: impl UserInput) -> Vec { + self.read_axis_values_on_gamepad(input, None) + } + + fn read_axis_values_on_gamepad( + &self, + input: impl UserInput, + gamepad: Option, + ) -> Vec { + let input_streams = InputStreams::from_world(self, gamepad); + + input_streams.read_axis_values(input) } } @@ -576,6 +658,18 @@ impl QueryInput for App { fn pressed_on_gamepad(&self, input: impl UserInput, gamepad: Option) -> bool { self.world.pressed_on_gamepad(input, gamepad) } + + fn read_axis_values(&self, input: impl UserInput) -> Vec { + self.world.read_axis_values(input) + } + + fn read_axis_values_on_gamepad( + &self, + input: impl UserInput, + gamepad: Option, + ) -> Vec { + self.world.read_axis_values_on_gamepad(input, gamepad) + } } #[cfg(feature = "ui")] @@ -592,19 +686,35 @@ impl MockUIInteraction for App { #[cfg(test)] mod test { use crate::input_mocking::{MockInput, QueryInput}; - use bevy::{ - input::{ - gamepad::{GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo}, - InputPlugin, - }, - prelude::*, + use crate::user_input::*; + use bevy::input::gamepad::{ + GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo, }; + use bevy::input::InputPlugin; + use bevy::prelude::*; - #[test] - fn ordinary_button_inputs() { + fn test_app() -> App { let mut app = App::new(); app.add_plugins(InputPlugin); + let gamepad = Gamepad::new(0); + let mut gamepad_events = app.world.resource_mut::>(); + gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { + gamepad, + connection: GamepadConnection::Connected(GamepadInfo { + name: "TestController".into(), + }), + })); + app.update(); + app.update(); + + app + } + + #[test] + fn ordinary_button_inputs() { + let mut app = test_app(); + // Test that buttons are unpressed by default assert!(!app.pressed(KeyCode::Space)); assert!(!app.pressed(MouseButton::Right)); @@ -632,47 +742,30 @@ mod test { #[test] fn explicit_gamepad_button_inputs() { - let mut app = App::new(); - app.add_plugins(InputPlugin); - - let gamepad = Gamepad { id: 0 }; - let mut gamepad_events = app.world.resource_mut::>(); - gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { - gamepad, - connection: GamepadConnection::Connected(GamepadInfo { - name: "TestController".into(), - }), - })); - app.update(); + let mut app = test_app(); + let gamepad = Some(Gamepad::new(0)); // Test that buttons are unpressed by default - assert!(!app.pressed_on_gamepad(GamepadButtonType::North, Some(gamepad))); + assert!(!app.pressed_on_gamepad(GamepadButtonType::North, gamepad)); // Press buttons - app.press_input_as_gamepad(GamepadButtonType::North, Some(gamepad)); + app.press_input_as_gamepad(GamepadButtonType::North, gamepad); app.update(); + // Verify the button are pressed + assert!(app.pressed_on_gamepad(GamepadButtonType::North, gamepad)); + // Test that resetting inputs works app.reset_inputs(); app.update(); - assert!(!app.pressed_on_gamepad(GamepadButtonType::North, Some(gamepad))); + // Verify the button are released + assert!(!app.pressed_on_gamepad(GamepadButtonType::North, gamepad)); } #[test] fn implicit_gamepad_button_inputs() { - let mut app = App::new(); - app.add_plugins(InputPlugin); - - let gamepad = Gamepad { id: 0 }; - let mut gamepad_events = app.world.resource_mut::>(); - gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent { - gamepad, - connection: GamepadConnection::Connected(GamepadInfo { - name: "TestController".into(), - }), - })); - app.update(); + let mut app = test_app(); // Test that buttons are unpressed by default assert!(!app.pressed(GamepadButtonType::North)); @@ -681,16 +774,55 @@ mod test { app.press_input(GamepadButtonType::North); app.update(); - // Test the convenient `pressed` API + // Verify the button are pressed assert!(app.pressed(GamepadButtonType::North)); // Test that resetting inputs works app.reset_inputs(); app.update(); + // Verify the button are released assert!(!app.pressed(GamepadButtonType::North)); } + #[test] + fn mouse_inputs() { + let mut app = test_app(); + + // Mouse axes should be inactive by default (no scroll or movement) + assert_eq!(app.read_axis_values(MouseMove::default()), [0.0, 0.0]); + assert_eq!(app.read_axis_values(MouseScroll::default()), [0.0, 0.0]); + + // Send a simulated mouse scroll event with a value of 3 (positive for up) + app.send_axis_values(MouseScrollAxis::Y, [3.0]); + app.update(); + + // Verify the mouse wheel Y axis reflects the simulated scroll + // and the other axis isn't affected + assert_eq!(app.read_axis_values(MouseScrollAxis::X), [0.0]); + assert_eq!(app.read_axis_values(MouseScrollAxis::Y), [3.0]); + assert_eq!(app.read_axis_values(MouseScroll::default()), [0.0, 3.0]); + + // Send a simulated mouse movement event with a delta of (3.0, 2.0) + app.send_axis_values(MouseScroll::default(), [3.0, 2.0]); + app.update(); + + // Verify the mouse motion axes reflects the simulated movement + assert_eq!(app.read_axis_values(MouseScroll::default()), [3.0, 2.0]); + + // Mouse input data is typically reset every frame + // Verify other axes aren't affected + assert_eq!(app.read_axis_values(MouseMove::default()), [0.0, 0.0]); + + // Test that resetting inputs works + app.reset_inputs(); + app.update(); + + // Verify all axes have no value after reset + assert_eq!(app.read_axis_values(MouseScrollAxis::Y), [0.0]); + assert_eq!(app.read_axis_values(MouseScroll::default()), [0.0, 0.0]); + } + #[test] #[cfg(feature = "ui")] fn ui_inputs() { diff --git a/src/input_processing/dual_axis/mod.rs b/src/input_processing/dual_axis/mod.rs index 61609f51..d05c8874 100644 --- a/src/input_processing/dual_axis/mod.rs +++ b/src/input_processing/dual_axis/mod.rs @@ -7,6 +7,8 @@ use bevy::prelude::{BVec2, Reflect, Vec2}; use bevy::utils::FloatOrd; use serde::{Deserialize, Serialize}; +use crate::input_processing::AxisProcessor; + pub use self::circle::*; pub use self::custom::*; pub use self::range::*; @@ -25,6 +27,31 @@ pub enum DualAxisProcessor { #[default] None, + /// Converts input values into three discrete values along each axis, + /// similar to [`Vec2::signum()`] but returning `0.0` for zero values. + /// + /// ```rust + /// use bevy::prelude::*; + /// use leafwing_input_manager::prelude::*; + /// + /// // 1.0 for positive values + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::splat(2.5)), Vec2::ONE); + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::splat(0.5)), Vec2::ONE); + /// + /// // 0.0 for zero values + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::ZERO), Vec2::ZERO); + /// assert_eq!(DualAxisProcessor::Digital.process(-Vec2::ZERO), Vec2::ZERO); + /// + /// // -1.0 for negative values + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::splat(-0.5)), Vec2::NEG_ONE); + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::splat(-2.5)), Vec2::NEG_ONE); + /// + /// // Mixed digital values + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::new(0.5, -0.5)), Vec2::new(1.0, -1.0)); + /// assert_eq!(DualAxisProcessor::Digital.process(Vec2::new(-0.5, 0.5)), Vec2::new(-1.0, 1.0)); + /// ``` + Digital, + /// A wrapper around [`DualAxisInverted`] to represent inversion. Inverted(DualAxisInverted), @@ -95,6 +122,10 @@ impl DualAxisProcessor { pub fn process(&self, input_value: Vec2) -> Vec2 { match self { Self::None => input_value, + Self::Digital => Vec2::new( + AxisProcessor::Digital.process(input_value.x), + AxisProcessor::Digital.process(input_value.y), + ), Self::Inverted(inversion) => inversion.invert(input_value), Self::Sensitivity(sensitivity) => sensitivity.scale(input_value), Self::ValueBounds(bounds) => bounds.clamp(input_value), @@ -158,6 +189,13 @@ pub trait WithDualAxisProcessingPipelineExt: Sized { /// Appends the given [`DualAxisProcessor`] as the next processing step. fn with_processor(self, processor: impl Into) -> Self; + /// Appends an [`DualAxisProcessor::Digital`] processor as the next processing step, + /// similar to [`Vec2::signum`] but returning `0.0` for zero values. + #[inline] + fn digital(self) -> Self { + self.with_processor(DualAxisProcessor::Digital) + } + /// Appends a [`DualAxisInverted::ALL`] processor as the next processing step, /// flipping the sign of values on both axes. #[inline] diff --git a/src/input_processing/mod.rs b/src/input_processing/mod.rs index c04bff79..80522dd3 100644 --- a/src/input_processing/mod.rs +++ b/src/input_processing/mod.rs @@ -29,6 +29,14 @@ //! - [`AxisProcessor::pipeline`] or [`AxisProcessor::with_processor`] for [`AxisProcessor::Pipeline`]. //! - [`DualAxisProcessor::pipeline`] or [`DualAxisProcessor::with_processor`] for [`DualAxisProcessor::Pipeline`]. //! +//! ## Digital Conversion +//! +//! Digital processors convert raw input values into discrete values, +//! similar to [`f32::signum`] but returning `0.0` for zero values. +//! +//! - [`AxisProcessor::Digital`]: Single-axis digital conversion. +//! - [`DualAxisProcessor::Digital`]: Dual-axis digital conversion. +//! //! ## Inversion //! //! Inversion flips the sign of input values, resulting in a directional reversal of control. diff --git a/src/input_processing/single_axis/mod.rs b/src/input_processing/single_axis/mod.rs index f26b652c..dce25aed 100644 --- a/src/input_processing/single_axis/mod.rs +++ b/src/input_processing/single_axis/mod.rs @@ -23,6 +23,26 @@ pub enum AxisProcessor { #[default] None, + /// Converts input values into three discrete values, + /// similar to [`f32::signum()`] but returning `0.0` for zero values. + /// + /// ```rust + /// use leafwing_input_manager::prelude::*; + /// + /// // 1.0 for positive values + /// assert_eq!(AxisProcessor::Digital.process(2.5), 1.0); + /// assert_eq!(AxisProcessor::Digital.process(0.5), 1.0); + /// + /// // 0.0 for zero values + /// assert_eq!(AxisProcessor::Digital.process(0.0), 0.0); + /// assert_eq!(AxisProcessor::Digital.process(-0.0), 0.0); + /// + /// // -1.0 for negative values + /// assert_eq!(AxisProcessor::Digital.process(-0.5), -1.0); + /// assert_eq!(AxisProcessor::Digital.process(-2.5), -1.0); + /// ``` + Digital, + /// Flips the sign of input values, resulting in a directional reversal of control. /// /// ```rust @@ -104,6 +124,13 @@ impl AxisProcessor { pub fn process(&self, input_value: f32) -> f32 { match self { Self::None => input_value, + Self::Digital => { + if input_value == 0.0 { + 0.0 + } else { + input_value.signum() + } + } Self::Inverted => -input_value, Self::Sensitivity(sensitivity) => sensitivity * input_value, Self::ValueBounds(bounds) => bounds.clamp(input_value), @@ -160,6 +187,7 @@ impl Hash for AxisProcessor { std::mem::discriminant(self).hash(state); match self { Self::None => {} + Self::Digital => {} Self::Inverted => {} Self::Sensitivity(sensitivity) => FloatOrd(*sensitivity).hash(state), Self::ValueBounds(bounds) => bounds.hash(state), @@ -182,6 +210,13 @@ pub trait WithAxisProcessingPipelineExt: Sized { /// Appends the given [`AxisProcessor`] as the next processing step. fn with_processor(self, processor: impl Into) -> Self; + /// Appends an [`AxisProcessor::Digital`] processor as the next processing step, + /// similar to [`f32::signum`] but returning `0.0` for zero values. + #[inline] + fn digital(self) -> Self { + self.with_processor(AxisProcessor::Digital) + } + /// Appends an [`AxisProcessor::Inverted`] processor as the next processing step, /// flipping the sign of values on the axis. #[inline] diff --git a/src/plugin.rs b/src/plugin.rs index 92fd61e5..a33e8057 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -175,7 +175,6 @@ impl Plugin for InputManagerPlugin { .register_user_input::() .register_user_input::() .register_user_input::() - .register_user_input::() .register_user_input::() .register_user_input::() .register_user_input::() diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index bb3c49c8..61453190 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -8,19 +8,10 @@ use crate as leafwing_input_manager; use crate::clashing_inputs::BasicInputs; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; -use crate::user_input::{DualAxisData, InputKind, UserInput}; +use crate::user_input::{DualAxisData, InputControlKind, UserInput}; /// A combined input that groups multiple [`UserInput`]s together, -/// which is useful for creating input combinations like hotkeys, shortcuts, and macros. -/// -/// # Behaviors -/// -/// - Simultaneous Activation Check: You can check if all the included inputs -/// are actively pressed at the same time. -/// - Single-Axis Input Combination: If some inner inputs are single-axis (like mouse wheel), -/// the input chord can combine (sum) their values into a single value. -/// - First Dual-Axis Input Only: Retrieves the values only from the first included -/// dual-axis input (like gamepad triggers). The state of other dual-axis inputs is ignored. +/// allowing you to define complex input combinations like hotkeys, shortcuts, and macros. /// /// # Warning /// @@ -30,6 +21,58 @@ use crate::user_input::{DualAxisData, InputKind, UserInput}; /// When using an input chord within another input that can hold multiple [`UserInput`]s, /// the chord itself will always be treated as a button. /// Any additional functionalities it offered (like single-axis values) will be ignored in this context. +/// +/// # Behaviors +/// +/// - Activation: All included inputs must be active simultaneously. +/// - Single-Axis Value: +/// - If the chord has single-axis inputs, their values are summed into a single value. +/// - Otherwise, it acts like a button (`1.0` when active and `0.0` when inactive). +/// - Dual-Axis Value: Retrieves the values only from the *first* included dual-axis input (others ignored). +/// - Deduplication: Adding duplicate inputs within a chord will ignore the extras, +/// preventing redundant data fetching. +/// - Nesting: Using an input chord within another multi-input element treats it as a single button, +/// ignoring its individual functionalities (like single-axis values). +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Define a chord using A and B keys +/// let input = InputChord::from_multiple([KeyCode::KeyA, KeyCode::KeyB]); +/// +/// // Pressing only one key doesn't activate the input +/// app.press_input(KeyCode::KeyA); +/// app.update(); +/// assert!(!app.pressed(input.clone())); +/// +/// // Pressing both keys activates the input +/// app.press_input(KeyCode::KeyB); +/// app.update(); +/// assert!(app.pressed(input.clone())); +/// +/// // Define a new chord with both axes for mouse movement. +/// let input = input.with_multiple([MouseMoveAxis::X, MouseMoveAxis::Y]); +/// +/// // Note that this chord only reports a combined single-axis value. +/// // because it constructed from two single-axis inputs, not one dual-axis input. +/// app.send_axis_values(MouseMove::default(), [2.0, 3.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input.clone()), [5.0]); +/// +/// // Define a new chord with two dual-axis inputs. +/// let input = input.with(MouseMove::default()).with(MouseScroll::default()); +/// +/// // Note that this chord only reports the value from the first included dual-axis input. +/// app.send_axis_values(MouseMove::default(), [2.0, 3.0]); +/// app.send_axis_values(MouseScroll::default(), [4.0, 5.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [2.0, 3.0]); +/// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct InputChord( @@ -97,10 +140,10 @@ impl InputChord { #[serde_typetag] impl UserInput for InputChord { - /// [`InputChord`] always acts as a virtual button. + /// [`InputChord`] acts as a virtual button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if all the inner inputs within the chord are active simultaneously. @@ -130,7 +173,7 @@ impl UserInput for InputChord { let mut has_axis = false; let mut axis_value = 0.0; for input in self.0.iter() { - if input.kind() == InputKind::Axis { + if input.kind() == InputControlKind::Axis { has_axis = true; axis_value += input.value(input_streams); } @@ -149,18 +192,18 @@ impl UserInput for InputChord { fn axis_pair(&self, input_streams: &InputStreams) -> Option { self.0 .iter() - .filter(|input| input.kind() == InputKind::DualAxis) + .filter(|input| input.kind() == InputControlKind::DualAxis) .flat_map(|input| input.axis_pair(input_streams)) .next() } /// Retrieves a list of simple, atomic [`UserInput`]s that compose the chord. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { let inputs = self .0 .iter() - .flat_map(|input| input.basic_inputs().inputs()) + .flat_map(|input| input.decompose().inputs()) .collect(); BasicInputs::Group(inputs) } @@ -359,7 +402,7 @@ mod tests { let data = DualAxisData::new(2.0, 3.0); let mut app = test_app(); - app.send_axis_values(MouseScroll::RAW, [data.x(), data.y()]); + app.send_axis_values(MouseScroll::default(), [data.x(), data.y()]); app.update(); let inputs = InputStreams::from_world(&app.world, None); check(&chord, &inputs, false, data.x() + data.y(), zeros); diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index d9ed4ae5..f8b0de50 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -7,7 +7,7 @@ use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; -use crate::axislike::{AxisDirection, AxisInputMode, DualAxisData}; +use crate::axislike::{AxisDirection, DualAxisData}; use crate::clashing_inputs::BasicInputs; use crate::input_processing::{ AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt, @@ -15,7 +15,7 @@ use crate::input_processing::{ }; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; -use crate::user_input::{InputKind, UserInput}; +use crate::user_input::{InputControlKind, UserInput}; /// Retrieves the current value of the specified `axis`. #[must_use] @@ -38,86 +38,109 @@ fn read_axis_value(input_streams: &InputStreams, axis: GamepadAxisType) -> f32 { } } -/// Captures values from a specified [`GamepadAxisType`] on a specific direction, -/// treated as a button press. +/// Provides button-like behavior for a specific direction on a [`GamepadAxisType`]. +/// +/// # Behaviors +/// +/// - Gamepad Selection: By default, reads from **any connected gamepad**. +/// Use the [`InputMap::set_gamepad`] for specific ones. +/// - Activation: Only if the axis is currently held in the chosen direction. +/// - Single-Axis Value: +/// - `1.0`: The input is currently active. +/// - `0.0`: The input is inactive. +/// +/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad +/// +/// ```rust,ignore +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use bevy::input::gamepad::GamepadEvent; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Positive Y-axis movement on left stick +/// let input = GamepadControlDirection::LEFT_UP; +/// +/// // Movement in the opposite direction doesn't activate the input +/// app.send_axis_values(GamepadControlAxis::LEFT_Y, [-1.0]); +/// app.update(); +/// assert!(!app.pressed(input)); +/// +/// // Movement in the chosen direction activates the input +/// app.send_axis_values(GamepadControlAxis::LEFT_Y, [1.0]); +/// app.update(); +/// assert!(app.pressed(input)); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadControlDirection { - /// The [`GamepadAxisType`] that this input tracks. + /// The axis that this input tracks. pub(crate) axis: GamepadAxisType, - /// The direction of the axis. - pub(crate) direction: AxisDirection, + /// The direction of the axis to monitor (positive or negative). + pub(crate) side: AxisDirection, } impl GamepadControlDirection { - /// Creates a negative [`GamepadControlDirection`] of the given `axis`. + /// Creates a [`GamepadControlDirection`] triggered by a negative value on the specified `axis`. #[inline] pub const fn negative(axis: GamepadAxisType) -> Self { - Self { - axis, - direction: AxisDirection::Negative, - } + let side = AxisDirection::Negative; + Self { axis, side } } - /// Creates a negative [`GamepadControlDirection`] of the given `axis`. + /// Creates a [`GamepadControlDirection`] triggered by a positive value on the specified `axis`. #[inline] pub const fn positive(axis: GamepadAxisType) -> Self { - Self { - axis, - direction: AxisDirection::Positive, - } + let side = AxisDirection::Positive; + Self { axis, side } } - /// The upward [`GamepadControlDirection`] of the left stick. + /// "Up" on the left analog stick (positive Y-axis movement). pub const LEFT_UP: Self = Self::positive(GamepadAxisType::LeftStickY); - /// The downward [`GamepadControlDirection`] of the left stick. + /// "Down" on the left analog stick (negative Y-axis movement). pub const LEFT_DOWN: Self = Self::negative(GamepadAxisType::LeftStickY); - /// The leftward [`GamepadControlDirection`] of the left stick. + /// "Left" on the left analog stick (negative X-axis movement). pub const LEFT_LEFT: Self = Self::negative(GamepadAxisType::LeftStickX); - /// The rightward [`GamepadControlDirection`] of the left stick. + /// "Right" on the left analog stick (positive X-axis movement). pub const LEFT_RIGHT: Self = Self::positive(GamepadAxisType::LeftStickX); - /// The upward [`GamepadControlDirection`] of the right stick. + /// "Up" on the right analog stick (positive Y-axis movement). pub const RIGHT_UP: Self = Self::positive(GamepadAxisType::RightStickY); - /// The downward [`GamepadControlDirection`] of the right stick. + /// "Down" on the right analog stick (positive Y-axis movement). pub const RIGHT_DOWN: Self = Self::negative(GamepadAxisType::RightStickY); - /// The leftward [`GamepadControlDirection`] of the right stick. + /// "Left" on the right analog stick (positive X-axis movement). pub const RIGHT_LEFT: Self = Self::negative(GamepadAxisType::RightStickX); - /// The rightward [`GamepadControlDirection`] of the right stick. + /// "Right" on the right analog stick (positive X-axis movement). pub const RIGHT_RIGHT: Self = Self::positive(GamepadAxisType::RightStickX); } #[serde_typetag] impl UserInput for GamepadControlDirection { - /// [`GamepadControlDirection`] always acts as a virtual button. + /// [`GamepadControlDirection`] acts as a virtual button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if there is any recent stick movement along the specified direction. - /// - /// When a [`Gamepad`] is specified, only checks the movement on the specified gamepad. - /// Otherwise, checks the movement on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { let value = read_axis_value(input_streams, self.axis); - self.direction.is_active(value) + self.side.is_active(value) } /// Retrieves the amount of the stick movement along the specified direction, /// returning `0.0` for no movement and `1.0` for full movement. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { @@ -131,10 +154,9 @@ impl UserInput for GamepadControlDirection { None } - /// Returns a [`BasicInputs`] that only contains the [`GamepadControlDirection`] itself, - /// as it represents a simple virtual button. + /// [`GamepadControlDirection`] represents a simple virtual button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -145,18 +167,46 @@ impl UserInput for GamepadControlDirection { } } -/// Captures values from a specified [`GamepadAxisType`]. +/// A wrapper around a specific [`GamepadAxisType`] (e.g., left stick X-axis, right stick Y-axis). +/// +/// # Behaviors +/// +/// - Gamepad Selection: By default, reads from **any connected gamepad**. +/// Use the [`InputMap::set_gamepad`] for specific ones. +/// - Raw Value: Captures the raw value on the axis, ranging from `-1.0` to `1.0`. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithAxisProcessingPipelineExt`] for details. +/// - Activation: Only if the processed value is non-zero. +/// +/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad +/// +/// ```rust,ignore +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Y-axis movement on left stick +/// let input = GamepadControlAxis::LEFT_Y; +/// +/// // Movement on the chosen axis activates the input +/// app.send_axis_values(GamepadControlAxis::LEFT_Y, [1.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the value) +/// let doubled = GamepadControlAxis::LEFT_Y.sensitivity(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadControlAxis { - /// The [`GamepadAxisType`] that this input tracks. + /// The wrapped axis. pub(crate) axis: GamepadAxisType, - /// The method to interpret values on the axis, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`AxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: AxisProcessor, } @@ -165,69 +215,42 @@ impl GamepadControlAxis { /// No processing is applied to raw data from the gamepad. #[inline] pub const fn new(axis: GamepadAxisType) -> Self { - Self { - axis, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - } - } - - /// Creates a [`GamepadControlAxis`] for discrete input from the given axis. - /// No processing is applied to raw data from the gamepad. - #[inline] - pub const fn digital(axis: GamepadAxisType) -> Self { - Self { - axis, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - } + let processor = AxisProcessor::None; + Self { axis, processor } } - /// The horizontal [`GamepadControlAxis`] for continuous input from the left stick. + /// The horizontal axis (X-axis) of the left stick. /// No processing is applied to raw data from the gamepad. pub const LEFT_X: Self = Self::new(GamepadAxisType::LeftStickX); - /// The vertical [`GamepadControlAxis`] for continuous input from the left stick. + /// The vertical axis (Y-axis) of the left stick. /// No processing is applied to raw data from the gamepad. pub const LEFT_Y: Self = Self::new(GamepadAxisType::LeftStickY); - /// The horizontal [`GamepadControlAxis`] for continuous input from the right stick. + /// The left `Z` button. No processing is applied to raw data from the gamepad. + pub const LEFT_Z: Self = Self::new(GamepadAxisType::LeftZ); + + /// The horizontal axis (X-axis) of the right stick. /// No processing is applied to raw data from the gamepad. pub const RIGHT_X: Self = Self::new(GamepadAxisType::RightStickX); - /// The vertical [`GamepadControlAxis`] for continuous input from the right stick. + /// The vertical axis (Y-axis) of the right stick. /// No processing is applied to raw data from the gamepad. pub const RIGHT_Y: Self = Self::new(GamepadAxisType::RightStickY); - /// The horizontal [`GamepadControlAxis`] for discrete input from the left stick. - /// No processing is applied to raw data from the gamepad. - pub const LEFT_X_DIGITAL: Self = Self::digital(GamepadAxisType::LeftStickX); - - /// The vertical [`GamepadControlAxis`] for discrete input from the left stick. - /// No processing is applied to raw data from the gamepad. - pub const LEFT_Y_DIGITAL: Self = Self::digital(GamepadAxisType::LeftStickY); - - /// The horizontal [`GamepadControlAxis`] for discrete input from the right stick. - /// No processing is applied to raw data from the gamepad. - pub const RIGHT_X_DIGITAL: Self = Self::digital(GamepadAxisType::RightStickX); - - /// The vertical [`GamepadControlAxis`] for discrete input from the right stick. - /// No processing is applied to raw data from the gamepad. - pub const RIGHT_Y_DIGITAL: Self = Self::digital(GamepadAxisType::RightStickY); + /// The right `Z` button. No processing is applied to raw data from the gamepad. + pub const RIGHT_Z: Self = Self::new(GamepadAxisType::RightZ); } #[serde_typetag] impl UserInput for GamepadControlAxis { - /// [`GamepadControlAxis`] always acts as an axis input. + /// [`GamepadControlAxis`] acts as an axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::Axis + fn kind(&self) -> InputControlKind { + InputControlKind::Axis } /// Checks if this axis has a non-zero value. - /// - /// When a [`Gamepad`] is specified, only checks if the axis is active on the specified gamepad. - /// Otherwise, checks if the axis is active on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -235,14 +258,10 @@ impl UserInput for GamepadControlAxis { } /// Retrieves the current value of this axis after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { let value = read_axis_value(input_streams, self.axis); - let value = self.input_mode.axis_value(value); self.processor.process(value) } @@ -253,9 +272,9 @@ impl UserInput for GamepadControlAxis { None } - /// Returns both positive and negative [`GamepadControlDirection`]s to represent the movement. + /// [`GamepadControlAxis`] represents a composition of two [`GamepadControlDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(GamepadControlDirection::negative(self.axis)), Box::new(GamepadControlDirection::positive(self.axis)), @@ -289,94 +308,87 @@ impl WithAxisProcessingPipelineExt for GamepadControlAxis { } } -/// Captures values from two specified [`GamepadAxisType`]s as the X and Y axes. +/// A gamepad stick (e.g., left stick and right stick). +/// +/// # Behaviors +/// +/// - Gamepad Selection: By default, reads from **any connected gamepad**. +/// Use the [`InputMap::set_gamepad`] for specific ones. +/// - Raw Value: Captures the raw value on both axes, ranging from `-1.0` to `1.0`. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithDualAxisProcessingPipelineExt`] for details. +/// - Activation: Only if its processed value is non-zero on either axis. +/// - Single-Axis Value: Reports the magnitude of the processed value. +/// +/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad +/// +/// ```rust,ignore +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Left stick +/// let input = GamepadStick::LEFT; +/// +/// // Movement on either axis activates the input +/// app.send_axis_values(GamepadControlAxis::LEFT_Y, [1.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [0.0, 1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the Y value) +/// let doubled = GamepadStick::LEFT.sensitivity_y(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadStick { - /// The [`GamepadAxisType`] used for the X-axis. + /// Horizontal movement of the stick. pub(crate) x: GamepadAxisType, - /// The [`GamepadAxisType`] used for the Y-axis. + /// Vertical movement of the stick. pub(crate) y: GamepadAxisType, - /// The method to interpret values on both axes, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`DualAxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: DualAxisProcessor, } impl GamepadStick { - /// Creates a [`GamepadStick`] for continuous input from two given axes as the X and Y axes. - /// No processing is applied to raw data from the gamepad. - #[inline] - pub const fn new(x: GamepadAxisType, y: GamepadAxisType) -> Self { - Self { - x, - y, - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - } - } - - /// Creates a [`GamepadStick`] for discrete input from two given axes as the X and Y axes. - /// No processing is applied to raw data from the gamepad. - #[inline] - pub const fn digital(x: GamepadAxisType, y: GamepadAxisType) -> Self { - Self { - x, - y, - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - } - } - - /// The left [`GamepadStick`] for continuous input on the X and Y axes. - /// No processing is applied to raw data from the gamepad. - pub const LEFT: Self = Self::new(GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY); - - /// The right [`GamepadStick`] for continuous input on the X and Y axes. - /// No processing is applied to raw data from the gamepad. - pub const RIGHT: Self = Self::new(GamepadAxisType::RightStickX, GamepadAxisType::RightStickY); - - /// The left [`GamepadStick`] for discrete input from the left stick on the X and Y axes. - /// No processing is applied to raw data from the gamepad. - pub const LEFT_DIGITAL: Self = - Self::digital(GamepadAxisType::LeftStickX, GamepadAxisType::LeftStickY); + /// The left gamepad stick. No processing is applied to raw data from the gamepad. + pub const LEFT: Self = Self { + x: GamepadAxisType::LeftStickX, + y: GamepadAxisType::LeftStickY, + processor: DualAxisProcessor::None, + }; - /// The right [`GamepadStick`] for discrete input from the right stick on the X and Y axes. - /// No processing is applied to raw data from the gamepad. - pub const RIGHT_DIGITAL: Self = - Self::digital(GamepadAxisType::RightStickX, GamepadAxisType::RightStickY); + /// The right gamepad stick. No processing is applied to raw data from the gamepad. + pub const RIGHT: Self = Self { + x: GamepadAxisType::RightStickX, + y: GamepadAxisType::RightStickY, + processor: DualAxisProcessor::None, + }; /// Retrieves the current X and Y values of this stick after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn processed_value(&self, input_streams: &InputStreams) -> Vec2 { let x = read_axis_value(input_streams, self.x); let y = read_axis_value(input_streams, self.y); - let value = Vec2::new(x, y); - let value = self.input_mode.dual_axis_value(value); - self.processor.process(value) + self.processor.process(Vec2::new(x, y)) } } #[serde_typetag] impl UserInput for GamepadStick { - /// [`GamepadStick`] always acts as a dual-axis input. + /// [`GamepadStick`] acts as a dual-axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::DualAxis + fn kind(&self) -> InputControlKind { + InputControlKind::DualAxis } /// Checks if this stick has a non-zero magnitude. - /// - /// When a [`Gamepad`] is specified, only checks if the stick is active on the specified gamepad. - /// Otherwise, checks if the stick is active on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -384,20 +396,14 @@ impl UserInput for GamepadStick { } /// Retrieves the magnitude of the value from this stick after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { let value = self.processed_value(input_streams); - self.input_mode.dual_axis_magnitude(value) + value.length() } /// Retrieves the current X and Y values of this stick after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn axis_pair(&self, input_streams: &InputStreams) -> Option { @@ -405,9 +411,9 @@ impl UserInput for GamepadStick { Some(DualAxisData::from_xy(value)) } - /// Returns four [`GamepadControlDirection`]s to represent the movement. + /// [`GamepadStick`] represents a composition of four [`GamepadControlDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(GamepadControlDirection::negative(self.x)), Box::new(GamepadControlDirection::positive(self.x)), @@ -485,16 +491,13 @@ fn button_value_any(input_streams: &InputStreams, button: GamepadButtonType) -> // Built-in support for Bevy's GamepadButtonType. #[serde_typetag] impl UserInput for GamepadButtonType { - /// [`GamepadButtonType`] always acts as a button. + /// [`GamepadButtonType`] acts as a button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if the specified button is currently pressed down. - /// - /// When a [`Gamepad`] is specified, only checks if the button is pressed on the specified gamepad. - /// Otherwise, checks if the button is pressed on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -509,9 +512,6 @@ impl UserInput for GamepadButtonType { } /// Retrieves the strength of the button press for the specified button. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { @@ -532,7 +532,7 @@ impl UserInput for GamepadButtonType { /// Creates a [`BasicInputs`] that only contains the [`GamepadButtonType`] itself, /// as it represents a simple physical button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -543,26 +543,59 @@ impl UserInput for GamepadButtonType { } } -/// A virtual single-axis control constructed from two [`GamepadButtonType`]s. -/// One button represents the negative direction (typically left or down), -/// while the other represents the positive direction (typically right or up). +/// A virtual single-axis control constructed by combining two [`GamepadButtonType`]s. +/// One button represents the negative direction (left for the X-axis, down for the Y-axis), +/// while the other represents the positive direction (right for the X-axis, up for the Y-axis). +/// +/// # Behaviors +/// +/// - Gamepad Selection: By default, reads from **any connected gamepad**. +/// Use the [`InputMap::set_gamepad`] for specific ones. +/// - Raw Value: +/// - `-1.0`: Only the negative button is currently pressed. +/// - `1.0`: Only the positive button is currently pressed. +/// - `0.0`: Neither button is pressed, or both are pressed simultaneously. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithAxisProcessingPipelineExt`] for details. +/// - Activation: Only if the processed value is non-zero. +/// +/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad +/// +/// ```rust,ignore +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Define a virtual Y-axis using D-pad "up" and "down" buttons +/// let axis = GamepadVirtualAxis::DPAD_Y; +/// +/// // Pressing either button activates the input +/// app.press_input(GamepadButtonType::DPadUp); +/// app.update(); +/// assert_eq!(app.read_axis_values(axis), [1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the value) +/// let doubled = GamepadVirtualAxis::DPAD_Y.sensitivity(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadVirtualAxis { - /// The [`GamepadButtonType`] used for the negative direction (typically left or down). + /// The button that represents the negative direction. pub(crate) negative: GamepadButtonType, - /// The [`GamepadButtonType`] used for the positive direction (typically right or up). + /// The button that represents the positive direction. pub(crate) positive: GamepadButtonType, - /// The [`AxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: AxisProcessor, } impl GamepadVirtualAxis { /// Creates a new [`GamepadVirtualAxis`] with two given [`GamepadButtonType`]s. - /// One button represents the negative direction (typically left or down), - /// while the other represents the positive direction (typically right or up). /// No processing is applied to raw data from the gamepad. #[inline] pub const fn new(negative: GamepadButtonType, positive: GamepadButtonType) -> Self { @@ -604,16 +637,13 @@ impl GamepadVirtualAxis { #[serde_typetag] impl UserInput for GamepadVirtualAxis { - /// [`GamepadVirtualAxis`] always acts as an axis input. + /// [`GamepadVirtualAxis`] acts as an axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::Axis + fn kind(&self) -> InputControlKind { + InputControlKind::Axis } /// Checks if this axis has a non-zero value after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only checks if the buttons are pressed on the specified gamepad. - /// Otherwise, checks if the buttons are pressed on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -621,9 +651,6 @@ impl UserInput for GamepadVirtualAxis { } /// Retrieves the current value of this axis after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { @@ -648,7 +675,7 @@ impl UserInput for GamepadVirtualAxis { /// Returns the two [`GamepadButtonType`]s used by this axis. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![Box::new(self.negative), Box::new(self.positive)]) } @@ -683,22 +710,57 @@ impl WithAxisProcessingPipelineExt for GamepadVirtualAxis { /// Each button represents a specific direction (up, down, left, right), /// functioning similarly to a directional pad (D-pad) on both X and Y axes, /// and offering intermediate diagonals by means of two-button combinations. +/// +/// # Behaviors +/// +/// - Gamepad Selection: By default, reads from **any connected gamepad**. +/// Use the [`InputMap::set_gamepad`] for specific ones. +/// - Raw Value: Each axis behaves as follows: +/// - `-1.0`: Only the negative button is currently pressed (Down/Left). +/// - `1.0`: Only the positive button is currently pressed (Up/Right). +/// - `0.0`: Neither button is pressed, or both buttons on the same axis are pressed simultaneously. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithDualAxisProcessingPipelineExt`] for details. +/// - Activation: Only if the processed value is non-zero on either axis. +/// +/// [`InputMap::set_gamepad`]: crate::input_map::InputMap::set_gamepad +/// +/// ```rust,ignore +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Define a virtual D-pad using the physical D-pad buttons +/// let input = GamepadVirtualDPad::DPAD; +/// +/// // Pressing a D-pad button activates the corresponding axis +/// app.press_input(GamepadButtonType::DPadUp); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [0.0, 1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the Y value) +/// let doubled = GamepadVirtualDPad::DPAD.sensitivity_y(2.0); +/// assert_eq!(app.read_axis_values(doubled), [0.0, 2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadVirtualDPad { - /// The [`GamepadButtonType`] used for the upward direction. + /// The button for the upward direction. pub(crate) up: GamepadButtonType, - /// The [`GamepadButtonType`] used for the downward direction. + /// The button for the downward direction. pub(crate) down: GamepadButtonType, - /// The [`GamepadButtonType`] used for the leftward direction. + /// The button for the leftward direction. pub(crate) left: GamepadButtonType, - /// The [`GamepadButtonType`] used for the rightward direction. + /// The button for the rightward direction. pub(crate) right: GamepadButtonType, - /// The [`DualAxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: DualAxisProcessor, } @@ -748,9 +810,6 @@ impl GamepadVirtualDPad { ); /// Retrieves the current X and Y values of this D-pad after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[inline] fn processed_value(&self, input_streams: &InputStreams) -> Vec2 { let value = if let Some(gamepad) = input_streams.associated_gamepad { @@ -772,16 +831,13 @@ impl GamepadVirtualDPad { #[serde_typetag] impl UserInput for GamepadVirtualDPad { - /// [`GamepadVirtualDPad`] always acts as a dual-axis input. + /// [`GamepadVirtualDPad`] acts as a dual-axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::DualAxis + fn kind(&self) -> InputControlKind { + InputControlKind::DualAxis } /// Checks if this D-pad has a non-zero magnitude after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only checks if the button is pressed on the specified gamepad. - /// Otherwise, checks if the button is pressed on any connected gamepads. #[must_use] #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -789,9 +845,6 @@ impl UserInput for GamepadVirtualDPad { } /// Retrieves the magnitude of the value from this D-pad after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { @@ -799,9 +852,6 @@ impl UserInput for GamepadVirtualDPad { } /// Retrieves the current X and Y values of this D-pad after processing by the associated processor. - /// - /// When a [`Gamepad`] is specified, only retrieves the value on the specified gamepad. - /// Otherwise, retrieves the value on any connected gamepads. #[must_use] #[inline] fn axis_pair(&self, input_streams: &InputStreams) -> Option { @@ -811,7 +861,7 @@ impl UserInput for GamepadVirtualDPad { /// Returns the four [`GamepadButtonType`]s used by this D-pad. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(self.up), Box::new(self.down), @@ -903,44 +953,44 @@ mod tests { #[test] fn test_gamepad_axes() { let left_up = GamepadControlDirection::LEFT_UP; - assert_eq!(left_up.kind(), InputKind::Button); + assert_eq!(left_up.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); assert_eq!(left_up.raw_inputs(), raw_inputs); // The opposite of left up let left_down = GamepadControlDirection::LEFT_DOWN; - assert_eq!(left_down.kind(), InputKind::Button); + assert_eq!(left_down.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([left_up]); assert_eq!(left_up.raw_inputs(), raw_inputs); let left_x = GamepadControlAxis::LEFT_X; - assert_eq!(left_x.kind(), InputKind::Axis); + assert_eq!(left_x.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_x.axis]); assert_eq!(left_x.raw_inputs(), raw_inputs); let left_y = GamepadControlAxis::LEFT_Y; - assert_eq!(left_y.kind(), InputKind::Axis); + assert_eq!(left_y.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([left_y.axis]); assert_eq!(left_y.raw_inputs(), raw_inputs); let left = GamepadStick::LEFT; - assert_eq!(left.kind(), InputKind::DualAxis); + assert_eq!(left.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([left.x, left.y]); assert_eq!(left.raw_inputs(), raw_inputs); // Up; but for the other stick let right_up = GamepadControlDirection::RIGHT_DOWN; - assert_eq!(right_up.kind(), InputKind::Button); + assert_eq!(right_up.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_control_directions([right_up]); assert_eq!(right_up.raw_inputs(), raw_inputs); let right_y = GamepadControlAxis::RIGHT_Y; - assert_eq!(right_y.kind(), InputKind::Axis); + assert_eq!(right_y.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); assert_eq!(right_y.raw_inputs(), raw_inputs); let right = GamepadStick::RIGHT; - assert_eq!(right.kind(), InputKind::DualAxis); + assert_eq!(right.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_axes([right_y.axis]); assert_eq!(right_y.raw_inputs(), raw_inputs); @@ -1008,37 +1058,37 @@ mod tests { #[ignore = "Input mocking is subtly broken: https://github.com/Leafwing-Studios/leafwing-input-manager/issues/516"] fn test_gamepad_buttons() { let up = GamepadButtonType::DPadUp; - assert_eq!(up.kind(), InputKind::Button); + assert_eq!(up.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([up]); assert_eq!(up.raw_inputs(), raw_inputs); let left = GamepadButtonType::DPadLeft; - assert_eq!(left.kind(), InputKind::Button); + assert_eq!(left.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([left]); assert_eq!(left.raw_inputs(), raw_inputs); let down = GamepadButtonType::DPadDown; - assert_eq!(left.kind(), InputKind::Button); + assert_eq!(left.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([down]); assert_eq!(down.raw_inputs(), raw_inputs); let right = GamepadButtonType::DPadRight; - assert_eq!(left.kind(), InputKind::Button); + assert_eq!(left.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_gamepad_buttons([right]); assert_eq!(right.raw_inputs(), raw_inputs); let x_axis = GamepadVirtualAxis::DPAD_X; - assert_eq!(x_axis.kind(), InputKind::Axis); + assert_eq!(x_axis.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([left, right]); assert_eq!(x_axis.raw_inputs(), raw_inputs); let y_axis = GamepadVirtualAxis::DPAD_Y; - assert_eq!(y_axis.kind(), InputKind::Axis); + assert_eq!(y_axis.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_gamepad_buttons([down, up]); assert_eq!(y_axis.raw_inputs(), raw_inputs); let dpad = GamepadVirtualDPad::DPAD; - assert_eq!(dpad.kind(), InputKind::DualAxis); + assert_eq!(dpad.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_gamepad_buttons([up, down, left, right]); assert_eq!(dpad.raw_inputs(), raw_inputs); diff --git a/src/user_input/keyboard.rs b/src/user_input/keyboard.rs index 96d80ee8..af57ef66 100644 --- a/src/user_input/keyboard.rs +++ b/src/user_input/keyboard.rs @@ -13,112 +13,15 @@ use crate::input_processing::{ }; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; -use crate::user_input::{InputChord, InputKind, UserInput}; - -/// A key or combination of keys used for capturing user input from the keyboard. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -pub enum KeyboardKey { - /// A single physical key on the keyboard. - PhysicalKey(KeyCode), - - /// Any of the specified physical keys. - PhysicalKeyAny(Vec), - - /// Keyboard modifiers like Alt, Control, Shift, and Super (OS symbol key). - ModifierKey(ModifierKey), -} - -impl KeyboardKey { - /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. - #[must_use] - #[inline] - pub fn keycodes(&self) -> Vec { - match self { - Self::PhysicalKey(keycode) => vec![*keycode], - Self::PhysicalKeyAny(keycodes) => keycodes.clone(), - Self::ModifierKey(modifier) => modifier.keycodes().to_vec(), - } - } -} - -#[serde_typetag] -impl UserInput for KeyboardKey { - /// [`KeyboardKey`] always acts as a button. - #[inline] - fn kind(&self) -> InputKind { - InputKind::Button - } - - /// Checks if the specified [`KeyboardKey`] is currently pressed down. - #[must_use] - #[inline] - fn pressed(&self, input_streams: &InputStreams) -> bool { - input_streams - .keycodes - .is_some_and(|keycodes| keycodes.any_pressed(self.keycodes())) - } - - /// Retrieves the strength of the key press for the specified [`KeyboardKey`], - /// returning `0.0` for no press and `1.0` for a currently pressed key. - #[must_use] - #[inline] - fn value(&self, input_streams: &InputStreams) -> f32 { - f32::from(self.pressed(input_streams)) - } - - /// Always returns [`None`] as [`KeyboardKey`] doesn't represent dual-axis input. - #[must_use] - #[inline] - fn axis_pair(&self, _input_streams: &InputStreams) -> Option { - None - } - - /// Returns a list of [`KeyCode`]s used by this [`KeyboardKey`]. - #[inline] - fn basic_inputs(&self) -> BasicInputs { - let keycodes = self - .keycodes() - .iter() - .map(|keycode| Box::new(*keycode) as Box) - .collect(); - BasicInputs::Group(keycodes) - } - - /// Creates a [`RawInputs`] from the [`KeyCode`]s used by this [`KeyboardKey`]. - #[inline] - fn raw_inputs(&self) -> RawInputs { - RawInputs::from_keycodes(self.keycodes()) - } -} - -impl From for KeyboardKey { - #[inline] - fn from(value: KeyCode) -> Self { - Self::PhysicalKey(value) - } -} - -impl FromIterator for KeyboardKey { - #[inline] - fn from_iter>(iter: T) -> Self { - Self::PhysicalKeyAny(iter.into_iter().collect()) - } -} - -impl From for KeyboardKey { - #[inline] - fn from(value: ModifierKey) -> Self { - Self::ModifierKey(value) - } -} +use crate::user_input::{InputChord, InputControlKind, UserInput}; // Built-in support for Bevy's KeyCode #[serde_typetag] impl UserInput for KeyCode { - /// [`KeyCode`] always acts as a button. + /// [`KeyCode`] acts as a button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if the specified key is currently pressed down. @@ -148,7 +51,7 @@ impl UserInput for KeyCode { /// Returns a [`BasicInputs`] that only contains the [`KeyCode`] itself, /// as it represents a simple physical button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -163,6 +66,13 @@ impl UserInput for KeyCode { /// /// Each variant represents a pair of [`KeyCode`]s, the left and right version of the modifier key, /// allowing for handling modifiers regardless of which side is pressed. +/// +/// # Behaviors +/// +/// - Activation: Only if at least one corresponding keys is currently pressed down. +/// - Single-Axis Value: +/// - `1.0`: The input is currently active. +/// - `0.0`: The input is inactive. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub enum ModifierKey { @@ -220,10 +130,10 @@ impl ModifierKey { #[serde_typetag] impl UserInput for ModifierKey { - /// [`ModifierKey`] always acts as a button. + /// [`ModifierKey`] acts as a button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if the specified modifier key is currently pressed down. @@ -252,7 +162,7 @@ impl UserInput for ModifierKey { /// Returns the two [`KeyCode`]s used by this [`ModifierKey`]. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![Box::new(self.left()), Box::new(self.right())]) } @@ -263,32 +173,61 @@ impl UserInput for ModifierKey { } } -/// A virtual single-axis control constructed from two [`KeyboardKey`]s. -/// One button represents the negative direction (typically left or down), -/// while the other represents the positive direction (typically right or up). +/// A virtual single-axis control constructed from two [`KeyCode`]s. +/// One key represents the negative direction (left for the X-axis, down for the Y-axis), +/// while the other represents the positive direction (right for the X-axis, up for the Y-axis). +/// +/// # Behaviors +/// +/// - Raw Value: +/// - `-1.0`: Only the negative key is currently pressed. +/// - `1.0`: Only the positive key is currently pressed. +/// - `0.0`: Neither key is pressed, or both are pressed simultaneously. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithAxisProcessingPipelineExt`] for details. +/// - Activation: Only if the processed value is non-zero. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Define a virtual Y-axis using arrow "up" and "down" keys +/// let axis = KeyboardVirtualAxis::VERTICAL_ARROW_KEYS; +/// +/// // Pressing either key activates the input +/// app.press_input(KeyCode::ArrowUp); +/// app.update(); +/// assert_eq!(app.read_axis_values(axis), [1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the value) +/// let doubled = KeyboardVirtualAxis::VERTICAL_ARROW_KEYS.sensitivity(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct KeyboardVirtualAxis { - /// The [`KeyboardKey`] used for the negative direction (typically left or down). - pub(crate) negative: KeyboardKey, + /// The key that represents the negative direction. + pub(crate) negative: KeyCode, - /// The [`KeyboardKey`] used for the negative direction (typically left or down). - pub(crate) positive: KeyboardKey, + /// The key that represents the positive direction. + pub(crate) positive: KeyCode, - /// The [`AxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: AxisProcessor, } impl KeyboardVirtualAxis { - /// Creates a new [`KeyboardVirtualAxis`] with two given [`KeyboardKey`]s. - /// One button represents the negative direction (typically left or down), - /// while the other represents the positive direction (typically right or up). + /// Creates a new [`KeyboardVirtualAxis`] with two given [`KeyCode`]s. /// No processing is applied to raw data from the gamepad. #[inline] - pub fn new(negative: impl Into, positive: impl Into) -> Self { + pub fn new(negative: KeyCode, positive: KeyCode) -> Self { Self { - negative: negative.into(), - positive: positive.into(), + negative, + positive, processor: AxisProcessor::None, } } @@ -298,8 +237,8 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::ArrowDown`] for negative direction. /// - [`KeyCode::ArrowUp`] for positive direction. pub const VERTICAL_ARROW_KEYS: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::ArrowDown), - positive: KeyboardKey::PhysicalKey(KeyCode::ArrowUp), + negative: KeyCode::ArrowDown, + positive: KeyCode::ArrowUp, processor: AxisProcessor::None, }; @@ -308,8 +247,8 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::ArrowLeft`] for negative direction. /// - [`KeyCode::ArrowRight`] for positive direction. pub const HORIZONTAL_ARROW_KEYS: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::ArrowLeft), - positive: KeyboardKey::PhysicalKey(KeyCode::ArrowRight), + negative: KeyCode::ArrowLeft, + positive: KeyCode::ArrowRight, processor: AxisProcessor::None, }; @@ -318,8 +257,8 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::KeyS`] for negative direction. /// - [`KeyCode::KeyW`] for positive direction. pub const WS: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::KeyS), - positive: KeyboardKey::PhysicalKey(KeyCode::KeyW), + negative: KeyCode::KeyS, + positive: KeyCode::KeyW, processor: AxisProcessor::None, }; @@ -328,8 +267,8 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::KeyA`] for negative direction. /// - [`KeyCode::KeyD`] for positive direction. pub const AD: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::KeyA), - positive: KeyboardKey::PhysicalKey(KeyCode::KeyD), + negative: KeyCode::KeyA, + positive: KeyCode::KeyD, processor: AxisProcessor::None, }; @@ -338,8 +277,8 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::Numpad2`] for negative direction. /// - [`KeyCode::Numpad8`] for positive direction. pub const VERTICAL_NUMPAD: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::Numpad2), - positive: KeyboardKey::PhysicalKey(KeyCode::Numpad8), + negative: KeyCode::Numpad2, + positive: KeyCode::Numpad8, processor: AxisProcessor::None, }; @@ -348,18 +287,18 @@ impl KeyboardVirtualAxis { /// - [`KeyCode::Numpad4`] for negative direction. /// - [`KeyCode::Numpad6`] for positive direction. pub const HORIZONTAL_NUMPAD: Self = Self { - negative: KeyboardKey::PhysicalKey(KeyCode::Numpad4), - positive: KeyboardKey::PhysicalKey(KeyCode::Numpad6), + negative: KeyCode::Numpad4, + positive: KeyCode::Numpad6, processor: AxisProcessor::None, }; } #[serde_typetag] impl UserInput for KeyboardVirtualAxis { - /// [`KeyboardVirtualAxis`] always acts as a virtual axis input. + /// [`KeyboardVirtualAxis`] acts as a virtual axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::Axis + fn kind(&self) -> InputControlKind { + InputControlKind::Axis } /// Checks if this axis has a non-zero value after processing by the associated processor. @@ -377,8 +316,8 @@ impl UserInput for KeyboardVirtualAxis { return 0.0; }; - let negative = f32::from(keycodes.any_pressed(self.negative.keycodes())); - let positive = f32::from(keycodes.any_pressed(self.positive.keycodes())); + let negative = f32::from(keycodes.pressed(self.negative)); + let positive = f32::from(keycodes.pressed(self.positive)); let value = positive - negative; self.processor.process(value) } @@ -390,28 +329,16 @@ impl UserInput for KeyboardVirtualAxis { None } - /// Returns all the [`KeyCode`]s used by this axis. + /// [`KeyboardVirtualAxis`] represents a compositions of two [`KeyCode`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { - let keycodes = self - .negative - .keycodes() - .into_iter() - .chain(self.positive.keycodes()) - .map(|keycode| Box::new(keycode) as Box) - .collect(); - BasicInputs::Composite(keycodes) + fn decompose(&self) -> BasicInputs { + BasicInputs::Composite(vec![Box::new(self.negative), Box::new(self.negative)]) } - /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this axis. + /// Creates a [`RawInputs`] from two [`KeyCode`]s used by this axis. #[inline] fn raw_inputs(&self) -> RawInputs { - let keycodes = self - .negative - .keycodes() - .into_iter() - .chain(self.positive.keycodes()); - RawInputs::from_keycodes(keycodes) + RawInputs::from_keycodes([self.negative, self.positive]) } } @@ -435,45 +362,70 @@ impl WithAxisProcessingPipelineExt for KeyboardVirtualAxis { } } -/// A virtual single-axis control constructed from four [`KeyboardKey`]s. -/// Each button represents a specific direction (up, down, left, right), +/// A virtual single-axis control constructed from four [`KeyCode`]s. +/// Each key represents a specific direction (up, down, left, right), /// functioning similarly to a directional pad (D-pad) on both X and Y axes, -/// and offering intermediate diagonals by means of two-button combinations. +/// and offering intermediate diagonals by means of two-key combinations. +/// +/// # Behaviors +/// +/// - Raw Value: Each axis behaves as follows: +/// - `-1.0`: Only the negative key is currently pressed (Down/Left). +/// - `1.0`: Only the positive key is currently pressed (Up/Right). +/// - `0.0`: Neither key is pressed, or both keys on the same axis are pressed simultaneously. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithDualAxisProcessingPipelineExt`] for details. +/// - Activation: Only if the processed value is non-zero on either axis. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Define a virtual D-pad using the arrow keys +/// let input = KeyboardVirtualDPad::ARROW_KEYS; +/// +/// // Pressing an arrow key activates the corresponding axis +/// app.press_input(KeyCode::ArrowUp); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [0.0, 1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the Y value) +/// let doubled = KeyboardVirtualDPad::ARROW_KEYS.sensitivity_y(2.0); +/// assert_eq!(app.read_axis_values(doubled), [0.0, 2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct KeyboardVirtualDPad { - /// The [`KeyboardKey`] used for the upward direction. - pub(crate) up: KeyboardKey, + /// The key for the upward direction. + pub(crate) up: KeyCode, - /// The [`KeyboardKey`] used for the downward direction. - pub(crate) down: KeyboardKey, + /// The key for the downward direction. + pub(crate) down: KeyCode, - /// The [`KeyboardKey`] used for the leftward direction. - pub(crate) left: KeyboardKey, + /// The key for the leftward direction. + pub(crate) left: KeyCode, - /// The [`KeyboardKey`] used for the rightward direction. - pub(crate) right: KeyboardKey, + /// The key for the rightward direction. + pub(crate) right: KeyCode, - /// The [`DualAxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: DualAxisProcessor, } impl KeyboardVirtualDPad { - /// Creates a new [`KeyboardVirtualDPad`] with four given [`KeyboardKey`]s. - /// Each button represents a specific direction (up, down, left, right). + /// Creates a new [`KeyboardVirtualDPad`] with four given [`KeyCode`]s. /// No processing is applied to raw data from the keyboard. #[inline] - pub fn new( - up: impl Into, - down: impl Into, - left: impl Into, - right: impl Into, - ) -> Self { + pub fn new(up: KeyCode, down: KeyCode, left: KeyCode, right: KeyCode) -> Self { Self { - up: up.into(), - down: down.into(), - left: left.into(), - right: right.into(), + up, + down, + left, + right, processor: DualAxisProcessor::None, } } @@ -485,10 +437,10 @@ impl KeyboardVirtualDPad { /// - [`KeyCode::ArrowLeft`] for leftward direction. /// - [`KeyCode::ArrowRight`] for rightward direction. pub const ARROW_KEYS: Self = Self { - up: KeyboardKey::PhysicalKey(KeyCode::ArrowUp), - down: KeyboardKey::PhysicalKey(KeyCode::ArrowDown), - left: KeyboardKey::PhysicalKey(KeyCode::ArrowLeft), - right: KeyboardKey::PhysicalKey(KeyCode::ArrowRight), + up: KeyCode::ArrowUp, + down: KeyCode::ArrowDown, + left: KeyCode::ArrowLeft, + right: KeyCode::ArrowRight, processor: DualAxisProcessor::None, }; @@ -499,10 +451,10 @@ impl KeyboardVirtualDPad { /// - [`KeyCode::KeyA`] for leftward direction. /// - [`KeyCode::KeyD`] for rightward direction. pub const WASD: Self = Self { - up: KeyboardKey::PhysicalKey(KeyCode::KeyW), - down: KeyboardKey::PhysicalKey(KeyCode::KeyS), - left: KeyboardKey::PhysicalKey(KeyCode::KeyA), - right: KeyboardKey::PhysicalKey(KeyCode::KeyD), + up: KeyCode::KeyW, + down: KeyCode::KeyS, + left: KeyCode::KeyA, + right: KeyCode::KeyD, processor: DualAxisProcessor::None, }; @@ -513,10 +465,10 @@ impl KeyboardVirtualDPad { /// - [`KeyCode::Numpad4`] for leftward direction. /// - [`KeyCode::Numpad6`] for rightward direction. pub const NUMPAD: Self = Self { - up: KeyboardKey::PhysicalKey(KeyCode::Numpad8), - down: KeyboardKey::PhysicalKey(KeyCode::Numpad2), - left: KeyboardKey::PhysicalKey(KeyCode::Numpad4), - right: KeyboardKey::PhysicalKey(KeyCode::Numpad6), + up: KeyCode::Numpad8, + down: KeyCode::Numpad2, + left: KeyCode::Numpad4, + right: KeyCode::Numpad6, processor: DualAxisProcessor::None, }; @@ -528,10 +480,10 @@ impl KeyboardVirtualDPad { return Vec2::ZERO; }; - let up = f32::from(keycodes.any_pressed(self.up.keycodes())); - let down = f32::from(keycodes.any_pressed(self.down.keycodes())); - let left = f32::from(keycodes.any_pressed(self.left.keycodes())); - let right = f32::from(keycodes.any_pressed(self.right.keycodes())); + let up = f32::from(keycodes.pressed(self.up)); + let down = f32::from(keycodes.pressed(self.down)); + let left = f32::from(keycodes.pressed(self.left)); + let right = f32::from(keycodes.pressed(self.right)); let value = Vec2::new(right - left, up - down); self.processor.process(value) } @@ -539,10 +491,10 @@ impl KeyboardVirtualDPad { #[serde_typetag] impl UserInput for KeyboardVirtualDPad { - /// [`KeyboardVirtualDPad`] always acts as a virtual dual-axis input. + /// [`KeyboardVirtualDPad`] acts as a virtual dual-axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::DualAxis + fn kind(&self) -> InputControlKind { + InputControlKind::DualAxis } /// Checks if this D-pad has a non-zero magnitude after processing by the associated processor. @@ -567,32 +519,21 @@ impl UserInput for KeyboardVirtualDPad { Some(DualAxisData::from_xy(value)) } - /// Returns all the [`KeyCode`]s used by this D-pad. + /// [`KeyboardVirtualDPad`] represents a compositions of four [`KeyCode`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { - let keycodes = self - .up - .keycodes() - .into_iter() - .chain(self.down.keycodes()) - .chain(self.left.keycodes()) - .chain(self.right.keycodes()) - .map(|keycode| Box::new(keycode) as Box) - .collect(); - BasicInputs::Composite(keycodes) + fn decompose(&self) -> BasicInputs { + BasicInputs::Composite(vec![ + Box::new(self.up), + Box::new(self.down), + Box::new(self.left), + Box::new(self.right), + ]) } - /// Creates a [`RawInputs`] from all the [`KeyCode`]s used by this D-pad. + /// Creates a [`RawInputs`] from four [`KeyCode`]s used by this D-pad. #[inline] fn raw_inputs(&self) -> RawInputs { - let keycodes = self - .up - .keycodes() - .into_iter() - .chain(self.down.keycodes()) - .chain(self.left.keycodes()) - .chain(self.right.keycodes()); - RawInputs::from_keycodes(keycodes) + RawInputs::from_keycodes([self.up, self.down, self.left, self.right]) } } @@ -653,38 +594,25 @@ mod tests { #[test] fn test_keyboard_input() { let up = KeyCode::ArrowUp; - assert_eq!(up.kind(), InputKind::Button); + assert_eq!(up.kind(), InputControlKind::Button); assert_eq!(up.raw_inputs(), RawInputs::from_keycodes([up])); let left = KeyCode::ArrowLeft; - assert_eq!(left.kind(), InputKind::Button); + assert_eq!(left.kind(), InputControlKind::Button); assert_eq!(left.raw_inputs(), RawInputs::from_keycodes([left])); let alt = ModifierKey::Alt; - assert_eq!(alt.kind(), InputKind::Button); + assert_eq!(alt.kind(), InputControlKind::Button); let alt_raw_inputs = RawInputs::from_keycodes([KeyCode::AltLeft, KeyCode::AltRight]); assert_eq!(alt.raw_inputs(), alt_raw_inputs); - let physical_up = KeyboardKey::PhysicalKey(up); - assert_eq!(physical_up.kind(), InputKind::Button); - assert_eq!(physical_up.raw_inputs(), RawInputs::from_keycodes([up])); - - let physical_any_up_left = KeyboardKey::PhysicalKeyAny(vec![up, left]); - assert_eq!(physical_any_up_left.kind(), InputKind::Button); - let raw_inputs = RawInputs::from_keycodes([up, left]); - assert_eq!(physical_any_up_left.raw_inputs(), raw_inputs); - - let keyboard_alt = KeyboardKey::ModifierKey(alt); - assert_eq!(keyboard_alt.kind(), InputKind::Button); - assert_eq!(keyboard_alt.raw_inputs(), alt_raw_inputs); - let arrow_y = KeyboardVirtualAxis::VERTICAL_ARROW_KEYS; - assert_eq!(arrow_y.kind(), InputKind::Axis); + assert_eq!(arrow_y.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_keycodes([KeyCode::ArrowDown, KeyCode::ArrowUp]); assert_eq!(arrow_y.raw_inputs(), raw_inputs); let arrows = KeyboardVirtualDPad::ARROW_KEYS; - assert_eq!(arrows.kind(), InputKind::DualAxis); + assert_eq!(arrows.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_keycodes([ KeyCode::ArrowUp, KeyCode::ArrowDown, @@ -701,9 +629,6 @@ mod tests { released(&up, &inputs); released(&left, &inputs); released(&alt, &inputs); - released(&physical_up, &inputs); - released(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); released(&arrow_y, &inputs); check(&arrows, &inputs, false, 0.0, zeros); @@ -716,9 +641,6 @@ mod tests { pressed(&up, &inputs); released(&left, &inputs); released(&alt, &inputs); - pressed(&physical_up, &inputs); - pressed(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); check(&arrow_y, &inputs, true, data.y(), None); check(&arrows, &inputs, true, data.length(), Some(data)); @@ -731,9 +653,6 @@ mod tests { released(&up, &inputs); released(&left, &inputs); released(&alt, &inputs); - released(&physical_up, &inputs); - released(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); check(&arrow_y, &inputs, true, data.y(), None); check(&arrows, &inputs, true, data.length(), Some(data)); @@ -746,9 +665,6 @@ mod tests { released(&up, &inputs); pressed(&left, &inputs); released(&alt, &inputs); - released(&physical_up, &inputs); - pressed(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); released(&arrow_y, &inputs); check(&arrows, &inputs, true, data.length(), Some(data)); @@ -761,9 +677,6 @@ mod tests { pressed(&up, &inputs); released(&left, &inputs); released(&alt, &inputs); - pressed(&physical_up, &inputs); - pressed(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); released(&arrow_y, &inputs); check(&arrows, &inputs, false, 0.0, zeros); @@ -777,9 +690,6 @@ mod tests { pressed(&up, &inputs); pressed(&left, &inputs); released(&alt, &inputs); - pressed(&physical_up, &inputs); - pressed(&physical_any_up_left, &inputs); - released(&keyboard_alt, &inputs); check(&arrow_y, &inputs, true, data.y(), None); check(&arrows, &inputs, true, data.length(), Some(data)); @@ -791,9 +701,6 @@ mod tests { released(&up, &inputs); released(&left, &inputs); pressed(&alt, &inputs); - released(&physical_up, &inputs); - released(&physical_any_up_left, &inputs); - pressed(&keyboard_alt, &inputs); released(&arrow_y, &inputs); check(&arrows, &inputs, false, 0.0, zeros); @@ -805,9 +712,6 @@ mod tests { released(&up, &inputs); released(&left, &inputs); pressed(&alt, &inputs); - released(&physical_up, &inputs); - released(&physical_any_up_left, &inputs); - pressed(&keyboard_alt, &inputs); released(&arrow_y, &inputs); check(&arrows, &inputs, false, 0.0, zeros); } diff --git a/src/user_input/mod.rs b/src/user_input/mod.rs index d235cc77..0e46175e 100644 --- a/src/user_input/mod.rs +++ b/src/user_input/mod.rs @@ -13,25 +13,25 @@ //! //! Feel free to suggest additions to the built-in inputs if you have a common use case! //! -//! ## Input Kinds +//! ## Control Types //! -//! [`UserInput`]s use the method [`UserInput::kind`] returning an [`InputKind`] -//! to classify the type of data they provide (buttons, analog axes, etc.). +//! [`UserInput`]s use the method [`UserInput::kind`] returning an [`InputControlKind`] +//! to classify the behavior of the input (buttons, analog axes, etc.). //! -//! - [`InputKind::Button`]: Represents a digital input with an on/off state (e.g., button press). +//! - [`InputControlKind::Button`]: Represents a digital input with an on/off state (e.g., button press). //! These inputs typically provide two values, typically `0.0` (inactive) and `1.0` (fully active). //! -//! - [`InputKind::Axis`]: Represents an analog input (e.g., mouse wheel) +//! - [`InputControlKind::Axis`]: Represents an analog input (e.g., mouse wheel) //! with a continuous value typically ranging from `-1.0` (fully left/down) to `1.0` (fully right/up). //! Non-zero values are considered active. //! -//! - [`InputKind::DualAxis`]: Represents a combination of two analog axes (e.g., thumb stick). +//! - [`InputControlKind::DualAxis`]: Represents a combination of two analog axes (e.g., thumb stick). //! These inputs provide separate X and Y values typically ranging from `-1.0` to `1.0`. //! Non-zero values are considered active. //! //! ## Basic Inputs //! -//! [`UserInput`]s use the method [`UserInput::basic_inputs`] returning a [`BasicInputs`] +//! [`UserInput`]s use the method [`UserInput::decompose`] returning a [`BasicInputs`] //! used for clashing detection, see [clashing input check](crate::clashing_inputs) for details. //! //! ## Raw Input Events @@ -50,7 +50,6 @@ //! //! - Check physical keys presses using Bevy's [`KeyCode`] directly. //! - Use [`ModifierKey`] to check for either left or right modifier keys is pressed. -//! - Create complex combinations with [`KeyboardKey`], including individual keys, modifiers, and checks for any key press. //! //! ### Mouse Inputs //! @@ -67,11 +66,11 @@ //! //! - Create a virtual axis control: //! - [`GamepadVirtualAxis`] from two [`GamepadButtonType`]s. -//! - [`KeyboardVirtualAxis`] from two [`KeyboardKey`]s. +//! - [`KeyboardVirtualAxis`] from two [`KeyCode`]s. //! //! - Create a virtual directional pad (D-pad) for dual-axis control: //! - [`GamepadVirtualDPad`] from four [`GamepadButtonType`]s. -//! - [`KeyboardVirtualDPad`] from four [`KeyboardKey`]s. +//! - [`KeyboardVirtualDPad`] from four [`KeyCode`]s. //! //! [`GamepadAxisType`]: bevy::prelude::GamepadAxisType //! [`GamepadButtonType`]: bevy::prelude::GamepadButtonType @@ -117,7 +116,7 @@ pub mod mouse; /// Classifies [`UserInput`]s based on their behavior (buttons, analog axes, etc.). #[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] #[must_use] -pub enum InputKind { +pub enum InputControlKind { /// A single input with binary state (active or inactive), typically a button press (on or off). Button, @@ -153,11 +152,11 @@ pub enum InputKind { /// // Add this attribute for ensuring proper serialization and deserialization. /// #[serde_typetag] /// impl UserInput for MouseScrollAlwaysFiveOnYAxis { -/// fn kind(&self) -> InputKind { +/// fn kind(&self) -> InputControlKind { /// // Returns the kind of input this represents. /// // /// // In this case, it represents an axial input. -/// InputKind::Axis +/// InputControlKind::Axis /// } /// /// fn pressed(&self, input_streams: &InputStreams) -> bool { @@ -183,7 +182,7 @@ pub enum InputKind { /// None /// } /// -/// fn basic_inputs(&self) -> BasicInputs { +/// fn decompose(&self) -> BasicInputs { /// // Gets the most basic form of this input for clashing input detection. /// // /// // This input is a simple, atomic unit, @@ -206,8 +205,8 @@ pub enum InputKind { pub trait UserInput: Send + Sync + Debug + DynClone + DynEq + DynHash + Reflect + erased_serde::Serialize { - /// Defines the kind of data that the input should provide. - fn kind(&self) -> InputKind; + /// Defines the kind of behavior that the input should be. + fn kind(&self) -> InputControlKind; /// Checks if the input is currently active. fn pressed(&self, input_streams: &InputStreams) -> bool; @@ -222,13 +221,13 @@ pub trait UserInput: /// /// For inputs that don't represent dual-axis input, there is no need to override this method. /// The default implementation will always return [`None`]. - fn axis_pair(&self, _input_streams: &InputStreams) -> Option; + fn axis_pair(&self, input_streams: &InputStreams) -> Option; /// Returns the most basic inputs that make up this input. /// /// For inputs that represent a simple, atomic control, /// this method should always return a [`BasicInputs::Simple`] that only contains the input itself. - fn basic_inputs(&self) -> BasicInputs; + fn decompose(&self) -> BasicInputs; /// Returns the raw input events that make up this input. fn raw_inputs(&self) -> RawInputs; diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index f8e197cd..98468217 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -5,20 +5,20 @@ use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; use crate as leafwing_input_manager; -use crate::axislike::{AxisInputMode, DualAxisData, DualAxisDirection, DualAxisType}; +use crate::axislike::{DualAxisData, DualAxisDirection, DualAxisType}; use crate::clashing_inputs::BasicInputs; use crate::input_processing::*; use crate::input_streams::InputStreams; use crate::raw_inputs::RawInputs; -use crate::user_input::{InputKind, UserInput}; +use crate::user_input::{InputControlKind, UserInput}; // Built-in support for Bevy's MouseButton #[serde_typetag] impl UserInput for MouseButton { - /// [`MouseButton`] always acts as a button. + /// [`MouseButton`] acts as a button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if the specified button is currently pressed down. @@ -50,7 +50,7 @@ impl UserInput for MouseButton { /// Returns a [`BasicInputs`] that only contains the [`MouseButton`] itself, /// as it represents a simple physical button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -75,7 +75,36 @@ fn accumulate_mouse_movement(input_streams: &InputStreams) -> Vec2 { .sum() } -/// Captures ongoing mouse movement on a specific direction, treated as a button press. +/// Provides button-like behavior for mouse movement in cardinal directions. +/// +/// # Behaviors +/// +/// - Activation: Only if the mouse moves in the chosen direction. +/// - Single-Axis Value: +/// - `1.0`: The input is currently active. +/// - `0.0`: The input is inactive. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Positive Y-axis movement +/// let input = MouseMoveDirection::UP; +/// +/// // Movement in the opposite direction doesn't activate the input +/// app.send_axis_values(MouseMoveAxis::Y, [-5.0]); +/// app.update(); +/// assert!(!app.pressed(input)); +/// +/// // Movement in the chosen direction activates the input +/// app.send_axis_values(MouseMoveAxis::Y, [5.0]); +/// app.update(); +/// assert!(app.pressed(input)); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseMoveDirection(pub(crate) DualAxisDirection); @@ -96,10 +125,10 @@ impl MouseMoveDirection { #[serde_typetag] impl UserInput for MouseMoveDirection { - /// [`MouseMoveDirection`] always acts as a virtual button. + /// [`MouseMoveDirection`] acts as a virtual button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if there is any recent mouse movement along the specified direction. @@ -125,10 +154,9 @@ impl UserInput for MouseMoveDirection { None } - /// Returns a [`BasicInputs`] that only contains the [`MouseMoveDirection`] itself, - /// as it represents a simple virtual button. + /// [`MouseMoveDirection`] represents a simple virtual button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -139,61 +167,65 @@ impl UserInput for MouseMoveDirection { } } -/// Captures ongoing mouse movement on a specific axis. +/// Relative changes in position of mouse movement on a single axis (X or Y). +/// +/// # Behaviors +/// +/// - Raw Value: Captures the amount of movement on the chosen axis (X or Y). +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithAxisProcessingPipelineExt`] for details. +/// - Activation: Only if its value is non-zero. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Y-axis movement +/// let input = MouseMoveAxis::Y; +/// +/// // Movement on the chosen axis activates the input +/// app.send_axis_values(MouseMoveAxis::Y, [1.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the value) +/// let doubled = MouseMoveAxis::Y.sensitivity(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseMoveAxis { - /// The axis that this input tracks. + /// The specified axis that this input tracks. pub(crate) axis: DualAxisType, - /// The method to interpret values on the axis, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`AxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: AxisProcessor, } impl MouseMoveAxis { - /// The horizontal [`MouseMoveAxis`] for continuous input. - /// No processing is applied to raw data from the mouse. + /// Movement on the X-axis. No processing is applied to raw data from the mouse. pub const X: Self = Self { axis: DualAxisType::X, - input_mode: AxisInputMode::Analog, processor: AxisProcessor::None, }; - /// The vertical [`MouseMoveAxis`] for continuous input. - /// No processing is applied to raw data from the mouse. + /// Movement on the Y-axis. No processing is applied to raw data from the mouse. pub const Y: Self = Self { axis: DualAxisType::Y, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - }; - - /// The horizontal [`MouseMoveAxis`] for discrete input. - /// No processing is applied to raw data from the mouse. - pub const X_DIGITAL: Self = Self { - axis: DualAxisType::X, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - }; - - /// The vertical [`MouseMoveAxis`] for discrete input. - /// No processing is applied to raw data from the mouse. - pub const Y_DIGITAL: Self = Self { - axis: DualAxisType::Y, - input_mode: AxisInputMode::Digital, processor: AxisProcessor::None, }; } #[serde_typetag] impl UserInput for MouseMoveAxis { - /// [`MouseMoveAxis`] always acts as an axis input. + /// [`MouseMoveAxis`] acts as an axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::Axis + fn kind(&self) -> InputControlKind { + InputControlKind::Axis } /// Checks if there is any recent mouse movement along the specified axis. @@ -210,7 +242,6 @@ impl UserInput for MouseMoveAxis { fn value(&self, input_streams: &InputStreams) -> f32 { let movement = accumulate_mouse_movement(input_streams); let value = self.axis.get_value(movement); - let value = self.input_mode.axis_value(value); self.processor.process(value) } @@ -221,9 +252,9 @@ impl UserInput for MouseMoveAxis { None } - /// Returns both negative and positive [`MouseMoveDirection`] to represent the movement. + /// [`MouseMoveAxis`] represents a composition of two [`MouseMoveDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(MouseMoveDirection(self.axis.negative())), Box::new(MouseMoveDirection(self.axis.positive())), @@ -257,40 +288,48 @@ impl WithAxisProcessingPipelineExt for MouseMoveAxis { } } -/// Captures ongoing mouse movement on both X and Y axes. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +/// Relative changes in position of mouse movement on both axes. +/// +/// # Behaviors +/// +/// - Raw Value: Captures the amount of movement on both axes. +/// - Value Processing: Configure a pipeline to modify the raw value before use, +/// see [`WithDualAxisProcessingPipelineExt`] for details. +/// - Activation: Only if its processed value is non-zero on either axis. +/// - Single-Axis Value: Reports the magnitude of the processed value. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// let input = MouseMove::default(); +/// +/// // Movement on either axis activates the input +/// app.send_axis_values(MouseMoveAxis::Y, [3.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [0.0, 3.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the Y value) +/// let doubled = MouseMove::default().sensitivity_y(2.0); +/// assert_eq!(app.read_axis_values(doubled), [0.0, 6.0]); +/// ``` +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseMove { - /// The method to interpret values on both axes, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`DualAxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: DualAxisProcessor, } -impl MouseMove { - /// Continuous [`MouseMove`] for input on both X and Y axes. - /// No processing is applied to raw data from the mouse. - pub const RAW: Self = Self { - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - }; - - /// Discrete [`MouseMove`] for input on both X and Y axes. - /// No processing is applied to raw data from the mouse. - pub const DIGITAL: Self = Self { - input_mode: AxisInputMode::Digital, - processor: DualAxisProcessor::None, - }; -} - #[serde_typetag] impl UserInput for MouseMove { - /// [`MouseMove`] always acts as a dual-axis input. + /// [`MouseMove`] acts as a dual-axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::DualAxis + fn kind(&self) -> InputControlKind { + InputControlKind::DualAxis } /// Checks if there is any recent mouse movement. @@ -298,8 +337,7 @@ impl UserInput for MouseMove { #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { let movement = accumulate_mouse_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); + let value = self.processor.process(movement); value != Vec2::ZERO } @@ -308,9 +346,8 @@ impl UserInput for MouseMove { #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { let movement = accumulate_mouse_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); - self.input_mode.dual_axis_magnitude(value) + let value = self.processor.process(movement); + value.length() } /// Retrieves the mouse displacement after processing by the associated processor. @@ -318,14 +355,13 @@ impl UserInput for MouseMove { #[inline] fn axis_pair(&self, input_streams: &InputStreams) -> Option { let movement = accumulate_mouse_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); + let value = self.processor.process(movement); Some(DualAxisData::from_xy(value)) } - /// Returns four [`MouseMoveDirection`]s to represent the movement. + /// [`MouseMove`] represents a composition of four [`MouseMoveDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(MouseMoveDirection::UP), Box::new(MouseMoveDirection::DOWN), @@ -375,7 +411,36 @@ fn accumulate_wheel_movement(input_streams: &InputStreams) -> Vec2 { wheel.iter().map(|event| Vec2::new(event.x, event.y)).sum() } -/// Captures ongoing mouse wheel movement on a specific direction, treated as a button press. +/// Provides button-like behavior for mouse wheel scrolling in cardinal directions. +/// +/// # Behaviors +/// +/// - Activation: Only if the mouse wheel is scrolling in the chosen direction. +/// - Single-Axis Value: +/// - `1.0`: The input is currently active. +/// - `0.0`: The input is inactive. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Positive Y-axis scrolling +/// let input = MouseScrollDirection::UP; +/// +/// // Scrolling in the opposite direction doesn't activate the input +/// app.send_axis_values(MouseScrollAxis::Y, [-5.0]); +/// app.update(); +/// assert!(!app.pressed(input)); +/// +/// // Scrolling in the chosen direction activates the input +/// app.send_axis_values(MouseScrollAxis::Y, [5.0]); +/// app.update(); +/// assert!(app.pressed(input)); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseScrollDirection(pub(crate) DualAxisDirection); @@ -396,10 +461,10 @@ impl MouseScrollDirection { #[serde_typetag] impl UserInput for MouseScrollDirection { - /// [`MouseScrollDirection`] always acts as a virtual button. + /// [`MouseScrollDirection`] acts as a virtual button. #[inline] - fn kind(&self) -> InputKind { - InputKind::Button + fn kind(&self) -> InputControlKind { + InputControlKind::Button } /// Checks if there is any recent mouse wheel movement along the specified direction. @@ -425,10 +490,9 @@ impl UserInput for MouseScrollDirection { None } - /// Returns a [`BasicInputs`] that only contains the [`MouseScrollDirection`] itself, - /// as it represents a simple virtual button. + /// [`MouseScrollDirection`] represents a simple virtual button. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Simple(Box::new(*self)) } @@ -439,61 +503,65 @@ impl UserInput for MouseScrollDirection { } } -/// Captures ongoing mouse wheel movement on a specific axis. +/// Amount of mouse wheel scrolling on a single axis (X or Y). +/// +/// # Behaviors +/// +/// - Raw Value: Captures the amount of scrolling on the chosen axis (X or Y). +/// - Value Processing: [`WithAxisProcessingPipelineExt`] offers methods +/// for managing a processing pipeline that can be applied to the raw value before use. +/// - Activation: Only if its value is non-zero. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// // Y-axis movement +/// let input = MouseScrollAxis::Y; +/// +/// // Scrolling on the chosen axis activates the input +/// app.send_axis_values(MouseScrollAxis::Y, [1.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [1.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the value) +/// let doubled = MouseScrollAxis::Y.sensitivity(2.0); +/// assert_eq!(app.read_axis_values(doubled), [2.0]); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseScrollAxis { /// The axis that this input tracks. pub(crate) axis: DualAxisType, - /// The method to interpret values on the axis, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`AxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: AxisProcessor, } impl MouseScrollAxis { - /// The horizontal [`MouseScrollAxis`] for continuous input. - /// No processing is applied to raw data from the mouse. + /// Horizontal scrolling of the mouse wheel. No processing is applied to raw data from the mouse. pub const X: Self = Self { axis: DualAxisType::X, - input_mode: AxisInputMode::Analog, processor: AxisProcessor::None, }; - /// The vertical [`MouseScrollAxis`] for continuous input. - /// No processing is applied to raw data from the mouse. + /// Vertical scrolling of the mouse wheel. No processing is applied to raw data from the mouse. pub const Y: Self = Self { axis: DualAxisType::Y, - input_mode: AxisInputMode::Analog, - processor: AxisProcessor::None, - }; - - /// The horizontal [`MouseScrollAxis`] for discrete input. - /// No processing is applied to raw data from the mouse. - pub const X_DIGITAL: Self = Self { - axis: DualAxisType::X, - input_mode: AxisInputMode::Digital, - processor: AxisProcessor::None, - }; - - /// The vertical [`MouseScrollAxis`] for discrete input. - /// No processing is applied to raw data from the mouse. - pub const Y_DIGITAL: Self = Self { - axis: DualAxisType::Y, - input_mode: AxisInputMode::Digital, processor: AxisProcessor::None, }; } #[serde_typetag] impl UserInput for MouseScrollAxis { - /// [`MouseScrollAxis`] always acts as an axis input. + /// [`MouseScrollAxis`] acts as an axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::Axis + fn kind(&self) -> InputControlKind { + InputControlKind::Axis } /// Checks if there is any recent mouse wheel movement along the specified axis. @@ -510,7 +578,6 @@ impl UserInput for MouseScrollAxis { fn value(&self, input_streams: &InputStreams) -> f32 { let movement = accumulate_wheel_movement(input_streams); let value = self.axis.get_value(movement); - let value = self.input_mode.axis_value(value); self.processor.process(value) } @@ -521,9 +588,9 @@ impl UserInput for MouseScrollAxis { None } - /// Returns both positive and negative [`MouseScrollDirection`]s to represent the movement. + /// [`MouseScrollAxis`] represents a composition of two [`MouseScrollDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(MouseScrollDirection(self.axis.negative())), Box::new(MouseScrollDirection(self.axis.positive())), @@ -557,40 +624,47 @@ impl WithAxisProcessingPipelineExt for MouseScrollAxis { } } -/// Captures ongoing mouse wheel movement on both X and Y axes. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +/// Amount of mouse wheel scrolling on both axes. +/// +/// # Behaviors +/// +/// - Raw Value: Captures the amount of scrolling on the chosen axis (X or Y). +/// - Value Processing: [`WithAxisProcessingPipelineExt`] offers methods +/// for managing a processing pipeline that can be applied to the raw value before use. +/// - Activation: Only if its value is non-zero. +/// +/// ```rust +/// use bevy::prelude::*; +/// use bevy::input::InputPlugin; +/// use leafwing_input_manager::prelude::*; +/// +/// let mut app = App::new(); +/// app.add_plugins(InputPlugin); +/// +/// let input = MouseScroll::default(); +/// +/// // Scrolling on either axis activates the input +/// app.send_axis_values(MouseScrollAxis::Y, [3.0]); +/// app.update(); +/// assert_eq!(app.read_axis_values(input), [0.0, 3.0]); +/// +/// // You can configure a processing pipeline (e.g., doubling the Y value) +/// let doubled = MouseScroll::default().sensitivity_y(2.0); +/// assert_eq!(app.read_axis_values(doubled), [0.0, 6.0]); +/// ``` +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[must_use] pub struct MouseScroll { - /// The method to interpret values on both axes, - /// either [`AxisInputMode::Analog`] or [`AxisInputMode::Digital`]. - pub(crate) input_mode: AxisInputMode, - - /// The [`DualAxisProcessor`] used to handle input values. + /// Processes input values. pub(crate) processor: DualAxisProcessor, } -impl MouseScroll { - /// Continuous [`MouseScroll`] for input on both X and Y axes. - /// No processing is applied to raw data from the mouse. - pub const RAW: Self = Self { - input_mode: AxisInputMode::Analog, - processor: DualAxisProcessor::None, - }; - - /// Discrete [`MouseScroll`] for input on both X and Y axes. - /// No processing is applied to raw data from the mouse. - pub const DIGITAL: Self = Self { - input_mode: AxisInputMode::Digital, - processor: DualAxisProcessor::None, - }; -} - #[serde_typetag] impl UserInput for MouseScroll { - /// [`MouseScroll`] always acts as an axis input. + /// [`MouseScroll`] acts as an axis input. #[inline] - fn kind(&self) -> InputKind { - InputKind::DualAxis + fn kind(&self) -> InputControlKind { + InputControlKind::DualAxis } /// Checks if there is any recent mouse wheel movement. @@ -598,8 +672,7 @@ impl UserInput for MouseScroll { #[inline] fn pressed(&self, input_streams: &InputStreams) -> bool { let movement = accumulate_wheel_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); + let value = self.processor.process(movement); value != Vec2::ZERO } @@ -608,9 +681,8 @@ impl UserInput for MouseScroll { #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { let movement = accumulate_wheel_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); - self.input_mode.dual_axis_magnitude(value) + let value = self.processor.process(movement); + value.length() } /// Retrieves the mouse scroll movement on both axes after processing by the associated processor. @@ -618,14 +690,13 @@ impl UserInput for MouseScroll { #[inline] fn axis_pair(&self, input_streams: &InputStreams) -> Option { let movement = accumulate_wheel_movement(input_streams); - let value = self.input_mode.dual_axis_value(movement); - let value = self.processor.process(value); + let value = self.processor.process(movement); Some(DualAxisData::from_xy(value)) } - /// Returns four [`MouseScrollDirection`]s to represent the movement. + /// [`MouseScroll`] represents a composition of four [`MouseScrollDirection`]s. #[inline] - fn basic_inputs(&self) -> BasicInputs { + fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ Box::new(MouseScrollDirection::UP), Box::new(MouseScrollDirection::DOWN), @@ -697,15 +768,15 @@ mod tests { #[test] fn test_mouse_button() { let left = MouseButton::Left; - assert_eq!(left.kind(), InputKind::Button); + assert_eq!(left.kind(), InputControlKind::Button); assert_eq!(left.raw_inputs(), RawInputs::from_mouse_buttons([left])); let middle = MouseButton::Middle; - assert_eq!(middle.kind(), InputKind::Button); + assert_eq!(middle.kind(), InputControlKind::Button); assert_eq!(middle.raw_inputs(), RawInputs::from_mouse_buttons([middle])); let right = MouseButton::Right; - assert_eq!(right.kind(), InputKind::Button); + assert_eq!(right.kind(), InputControlKind::Button); assert_eq!(right.raw_inputs(), RawInputs::from_mouse_buttons([right])); // No inputs @@ -747,17 +818,17 @@ mod tests { #[test] fn test_mouse_move() { let mouse_move_up = MouseMoveDirection::UP; - assert_eq!(mouse_move_up.kind(), InputKind::Button); + assert_eq!(mouse_move_up.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_mouse_move_directions([mouse_move_up]); assert_eq!(mouse_move_up.raw_inputs(), raw_inputs); let mouse_move_y = MouseMoveAxis::Y; - assert_eq!(mouse_move_y.kind(), InputKind::Axis); + assert_eq!(mouse_move_y.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::Y]); assert_eq!(mouse_move_y.raw_inputs(), raw_inputs); - let mouse_move = MouseMove::RAW; - assert_eq!(mouse_move.kind(), InputKind::DualAxis); + let mouse_move = MouseMove::default(); + assert_eq!(mouse_move.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_mouse_move_axes([DualAxisType::X, DualAxisType::Y]); assert_eq!(mouse_move.raw_inputs(), raw_inputs); @@ -813,7 +884,7 @@ mod tests { // Set changes in movement to (2.0, 3.0) let data = DualAxisData::new(2.0, 3.0); let mut app = test_app(); - app.send_axis_values(MouseMove::RAW, [data.x(), data.y()]); + app.send_axis_values(MouseMove::default(), [data.x(), data.y()]); app.update(); let inputs = InputStreams::from_world(&app.world, None); pressed(&mouse_move_up, &inputs); @@ -824,17 +895,17 @@ mod tests { #[test] fn test_mouse_scroll() { let mouse_scroll_up = MouseScrollDirection::UP; - assert_eq!(mouse_scroll_up.kind(), InputKind::Button); + assert_eq!(mouse_scroll_up.kind(), InputControlKind::Button); let raw_inputs = RawInputs::from_mouse_scroll_directions([mouse_scroll_up]); assert_eq!(mouse_scroll_up.raw_inputs(), raw_inputs); let mouse_scroll_y = MouseScrollAxis::Y; - assert_eq!(mouse_scroll_y.kind(), InputKind::Axis); + assert_eq!(mouse_scroll_y.kind(), InputControlKind::Axis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::Y]); assert_eq!(mouse_scroll_y.raw_inputs(), raw_inputs); - let mouse_scroll = MouseScroll::RAW; - assert_eq!(mouse_scroll.kind(), InputKind::DualAxis); + let mouse_scroll = MouseScroll::default(); + assert_eq!(mouse_scroll.kind(), InputControlKind::DualAxis); let raw_inputs = RawInputs::from_mouse_scroll_axes([DualAxisType::X, DualAxisType::Y]); assert_eq!(mouse_scroll.raw_inputs(), raw_inputs); @@ -880,7 +951,7 @@ mod tests { // Set changes in scrolling to (2.0, 3.0) let data = DualAxisData::new(2.0, 3.0); let mut app = test_app(); - app.send_axis_values(MouseScroll::RAW, [data.x(), data.y()]); + app.send_axis_values(MouseScroll::default(), [data.x(), data.y()]); app.update(); let inputs = InputStreams::from_world(&app.world, None); pressed(&mouse_scroll_up, &inputs); diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs index 5f54cfd4..6c0b4715 100644 --- a/tests/mouse_motion.rs +++ b/tests/mouse_motion.rs @@ -88,7 +88,7 @@ fn mouse_move_dual_axis_mocking() { let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = MouseMove::RAW; + let input = MouseMove::default(); app.send_axis_values(input, [1.0, 0.0]); let mut events = app.world.resource_mut::>(); @@ -198,9 +198,12 @@ fn mouse_move_single_axis() { #[test] fn mouse_move_dual_axis() { let mut app = test_app(); - app.insert_resource(InputMap::new([(AxislikeTestAction::XY, MouseMove::RAW)])); + app.insert_resource(InputMap::new([( + AxislikeTestAction::XY, + MouseMove::default(), + )])); - let input = MouseMove::RAW; + let input = MouseMove::default(); app.send_axis_values(input, [5.0, 0.0]); app.update(); @@ -219,10 +222,10 @@ fn mouse_move_discrete() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - MouseMove::DIGITAL, + MouseMove::default().digital(), )])); - let input = MouseMove::RAW; + let input = MouseMove::default(); app.send_axis_values(input, [0.0, -2.0]); app.update(); @@ -246,12 +249,12 @@ fn mouse_drag() { input_map.insert( AxislikeTestAction::XY, - InputChord::from_single(MouseMove::RAW).with(MouseButton::Right), + InputChord::from_single(MouseMove::default()).with(MouseButton::Right), ); app.insert_resource(input_map); - let input = MouseMove::RAW; + let input = MouseMove::default(); app.send_axis_values(input, [5.0, 0.0]); app.press_input(MouseButton::Right); app.update(); diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs index 7cd38abf..144326ad 100644 --- a/tests/mouse_wheel.rs +++ b/tests/mouse_wheel.rs @@ -89,7 +89,7 @@ fn mouse_scroll_dual_axis_mocking() { let mut events = app.world.resource_mut::>(); assert_eq!(events.drain().count(), 0); - let input = MouseScroll::RAW; + let input = MouseScroll::default(); app.send_axis_values(input, [-1.0]); let mut events = app.world.resource_mut::>(); @@ -199,9 +199,12 @@ fn mouse_scroll_single_axis() { #[test] fn mouse_scroll_dual_axis() { let mut app = test_app(); - app.insert_resource(InputMap::new([(AxislikeTestAction::XY, MouseScroll::RAW)])); + app.insert_resource(InputMap::new([( + AxislikeTestAction::XY, + MouseScroll::default(), + )])); - let input = MouseScroll::RAW; + let input = MouseScroll::default(); app.send_axis_values(input, [5.0, 0.0]); app.update(); @@ -220,10 +223,10 @@ fn mouse_scroll_discrete() { let mut app = test_app(); app.insert_resource(InputMap::new([( AxislikeTestAction::XY, - MouseScroll::DIGITAL, + MouseScroll::default().digital(), )])); - let input = MouseScroll::RAW; + let input = MouseScroll::default(); app.send_axis_values(input, [0.0, -2.0]); app.update(); From 9689734575b74119c96583cf095a68e162f6f794 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Tue, 7 May 2024 13:23:15 +0800 Subject: [PATCH 17/20] RELEASES.md --- RELEASES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 8e2be9ae..68401fdc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,10 +4,9 @@ ### Breaking Changes -- removed `UserInput` enum in favor of the new `UserInput` trait and its impls (see 'Enhancements: New Inputs' for details). +- removed `UserInput` and `InputKind` enums in favor of the new `UserInput` trait and its impls (see 'Enhancements: New Inputs' for details). - renamed `Modifier` enum to `ModifierKey`. - - refactored `InputKind` variants for representing user inputs, it now represents the kind of data an input can provide (e.g., button-like input, axis-like input). - - by default, all input events are unprocessed now, using `With*ProcessingPipelineExt` methods to define your preferred processing steps. + - by default, all input events are unprocessed now, using `With*ProcessingPipelineExt` methods to configure your preferred processing steps. - applied clashing check to continuous mouse inputs, for example: - `MouseScrollAxis::Y` will clash with `MouseScrollDirection::UP` and `MouseScrollDirection::DOWN`. - `MouseMove` will clash with all the two axes and the four directions. From 49d71ef437585be74f99123680353e12d58816ac Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sun, 2 Jun 2024 04:26:27 +0800 Subject: [PATCH 18/20] Fix wrong gamepad button value --- src/user_input/gamepad.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index f8b0de50..de44d75f 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -461,6 +461,16 @@ fn button_pressed( input_streams.gamepad_buttons.pressed(button) } +/// Checks if the given [`GamepadButtonType`] is currently pressed. +#[must_use] +#[inline] +fn button_pressed_any(input_streams: &InputStreams, button: GamepadButtonType) -> bool { + input_streams + .gamepads + .iter() + .any(|gamepad| button_pressed(input_streams, gamepad, *button)) +} + /// Retrieves the current value of the given [`GamepadButtonType`]. #[must_use] #[inline] @@ -469,8 +479,8 @@ fn button_value( gamepad: Gamepad, button: GamepadButtonType, ) -> Option { - // This implementation differs from `button_pressed()` because the upstream bevy::input - // still waffles about whether triggers are buttons or axes. + // This implementation differs from `button_pressed()` + // because the upstream bevy::input still waffles about whether triggers are buttons or axes. // So, we consider the axes for consistency with other gamepad axes (e.g., thumb sticks). let button = GamepadButton::new(gamepad, button); input_streams.gamepad_button_axes.get(button) @@ -485,7 +495,7 @@ fn button_value_any(input_streams: &InputStreams, button: GamepadButtonType) -> return value; } } - 0.0 + f32::from(button_pressed_any(input_streams, button)) } // Built-in support for Bevy's GamepadButtonType. @@ -504,10 +514,7 @@ impl UserInput for GamepadButtonType { if let Some(gamepad) = input_streams.associated_gamepad { button_pressed(input_streams, gamepad, *self) } else { - input_streams - .gamepads - .iter() - .any(|gamepad| button_pressed(input_streams, gamepad, *self)) + button_pressed_any(input_streams, *self) } } @@ -516,7 +523,8 @@ impl UserInput for GamepadButtonType { #[inline] fn value(&self, input_streams: &InputStreams) -> f32 { if let Some(gamepad) = input_streams.associated_gamepad { - button_value(input_streams, gamepad, *self).unwrap_or_default() + button_value(input_streams, gamepad, *self) + .unwrap_or_else(|| f32::from(button_pressed(input_streams, gamepad, *self))) } else { button_value_any(input_streams, *self) } From 0d5e93e8eda2d564dfa073dd4b4d9690b29c1f85 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sun, 2 Jun 2024 05:02:13 +0800 Subject: [PATCH 19/20] Rename `InputChord::from_multiple` to `new` --- README.md | 2 +- examples/clash_handling.rs | 13 +++++-------- src/action_state.rs | 5 +---- src/clashing_inputs.rs | 36 ++++++++++++++---------------------- src/input_map.rs | 2 +- src/input_mocking.rs | 2 +- src/user_input/chord.rs | 28 ++++++++++++++-------------- src/user_input/gamepad.rs | 2 +- tests/clashes.rs | 18 ++++++------------ 9 files changed, 44 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index e3937729..e827ea13 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ and a single input can result in multiple actions being triggered, which can be - Ergonomic insertion API that seamlessly blends multiple input types for you - Can't decide between `input_map.insert(Action::Jump, KeyCode::Space)` and `input_map.insert(Action::Jump, GamepadButtonType::South)`? Have both! - Full support for arbitrary button combinations: chord your heart out. - - `input_map.insert(Action::Console, InputChord::from_multiple([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))` + - `input_map.insert(Action::Console, InputChord::new([KeyCode::ControlLeft, KeyCode::Shift, KeyCode::KeyC]))` - Sophisticated input disambiguation with the `ClashStrategy` enum: stop triggering individual buttons when you meant to press a chord! - Create an arbitrary number of strongly typed disjoint action sets by adding multiple copies of this plugin: decouple your camera and player state - Local multiplayer support: freely bind keys to distinct entities, rather than worrying about singular global state diff --git a/examples/clash_handling.rs b/examples/clash_handling.rs index a1dc4782..f8a4475d 100644 --- a/examples/clash_handling.rs +++ b/examples/clash_handling.rs @@ -35,14 +35,11 @@ fn spawn_input_map(mut commands: Commands) { // Setting up input mappings in the obvious way let mut input_map = InputMap::new([(One, Digit1), (Two, Digit2), (Three, Digit3)]); - input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); - input_map.insert(OneAndThree, InputChord::from_multiple([Digit1, Digit3])); - input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); - - input_map.insert( - OneAndTwoAndThree, - InputChord::from_multiple([Digit1, Digit2, Digit3]), - ); + input_map.insert(OneAndTwo, InputChord::new([Digit1, Digit2])); + input_map.insert(OneAndThree, InputChord::new([Digit1, Digit3])); + input_map.insert(TwoAndThree, InputChord::new([Digit2, Digit3])); + + input_map.insert(OneAndTwoAndThree, InputChord::new([Digit1, Digit2, Digit3])); commands.spawn(InputManagerBundle::with_map(input_map)); } diff --git a/src/action_state.rs b/src/action_state.rs index 5a6d3cd2..22387914 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -710,10 +710,7 @@ mod tests { let mut input_map = InputMap::default(); input_map.insert(Action::One, Digit1); input_map.insert(Action::Two, Digit2); - input_map.insert( - Action::OneAndTwo, - InputChord::from_multiple([Digit1, Digit2]), - ); + input_map.insert(Action::OneAndTwo, InputChord::new([Digit1, Digit2])); let mut app = App::new(); app.add_plugins(InputPlugin); diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs index 42a5e557..d46ddb97 100644 --- a/src/clashing_inputs.rs +++ b/src/clashing_inputs.rs @@ -360,20 +360,14 @@ mod tests { input_map.insert(One, Digit1); input_map.insert(Two, Digit2); - input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); - input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); - input_map.insert( - OneAndTwoAndThree, - InputChord::from_multiple([Digit1, Digit2, Digit3]), - ); - input_map.insert(CtrlOne, InputChord::from_multiple([ControlLeft, Digit1])); - input_map.insert(AltOne, InputChord::from_multiple([AltLeft, Digit1])); - input_map.insert( - CtrlAltOne, - InputChord::from_multiple([ControlLeft, AltLeft, Digit1]), - ); + input_map.insert(OneAndTwo, InputChord::new([Digit1, Digit2])); + input_map.insert(TwoAndThree, InputChord::new([Digit2, Digit3])); + input_map.insert(OneAndTwoAndThree, InputChord::new([Digit1, Digit2, Digit3])); + input_map.insert(CtrlOne, InputChord::new([ControlLeft, Digit1])); + input_map.insert(AltOne, InputChord::new([AltLeft, Digit1])); + input_map.insert(CtrlAltOne, InputChord::new([ControlLeft, AltLeft, Digit1])); input_map.insert(MoveDPad, KeyboardVirtualDPad::ARROW_KEYS); - input_map.insert(CtrlUp, InputChord::from_multiple([ControlLeft, ArrowUp])); + input_map.insert(CtrlUp, InputChord::new([ControlLeft, ArrowUp])); input_map } @@ -394,13 +388,13 @@ mod tests { let a = KeyA; let b = KeyB; let c = KeyC; - let ab = InputChord::from_multiple([KeyA, KeyB]); - let bc = InputChord::from_multiple([KeyB, KeyC]); - let abc = InputChord::from_multiple([KeyA, KeyB, KeyC]); + let ab = InputChord::new([KeyA, KeyB]); + let bc = InputChord::new([KeyB, KeyC]); + let abc = InputChord::new([KeyA, KeyB, KeyC]); let axyz_dpad = KeyboardVirtualDPad::new(KeyA, KeyX, KeyY, KeyZ); let abcd_dpad = KeyboardVirtualDPad::WASD; - let ctrl_up = InputChord::from_multiple([ArrowUp, ControlLeft]); + let ctrl_up = InputChord::new([ArrowUp, ControlLeft]); let directions_dpad = KeyboardVirtualDPad::ARROW_KEYS; assert!(!test_input_clash(a, b)); @@ -425,7 +419,7 @@ mod tests { action_a: One, action_b: OneAndTwo, inputs_a: vec![Box::new(Digit1)], - inputs_b: vec![Box::new(InputChord::from_multiple([Digit1, Digit2]))], + inputs_b: vec![Box::new(InputChord::new([Digit1, Digit2]))], }; assert_eq!(observed_clash, correct_clash); @@ -441,10 +435,8 @@ mod tests { let correct_clash = Clash { action_a: OneAndTwoAndThree, action_b: OneAndTwo, - inputs_a: vec![Box::new(InputChord::from_multiple([ - Digit1, Digit2, Digit3, - ]))], - inputs_b: vec![Box::new(InputChord::from_multiple([Digit1, Digit2]))], + inputs_a: vec![Box::new(InputChord::new([Digit1, Digit2, Digit3]))], + inputs_b: vec![Box::new(InputChord::new([Digit1, Digit2]))], }; assert_eq!(observed_clash, correct_clash); diff --git a/src/input_map.rs b/src/input_map.rs index 31a6a47b..e4e1956e 100644 --- a/src/input_map.rs +++ b/src/input_map.rs @@ -622,7 +622,7 @@ mod tests { default_keyboard_map.insert(Action::Run, KeyCode::ShiftLeft); default_keyboard_map.insert( Action::Hide, - InputChord::from_multiple([KeyCode::ControlLeft, KeyCode::KeyH]), + InputChord::new([KeyCode::ControlLeft, KeyCode::KeyH]), ); let mut default_gamepad_map = InputMap::default(); diff --git a/src/input_mocking.rs b/src/input_mocking.rs index 5c3650f8..f239b8a5 100644 --- a/src/input_mocking.rs +++ b/src/input_mocking.rs @@ -55,7 +55,7 @@ use crate::user_input::*; /// /// // Or use chords to press multiple keys at the same time! /// let bevy = [KeyCode::KeyB, KeyCode::KeyE, KeyCode::KeyV, KeyCode::KeyY]; -/// app.press_input(InputChord::from_multiple(bevy)); +/// app.press_input(InputChord::new(bevy)); /// /// // Send values to an axis. /// app.send_axis_values(MouseScrollAxis::Y, [5.0]); diff --git a/src/user_input/chord.rs b/src/user_input/chord.rs index 61453190..b9114649 100644 --- a/src/user_input/chord.rs +++ b/src/user_input/chord.rs @@ -43,7 +43,7 @@ use crate::user_input::{DualAxisData, InputControlKind, UserInput}; /// app.add_plugins(InputPlugin); /// /// // Define a chord using A and B keys -/// let input = InputChord::from_multiple([KeyCode::KeyA, KeyCode::KeyB]); +/// let input = InputChord::new([KeyCode::KeyA, KeyCode::KeyB]); /// /// // Pressing only one key doesn't activate the input /// app.press_input(KeyCode::KeyA); @@ -85,13 +85,6 @@ pub struct InputChord( ); impl InputChord { - /// Creates a [`InputChord`] that only contains the given [`UserInput`]. - /// You can still use other methods to add different types of inputs into the chord. - #[inline] - pub fn from_single(input: impl UserInput) -> Self { - Self::default().with(input) - } - /// Creates a [`InputChord`] from multiple [`UserInput`]s, avoiding duplicates. /// Note that all elements within the iterator must be of the same type (homogeneous). /// You can still use other methods to add different types of inputs into the chord. @@ -99,17 +92,24 @@ impl InputChord { /// This ensures that the same input isn't added multiple times, /// preventing redundant data fetching from multiple instances of the same input. #[inline] - pub fn from_multiple(inputs: impl IntoIterator) -> Self { + pub fn new(inputs: impl IntoIterator) -> Self { Self::default().with_multiple(inputs) } + /// Creates a [`InputChord`] that only contains the given [`UserInput`]. + /// You can still use other methods to add different types of inputs into the chord. + #[inline] + pub fn from_single(input: impl UserInput) -> Self { + Self::default().with(input) + } + /// Adds the given [`UserInput`] into this chord, avoiding duplicates. /// /// This ensures that the same input isn't added multiple times, /// preventing redundant data fetching from multiple instances of the same input. #[inline] pub fn with(mut self, input: impl UserInput) -> Self { - self.push_boxed(Box::new(input)); + self.push_boxed_unique(Box::new(input)); self } @@ -121,7 +121,7 @@ impl InputChord { #[inline] pub fn with_multiple(mut self, inputs: impl IntoIterator) -> Self { for input in inputs.into_iter() { - self.push_boxed(Box::new(input)); + self.push_boxed_unique(Box::new(input)); } self } @@ -131,7 +131,7 @@ impl InputChord { /// This ensures that the same input isn't added multiple times, /// preventing redundant data fetching from multiple instances of the same input. #[inline] - fn push_boxed(&mut self, input: Box) { + fn push_boxed_unique(&mut self, input: Box) { if !self.0.contains(&input) { self.0.push(input); } @@ -285,7 +285,7 @@ mod tests { #[test] fn test_chord_with_buttons_only() { - let chord = InputChord::from_multiple([KeyCode::KeyC, KeyCode::KeyH]) + let chord = InputChord::new([KeyCode::KeyC, KeyCode::KeyH]) .with(KeyCode::KeyO) .with_multiple([KeyCode::KeyR, KeyCode::KeyD]); @@ -347,7 +347,7 @@ mod tests { #[test] fn test_chord_with_buttons_and_axes() { - let chord = InputChord::from_multiple([KeyCode::KeyA, KeyCode::KeyB]) + let chord = InputChord::new([KeyCode::KeyA, KeyCode::KeyB]) .with(MouseScrollAxis::X) .with(MouseScrollAxis::Y) .with(GamepadStick::LEFT) diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index de44d75f..bb3d1120 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -468,7 +468,7 @@ fn button_pressed_any(input_streams: &InputStreams, button: GamepadButtonType) - input_streams .gamepads .iter() - .any(|gamepad| button_pressed(input_streams, gamepad, *button)) + .any(|gamepad| button_pressed(input_streams, gamepad, button)) } /// Retrieves the current value of the given [`GamepadButtonType`]. diff --git a/tests/clashes.rs b/tests/clashes.rs index 92e5a3e3..5656a742 100644 --- a/tests/clashes.rs +++ b/tests/clashes.rs @@ -50,18 +50,12 @@ fn spawn_input_map(mut commands: Commands) { input_map.insert(One, Digit1); input_map.insert(Two, Digit2); - input_map.insert(OneAndTwo, InputChord::from_multiple([Digit1, Digit2])); - input_map.insert(TwoAndThree, InputChord::from_multiple([Digit2, Digit3])); - input_map.insert( - OneAndTwoAndThree, - InputChord::from_multiple([Digit1, Digit2, Digit3]), - ); - input_map.insert(CtrlOne, InputChord::from_multiple([ControlLeft, Digit1])); - input_map.insert(AltOne, InputChord::from_multiple([AltLeft, Digit1])); - input_map.insert( - CtrlAltOne, - InputChord::from_multiple([ControlLeft, AltLeft, Digit1]), - ); + input_map.insert(OneAndTwo, InputChord::new([Digit1, Digit2])); + input_map.insert(TwoAndThree, InputChord::new([Digit2, Digit3])); + input_map.insert(OneAndTwoAndThree, InputChord::new([Digit1, Digit2, Digit3])); + input_map.insert(CtrlOne, InputChord::new([ControlLeft, Digit1])); + input_map.insert(AltOne, InputChord::new([AltLeft, Digit1])); + input_map.insert(CtrlAltOne, InputChord::new([ControlLeft, AltLeft, Digit1])); commands.spawn(input_map); } From 07dca4edeb6663348ab121744b48babb88338318 Mon Sep 17 00:00:00 2001 From: Shute052 Date: Sun, 2 Jun 2024 06:39:41 +0800 Subject: [PATCH 20/20] Fix ci --- src/plugin.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/plugin.rs b/src/plugin.rs index 7c98e660..7074bb7b 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,17 +1,5 @@ //! Contains the main plugin exported by this crate. -use crate::action_state::{ActionData, ActionState}; -use crate::axislike::{ - AxisType, DualAxis, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, - VirtualAxis, VirtualDPad, -}; -use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection}; -use crate::clashing_inputs::ClashStrategy; -use crate::input_map::InputMap; -use crate::input_processing::*; -use crate::timing::Timing; -use crate::user_input::{InputKind, Modifier, UserInput}; -use crate::Actionlike; use core::hash::Hash; use core::marker::PhantomData; use std::fmt::Debug;