Repository: Leafwing-Studios/leafwing-input-manager (rust)

Issue:
Have an easy way to see if an entire ActionLike is disabled
## What problem does this solve?

https://github.com/Leafwing-Studios/leafwing-input-manager/pull/456 changed the way that disabling actions was handled, from using a `ToggleActions<A>` resource to adding a `disabled` flag **per Action** (not ActionState)
I think there still should be a way to disable an entire `ActionLike`.

Currently we only have the method `disabled(a: &Action)` to check if a given Action is disabled, but very often we would want to check quickly if the entire set of Actions is disabled for a given entity.

One use-case: in networking, if the entire set of actions is disabled, I would like to stop sending network packets for a given entity.
I guess currently I could iterate through all Actions and check if all of them are disabled, but that doesn't seem elegant.

## What solution would you like?

Maybe the `ActionState` should contain additional data that is shared between all Actions, on top of the existing HashMap
```
pub struct ActionState<A: Actionlike> {
    /// The [`ActionData`] of each action
    action_data: HashMap<A, ActionData>,
}
```

Something like
```
pub struct ActionState<A: Actionlike> {
    /// The [`ActionData`] of each action
    action_data: HashMap<A, ActionData>,
    disabled: bool,
}
```
(disabled could be a nested Metadata field)

Then I could easily disable the entire ActionState, instead of disabling each Action individually.



## Related work

Related to https://github.com/Leafwing-Studios/leafwing-input-manager/issues/553


Pull request:
Add ActionState-level disabling
Fixes #563.

I've added `ActionState` level disabling in this PR, while documenting how to disable action updates via run conditions.

The other change that I've made is releasing / resetting all actions when they get disabled: actions started before a cutscene begins should not surprisingly resume upon its conclusion.

Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 47a4bf6..345df4e 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -84,6 +84,12 @@ Input processors allow you to create custom logic for axis-like input manipulati
 - 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.
 
+#### Better disabling
+
+- Actions can now be disabled at both the individual action and `ActionState` level
+- Disabling actions now resets their value, exposed via the `ActionState::reset` method
+- `ActionState::release_all` has been renamed to `ActionState::reset_all` and now resets the values of `Axislike` and `DualAxislike` actions
+
 ### Usability
 
 #### InputMap
diff --git a/src/action_state/mod.rs b/src/action_state/mod.rs
index b4d1621..6a6b55d 100644
--- a/src/action_state/mod.rs
+++ b/src/action_state/mod.rs
@@ -20,6 +20,16 @@ pub use action_data::*;
 ///
 /// Can be used as either a resource or as a [`Component`] on entities that you wish to control directly from player input.
 ///
+/// Actions can be disabled in four different ways, with increasing granularity:
+///
+/// 1. By disabling updates to all actions using a run condition on [`InputManagerSystem::Update`](crate::plugin::InputManagerSystem::Update).
+/// 2. By disabling updates to all actions of type `A` using a run condition on [`tick_action_state::<A>`](crate::systems::tick_action_state).
+/// 3. By setting a specific action state to disabled using [`ActionState::disable`].
+/// 4. By disabling a specific action using [`ActionState::disable_action`].
+///
+/// More general mechanisms of disabling actions will cause specific mechanisms to be ignored.
+/// For example, if an entire action state is disabled, then enabling or disabling individual actions will have no effect.
+///
 /// # Example
 /// ```rust
 /// use bevy::reflect::Reflect;
@@ -63,6 +73,8 @@ pub use action_data::*;
 /// ```
 #[derive(Resource, Component, Clone, Debug, PartialEq, Serialize, Deserialize, Reflect)]
 pub struct ActionState<A: Actionlike> {
+    /// Whether or not all of the actions are disabled.
+    disabled: bool,
     /// The shared action data for each action
     action_data: HashMap<A, ActionData>,
 }
