From a6949e8290ec78354257caf9b99e33f8772a2613 Mon Sep 17 00:00:00 2001 From: Shute Date: Sun, 6 Oct 2024 20:54:34 +0100 Subject: [PATCH] Add `threshold` value for `GamepadControlDirection`, `MouseMoveDirection`, and `MouseScrollDirection` (#624) * threshold * minor * address * fix CI * mention panic --- RELEASES.md | 6 ++ src/axislike.rs | 35 +++++++-- src/user_input/gamepad.rs | 57 +++++++++++--- src/user_input/mouse.rs | 152 +++++++++++++++++++++++++++++++------- 4 files changed, 207 insertions(+), 43 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 6a4e7506..fab87d1f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,11 @@ # Release Notes +## Version 0.16.0 (Unreleased) + +### Usability (0.16.0) + +- added `threshold` value for `GamepadControlDirection`, `MouseMoveDirection`, and `MouseScrollDirection` to be considered pressed. + ## Version 0.15.1 ### Enhancements (0.15.1) diff --git a/src/axislike.rs b/src/axislike.rs index b5e6773e..89da095f 100644 --- a/src/axislike.rs +++ b/src/axislike.rs @@ -25,13 +25,23 @@ impl AxisDirection { } } - /// Checks if the given `value` represents an active input in this direction. + /// Checks if the given `value` represents an active input in this direction, + /// considering the specified `threshold`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirement isn't met. #[must_use] #[inline] - pub fn is_active(&self, value: f32) -> bool { + pub fn is_active(&self, value: f32, threshold: f32) -> bool { + assert!(threshold >= 0.0); match self { - Self::Negative => value < 0.0, - Self::Positive => value > 0.0, + Self::Negative => value < -threshold, + Self::Positive => value > threshold, } } } @@ -151,11 +161,20 @@ impl DualAxisDirection { } } - /// Checks if the given `value` represents an active input in this direction. + /// Checks if the given `value` represents an active input in this direction, + /// considering the specified `threshold`. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirement isn't met. #[must_use] #[inline] - 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) + pub fn is_active(&self, value: Vec2, threshold: f32) -> bool { + let axis_value = self.axis().get_value(value); + self.axis_direction().is_active(axis_value, threshold) } } diff --git a/src/user_input/gamepad.rs b/src/user_input/gamepad.rs index 3be5d901..53737a1c 100644 --- a/src/user_input/gamepad.rs +++ b/src/user_input/gamepad.rs @@ -1,7 +1,10 @@ //! Gamepad inputs +use std::hash::{Hash, Hasher}; + use bevy::input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadEvent}; use bevy::input::{Axis, ButtonInput}; +use bevy::math::FloatOrd; use bevy::prelude::{ Events, Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, Gamepads, Reflect, Res, ResMut, Vec2, World, @@ -69,29 +72,55 @@ fn read_axis_value( /// app.update(); /// assert!(app.read_pressed(input)); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] #[must_use] pub struct GamepadControlDirection { /// The axis that this input tracks. pub axis: GamepadAxisType, /// The direction of the axis to monitor (positive or negative). - pub side: AxisDirection, + pub direction: AxisDirection, + + /// The threshold value for the direction to be considered pressed. + /// Must be non-negative. + pub threshold: f32, } impl GamepadControlDirection { /// Creates a [`GamepadControlDirection`] triggered by a negative value on the specified `axis`. #[inline] pub const fn negative(axis: GamepadAxisType) -> Self { - let side = AxisDirection::Negative; - Self { axis, side } + Self { + axis, + direction: AxisDirection::Negative, + threshold: 0.0, + } } /// Creates a [`GamepadControlDirection`] triggered by a positive value on the specified `axis`. #[inline] pub const fn positive(axis: GamepadAxisType) -> Self { - let side = AxisDirection::Positive; - Self { axis, side } + Self { + axis, + direction: AxisDirection::Positive, + threshold: 0.0, + } + } + + /// Sets the `threshold` value. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirement isn't met. + #[inline] + pub fn threshold(mut self, threshold: f32) -> Self { + assert!(threshold >= 0.0); + self.threshold = threshold; + self } /// "Up" on the left analog stick (positive Y-axis movement). @@ -129,7 +158,7 @@ impl UserInput for GamepadControlDirection { /// [`GamepadControlDirection`] represents a simple virtual button. #[inline] fn decompose(&self) -> BasicInputs { - BasicInputs::Simple(Box::new(*self)) + BasicInputs::Simple(Box::new((*self).threshold(0.0))) } } @@ -140,7 +169,7 @@ impl Buttonlike for GamepadControlDirection { #[inline] fn pressed(&self, input_store: &CentralInputStore, gamepad: Gamepad) -> bool { let value = read_axis_value(input_store, gamepad, self.axis); - self.side.is_active(value) + self.direction.is_active(value, self.threshold) } /// Sends a [`GamepadEvent::Axis`] event with a magnitude of 1.0 for the specified direction on the provided [`Gamepad`]. @@ -150,7 +179,7 @@ impl Buttonlike for GamepadControlDirection { let event = GamepadEvent::Axis(GamepadAxisChangedEvent { gamepad, axis_type: self.axis, - value: self.side.full_active_value(), + value: self.direction.full_active_value(), }); world.resource_mut::>().send(event); } @@ -168,6 +197,16 @@ impl Buttonlike for GamepadControlDirection { } } +impl Eq for GamepadControlDirection {} + +impl Hash for GamepadControlDirection { + fn hash(&self, state: &mut H) { + self.axis.hash(state); + self.direction.hash(state); + FloatOrd(self.threshold).hash(state); + } +} + impl UpdatableInput for GamepadAxis { type SourceData = Axis; diff --git a/src/user_input/mouse.rs b/src/user_input/mouse.rs index ef1a086c..2e2cec10 100644 --- a/src/user_input/mouse.rs +++ b/src/user_input/mouse.rs @@ -1,12 +1,12 @@ //! Mouse inputs -use bevy::input::mouse::{MouseButtonInput, MouseMotion, MouseWheel}; +use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel}; use bevy::input::{ButtonInput, ButtonState}; -use bevy::prelude::{ - Entity, Events, Gamepad, MouseButton, Reflect, Res, ResMut, Resource, Vec2, World, -}; +use bevy::math::FloatOrd; +use bevy::prelude::{Entity, Events, Gamepad, Reflect, Res, ResMut, Resource, Vec2, World}; use leafwing_input_manager_macros::serde_typetag; use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; use crate as leafwing_input_manager; use crate::axislike::{DualAxisDirection, DualAxisType}; @@ -112,22 +112,57 @@ impl Buttonlike for MouseButton { /// app.update(); /// assert!(app.read_pressed(input)); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] #[must_use] -pub struct MouseMoveDirection(pub DualAxisDirection); +pub struct MouseMoveDirection { + /// The direction to monitor (up, down, left, or right). + pub direction: DualAxisDirection, + + /// The threshold value for the direction to be considered pressed. + /// Must be non-negative. + pub threshold: f32, +} impl MouseMoveDirection { + /// Sets the `threshold` value. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirement isn't met. + #[inline] + pub fn threshold(mut self, threshold: f32) -> Self { + assert!(threshold >= 0.0); + self.threshold = threshold; + self + } + /// Movement in the upward direction. - pub const UP: Self = Self(DualAxisDirection::Up); + pub const UP: Self = Self { + direction: DualAxisDirection::Up, + threshold: 0.0, + }; /// Movement in the downward direction. - pub const DOWN: Self = Self(DualAxisDirection::Down); + pub const DOWN: Self = Self { + direction: DualAxisDirection::Down, + threshold: 0.0, + }; /// Movement in the leftward direction. - pub const LEFT: Self = Self(DualAxisDirection::Left); + pub const LEFT: Self = Self { + direction: DualAxisDirection::Left, + threshold: 0.0, + }; /// Movement in the rightward direction. - pub const RIGHT: Self = Self(DualAxisDirection::Right); + pub const RIGHT: Self = Self { + direction: DualAxisDirection::Right, + threshold: 0.0, + }; } impl UserInput for MouseMoveDirection { @@ -140,7 +175,7 @@ impl UserInput for MouseMoveDirection { /// [`MouseMoveDirection`] represents a simple virtual button. #[inline] fn decompose(&self) -> BasicInputs { - BasicInputs::Simple(Box::new(*self)) + BasicInputs::Simple(Box::new((*self).threshold(0.0))) } } @@ -151,7 +186,7 @@ impl Buttonlike for MouseMoveDirection { #[inline] fn pressed(&self, input_store: &CentralInputStore, _gamepad: Gamepad) -> bool { let mouse_movement = input_store.pair(&MouseMove::default()); - self.0.is_active(mouse_movement) + self.direction.is_active(mouse_movement, self.threshold) } /// Sends a [`MouseMotion`] event with a magnitude of 1.0 in the direction defined by `self`. @@ -159,7 +194,7 @@ impl Buttonlike for MouseMoveDirection { world .resource_mut::>() .send(MouseMotion { - delta: self.0.full_active_value(), + delta: self.direction.full_active_value(), }); } @@ -170,6 +205,15 @@ impl Buttonlike for MouseMoveDirection { fn release(&self, _world: &mut World) {} } +impl Eq for MouseMoveDirection {} + +impl Hash for MouseMoveDirection { + fn hash(&self, state: &mut H) { + self.direction.hash(state); + FloatOrd(self.threshold).hash(state); + } +} + /// Relative changes in position of mouse movement on a single axis (X or Y). /// /// # Value Processing @@ -234,8 +278,14 @@ impl UserInput for MouseMoveAxis { #[inline] fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ - Box::new(MouseMoveDirection(self.axis.negative())), - Box::new(MouseMoveDirection(self.axis.positive())), + Box::new(MouseMoveDirection { + direction: self.axis.negative(), + threshold: 0.0, + }), + Box::new(MouseMoveDirection { + direction: self.axis.positive(), + threshold: 0.0, + }), ]) } } @@ -422,22 +472,57 @@ impl WithDualAxisProcessingPipelineExt for MouseMove { /// app.update(); /// assert!(app.read_pressed(input)); /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)] #[must_use] -pub struct MouseScrollDirection(pub DualAxisDirection); +pub struct MouseScrollDirection { + /// The direction to monitor (up, down, left, or right). + pub direction: DualAxisDirection, + + /// The threshold value for the direction to be considered pressed. + /// Must be non-negative. + pub threshold: f32, +} impl MouseScrollDirection { + /// Sets the `threshold` value. + /// + /// # Requirements + /// + /// - `threshold` >= `0.0`. + /// + /// # Panics + /// + /// Panics if the requirement isn't met. + #[inline] + pub fn threshold(mut self, threshold: f32) -> Self { + assert!(threshold >= 0.0); + self.threshold = threshold; + self + } + /// Movement in the upward direction. - pub const UP: Self = Self(DualAxisDirection::Up); + pub const UP: Self = Self { + direction: DualAxisDirection::Up, + threshold: 0.0, + }; /// Movement in the downward direction. - pub const DOWN: Self = Self(DualAxisDirection::Down); + pub const DOWN: Self = Self { + direction: DualAxisDirection::Down, + threshold: 0.0, + }; /// Movement in the leftward direction. - pub const LEFT: Self = Self(DualAxisDirection::Left); + pub const LEFT: Self = Self { + direction: DualAxisDirection::Left, + threshold: 0.0, + }; /// Movement in the rightward direction. - pub const RIGHT: Self = Self(DualAxisDirection::Right); + pub const RIGHT: Self = Self { + direction: DualAxisDirection::Right, + threshold: 0.0, + }; } impl UserInput for MouseScrollDirection { @@ -450,7 +535,7 @@ impl UserInput for MouseScrollDirection { /// [`MouseScrollDirection`] represents a simple virtual button. #[inline] fn decompose(&self) -> BasicInputs { - BasicInputs::Simple(Box::new(*self)) + BasicInputs::Simple(Box::new((*self).threshold(0.0))) } } @@ -461,7 +546,7 @@ impl Buttonlike for MouseScrollDirection { #[inline] fn pressed(&self, input_store: &CentralInputStore, _gamepad: Gamepad) -> bool { let movement = input_store.pair(&MouseScroll::default()); - self.0.is_active(movement) + self.direction.is_active(movement, self.threshold) } /// Sends a [`MouseWheel`] event with a magnitude of 1.0 px in the direction defined by `self`. @@ -470,7 +555,7 @@ impl Buttonlike for MouseScrollDirection { /// /// The `window` field will be filled with a placeholder value. fn press(&self, world: &mut World) { - let vec = self.0.full_active_value(); + let vec = self.direction.full_active_value(); world.resource_mut::>().send(MouseWheel { unit: bevy::input::mouse::MouseScrollUnit::Pixel, @@ -487,6 +572,15 @@ impl Buttonlike for MouseScrollDirection { fn release(&self, _world: &mut World) {} } +impl Eq for MouseScrollDirection {} + +impl Hash for MouseScrollDirection { + fn hash(&self, state: &mut H) { + self.direction.hash(state); + FloatOrd(self.threshold).hash(state); + } +} + /// Amount of mouse wheel scrolling on a single axis (X or Y). /// /// # Value Processing @@ -551,8 +645,14 @@ impl UserInput for MouseScrollAxis { #[inline] fn decompose(&self) -> BasicInputs { BasicInputs::Composite(vec![ - Box::new(MouseScrollDirection(self.axis.negative())), - Box::new(MouseScrollDirection(self.axis.positive())), + Box::new(MouseScrollDirection { + direction: self.axis.negative(), + threshold: 0.0, + }), + Box::new(MouseScrollDirection { + direction: self.axis.positive(), + threshold: 0.0, + }), ]) } }