@@ -72,6 +84,7 @@ pub struct ActionState<A: Actionlike> {
 impl<A: Actionlike> Default for ActionState<A> {
     fn default() -> Self {
         Self {
+            disabled: false,
             action_data: HashMap::default(),
         }
     }
@@ -107,7 +120,13 @@ impl<A: Actionlike> ActionState<A> {
     ///
     /// The `action_data` is typically constructed from [`InputMap::process_actions`](crate::input_map::InputMap::process_actions),
     /// which reads from the assorted [`ButtonInput`](bevy::input::ButtonInput) resources.
+    ///
+    /// If this [`ActionState`] is disabled, it will not be updated.
     pub fn update(&mut self, updated_actions: UpdatedActions<A>) {
+        if self.disabled {
+            return;
+        }
+
         for (action, updated_value) in updated_actions.iter() {
             match updated_value {
                 UpdatedValue::Button(pressed) => {
@@ -590,15 +609,31 @@ impl<A: Actionlike> ActionState<A> {
         action_data.state.release();
     }
 
-    /// Releases all [`Buttonlike`](crate::user_input::Buttonlike) actions.
-    pub fn release_all(&mut self) {
+    /// Resets an action to its default state.
+    ///
+    /// Buttons will be released, and axes will be set to 0.
+    pub fn reset(&mut self, action: &A) {
+        match action.input_control_kind() {
+            InputControlKind::Button => self.release(action),
+            InputControlKind::Axis => {
+                let axis_data = self.axis_data_mut_or_default(action);
+                axis_data.value = 0.0;
+            }
+            InputControlKind::DualAxis => {
+                let dual_axis_data = self.dual_axis_data_mut_or_default(action);
+                dual_axis_data.pair = Vec2::ZERO;
+            }
+        }
+    }
+
+    /// Releases all [`Buttonlike`](crate::user_input::Buttonlike) actions,
+    /// sets all [`Axislike`](crate::user_input::Axislike) actions to 0,
+    /// and sets all [`DualAxislike`](crate::user_input::DualAxislike) actions to [`Vec2::ZERO`].
+    pub fn reset_all(&mut self) {
         // Collect out to avoid angering the borrow checker
         let all_actions = self.action_data.keys().cloned().collect::<Vec<A>>();
-        for action in all_actions
-            .into_iter()
-            .filter(|action| action.input_control_kind() == InputControlKind::Button)
-        {
-            self.release(&action);
+        for action in all_actions.into_iter() {
+            self.reset(&action);
         }
     }
 
@@ -606,7 +641,7 @@ impl<A: Actionlike> ActionState<A> {
     ///
     /// The action will be released, and will not be able to be pressed again
     /// until it would have otherwise been released by [`ActionState::release`],
-    /// [`ActionState::release_all`] or [`ActionState::update`].
+    /// [`ActionState::reset`], [`ActionState::reset_all`] or [`ActionState::update`].
     ///
     /// No initial instant will be recorded
     /// Instead, this is set through [`ActionState::tick()`]
@@ -667,35 +702,62 @@ impl<A: Actionlike> ActionState<A> {
         matches!(self.button_data(action), Some(action_data) if action_data.consumed)
     }
 
-    /// Disables the `action`
+    /// Is the entire [`ActionState`] currently disabled?
+    pub fn disabled(&self) -> bool {
+        self.disabled
+    }
+
+    /// Is this `action` currently disabled?
     #[inline]
-    pub fn disable(&mut self, action: &A) {
+    #[must_use]
+    pub fn action_disabled(&self, action: &A) -> bool {
+        if self.disabled {
+            return true;
+        }
+
+        match self.action_data(action) {
+            Some(action_data) => action_data.disabled,
+            None => false,
+        }
+    }
+
+    /// Disables the entire [`ActionState`].
+    ///
+    /// All values will be reset to their default state.
+    #[inline]
+    pub fn disable(&mut self) {
+        self.disabled = true;
+        self.reset_all();
+    }
+
+    /// Disables the `action`.
+    ///
+    /// The action's value will be reset to its default state.
+    #[inline]
+    pub fn disable_action(&mut self, action: &A) {
         let action_data = self.action_data_mut_or_default(action);
 
         action_data.disabled = true;
+        self.reset(action);
     }
 
     /// Disables all actions
     #[inline]
-    pub fn disable_all(&mut self) {
+    pub fn disable_all_actions(&mut self) {
         for action in self.keys() {
-            self.disable(&action);
+            self.disable_action(&action);
         }
     }
 
-    /// Is this `action` currently disabled?
+    /// Enables the entire [`ActionState`]
     #[inline]
-    #[must_use]
-    pub fn disabled(&self, action: &A) -> bool {
-        match self.action_data(action) {
-            Some(action_data) => action_data.disabled,
-            None => false,
-        }
+    pub fn enable(&mut self) {
+        self.disabled = false;
     }
 
     /// Enables the `action`
     #[inline]
-    pub fn enable(&mut self, action: &A) {
+    pub fn enable_action(&mut self, action: &A) {
         let action_data = self.action_data_mut_or_default(action);
 
         action_data.disabled = false;
@@ -703,9 +765,9 @@ impl<A: Actionlike> ActionState<A> {
 
     /// Enables all actions
     #[inline]
-    pub fn enable_all(&mut self) {
+    pub fn enable_all_actions(&mut self) {
         for action in self.keys() {
-            self.enable(&action);
+            self.enable_action(&action);
         }
     }
 
@@ -720,10 +782,7 @@ impl<A: Actionlike> ActionState<A> {
     pub fn pressed(&self, action: &A) -> bool {
         debug_assert_eq!(action.input_control_kind(), InputControlKind::Button);
 
-        if self
-            .action_data(action)
-            .map_or(false, |action_data| action_data.disabled)
-        {
+        if self.action_disabled(action) {
             return false;
         }
 
@@ -744,10 +803,7 @@ impl<A: Actionlike> ActionState<A> {
     pub fn just_pressed(&self, action: &A) -> bool {
         debug_assert_eq!(action.input_control_kind(), InputControlKind::Button);
 
-        if self
-            .action_data(action)
-            .map_or(false, |action_data| action_data.disabled)
-        {
+        if self.action_disabled(action) {
             return false;
         }
 
@@ -770,10 +826,7 @@ impl<A: Actionlike> ActionState<A> {
     pub fn released(&self, action: &A) -> bool {
         debug_assert_eq!(action.input_control_kind(), InputControlKind::Button);
 
-        if self
-            .action_data(action)
-            .map_or(false, |action_data| action_data.disabled)
-        {
+        if self.action_disabled(action) {
             return true;
         }
 
@@ -794,10 +847,7 @@ impl<A: Actionlike> ActionState<A> {
     pub fn just_released(&self, action: &A) -> bool {
         debug_assert_eq!(action.input_control_kind(), InputControlKind::Button);
 
-        if self
-            .action_data(action)
-            .map_or(false, |action_data| action_data.disabled)
-        {
+        if self.action_disabled(action) {
             return false;
         }
 
diff --git a/src/systems.rs b/src/systems.rs
index aba8f47..e740a07 100644
--- a/src/systems.rs
+++ b/src/systems.rs
@@ -484,7 +484,7 @@ pub fn release_on_input_map_removed<A: Actionlike>(
 ) {
     let mut iter = action_state_query.iter_many_mut(removed_components.read());
     while let Some(mut action_state) = iter.fetch_next() {
-        action_state.release_all();
+        action_state.reset_all();
     }
 
     // Detect when an InputMap resource is removed.
@@ -496,7 +496,7 @@ pub fn release_on_input_map_removed<A: Actionlike>(
         // so we know the input map was removed.
 
         if let Some(mut action_state) = action_state_resource {
-            action_state.release_all();
+            action_state.reset_all();
         }
 
         // Reset our local so our removal detection is only triggered once.


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index b856dec..0f29f4b 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -78,7 +78,7 @@ fn disable_input() {
 
     // Disable the global input
     let mut action_state = app.world_mut().resource_mut::<ActionState<Action>>();
-    action_state.disable_all();
+    action_state.disable_all_actions();
 
     // But the player is still paying respects
     app.update();
@@ -90,7 +90,7 @@ fn disable_input() {
         .world_mut()
         .query_filtered::<&mut ActionState<Action>, With<Player>>()
         .single_mut(app.world_mut());
-    action_state.disable_all();
+    action_state.disable_all_actions();
 
     // Now, all respect has faded
     app.update();
@@ -105,7 +105,7 @@ fn disable_input() {
 
     // Re-enable the global input
     let mut action_state = app.world_mut().resource_mut::<ActionState<Action>>();
-    action_state.enable_all();
+    action_state.enable_all_actions();
 
     // And it will start paying respects again
     app.update();