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

Issue:
Adding inversion to mouse and gamepad axes
## What problem does this solve?

I want to implement keyboard and mouse control for a small game. I would like to give the user the choice of inverting the mouse axes independently from the keyboard input. In ego shooters some people like it to move the mouse forward to look up, others move the mouse backwards to look up.

## What solution would you like?

When setting the `InputMap` it would be nice to be able to set some axes to inverted.

## How could this be implemented?

A boolean `inverted` field in `SingleAxis` might already provide this feature. This can be set to `true` to mark this axis as being inverted.

## What alternatives have you considered?

Currently I implement custom `PitchInverted` action to represent the inverted axis for mouse only. This is a workaround and I would prefer a built-in solution, that is easily switchable by user choice.

## Related work

Using `SingleAxis` to solve this would enable inversion not only for mouse but also for all gamepad axes. Inversion could be useful for them as well.


Pull request description:
Add Inversion to SingleAxis, DualAxis, and VirtualDPad
Fixes #247.

Adds `inverted`, `inverted_x`, and `inverted_y` builder-style functions for `SingleAxis`, `DualAxis`, and `VirtualDPad`, where appropriate. The `SingleAxis` implementation uses an `inverted` field, the `DualAxis` implementation just inverts its internal axes, and the `VirtualDPad` implementation swaps the relevant inputs.

Current tests pass, but I haven't written any yet to test inversion.

## Todo
- [ ] Write tests
- [ ] Better documentation?

Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 381d3f0..2ec08f6 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -6,6 +6,7 @@
 
 - Changed `entity` field of `ActionStateDriver` to `targets: ActionStateDriverTarget` with variants for 0, 1, or multiple targets, to allow for one driver 
 to update multiple entities if needed.
+- Added builder-style functions to `SingleAxis`, `DualAxis`, and `VirtualDPad` that invert their output values, allowing, for example, binding inverted camera controls.
 
 ### Docs
 
diff --git a/src/axislike.rs b/src/axislike.rs
index 17dff9c..f980f64 100644
--- a/src/axislike.rs
+++ b/src/axislike.rs
@@ -28,6 +28,8 @@ pub struct SingleAxis {
     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,
     /// 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)
@@ -42,6 +44,7 @@ impl SingleAxis {
             axis_type: axis_type.into(),
             positive_low: threshold,
             negative_low: -threshold,
+            inverted: false,
             value: None,
         }
     }
@@ -56,6 +59,7 @@ impl SingleAxis {
             axis_type: axis_type.into(),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
             value: Some(value),
         }
     }
@@ -67,6 +71,7 @@ impl SingleAxis {
             axis_type: AxisType::MouseWheel(MouseWheelAxisType::X),
             positive_low: 0.,
             negative_low: 0.,
+            inverted: false,
             value: None,
         }
     }
@@ -78,6 +83,7 @@ impl SingleAxis {
             axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
             positive_low: 0.,
             negative_low: 0.,
+            inverted: false,
             value: None,
         }
     }
@@ -89,6 +95,7 @@ impl SingleAxis {
             axis_type: AxisType::MouseMotion(MouseMotionAxisType::X),
             positive_low: 0.,
             negative_low: 0.,
+            inverted: false,
             value: None,
         }
     }
@@ -100,6 +107,7 @@ impl SingleAxis {
             axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
             positive_low: 0.,
             negative_low: 0.,
+            inverted: false,
             value: None,
         }
     }
@@ -112,6 +120,7 @@ impl SingleAxis {
             axis_type: axis_type.into(),
             negative_low: threshold,
             positive_low: f32::MAX,
+            inverted: false,
             value: None,
         }
     }
@@ -124,6 +133,7 @@ impl SingleAxis {
             axis_type: axis_type.into(),
             negative_low: f32::MIN,
             positive_low: threshold,
+            inverted: false,
             value: None,
         }
     }
@@ -135,6 +145,13 @@ impl SingleAxis {
         self.positive_low = deadzone;
         self
     }
+
+    /// Returns this [`SingleAxis`] inverted.
+    #[must_use]
+    pub fn inverted(mut self) -> Self {
+        self.inverted = !self.inverted;
+        self
+    }
 }
 
 impl PartialEq for SingleAxis {
@@ -250,6 +267,28 @@ impl DualAxis {
         self.y = self.y.with_deadzone(deadzone);
         self
     }
+
+    /// Returns this [`DualAxis`] with an inverted X-axis.
+    #[must_use]
+    pub fn inverted_x(mut self) -> DualAxis {
+        self.x = self.x.inverted();
+        self
+    }
+
+    /// Returns this [`DualAxis`] with an inverted Y-axis.
+    #[must_use]
+    pub fn inverted_y(mut self) -> DualAxis {
+        self.y = self.y.inverted();
+        self
+    }
+
+    /// 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
+    }
 }
 
 #[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an un-quoted item
@@ -339,6 +378,25 @@ impl VirtualDPad {
             right: InputKind::MouseMotion(MouseMotionDirection::Right),
         }
     }
+
+    /// 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);
+        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);
+        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);
+        self
+    }
 }
 
 /// A virtual Axis that you can get a value between -1 and 1 from.
@@ -405,6 +463,13 @@ impl VirtualAxis {
             positive: InputKind::GamepadButton(GamepadButtonType::DPadUp),
         }
     }
+
+    /// 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);
+        self
+    }
 }
 
 /// The type of axis used by a [`UserInput`](crate::user_input::UserInput).
diff --git a/src/input_streams.rs b/src/input_streams.rs
index ae593ba..1f5366b 100644
--- a/src/input_streams.rs
+++ b/src/input_streams.rs
@@ -261,6 +261,8 @@ impl<'a> InputStreams<'a> {
         let value_in_axis_range = |axis: &SingleAxis, value: f32| -> f32 {
             if value >= axis.negative_low && value <= axis.positive_low {
                 0.0
+            } else if axis.inverted {
+                -value
             } else {
                 value
             }


Test patch:
diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs
index 115a101..d494941 100644
--- a/tests/gamepad_axis.rs
+++ b/tests/gamepad_axis.rs
@@ -79,6 +79,7 @@ fn game_pad_single_axis_mocking() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
 
     app.send_input(input);
@@ -98,12 +99,14 @@ fn game_pad_dual_axis_mocking() {
             value: Some(1.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
         y: SingleAxis {
             axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY),
             value: Some(0.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
     };
     app.send_input(input);
@@ -132,6 +135,7 @@ fn game_pad_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -144,6 +148,7 @@ fn game_pad_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -156,6 +161,7 @@ fn game_pad_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -168,6 +174,7 @@ fn game_pad_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -181,6 +188,7 @@ fn game_pad_single_axis() {
         // Usually a small deadzone threshold will be set
         positive_low: 0.1,
         negative_low: 0.1,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -193,6 +201,7 @@ fn game_pad_single_axis() {
         value: None,
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs
index e48316f..9e33341 100644
--- a/tests/mouse_motion.rs
+++ b/tests/mouse_motion.rs
@@ -74,6 +74,7 @@ fn mouse_motion_single_axis_mocking() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
 
     app.send_input(input);
@@ -93,12 +94,14 @@ fn mouse_motion_dual_axis_mocking() {
             value: Some(1.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
         y: SingleAxis {
             axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
             value: Some(0.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
     };
     app.send_input(input);
@@ -166,6 +169,7 @@ fn mouse_motion_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -178,6 +182,7 @@ fn mouse_motion_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -190,6 +195,7 @@ fn mouse_motion_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -202,6 +208,7 @@ fn mouse_motion_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -215,6 +222,7 @@ fn mouse_motion_single_axis() {
         // Usually a small deadzone threshold will be set
         positive_low: 0.1,
         negative_low: 0.1,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -227,6 +235,7 @@ fn mouse_motion_single_axis() {
         value: None,
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs
index 40536b2..5e420cb 100644
--- a/tests/mouse_wheel.rs
+++ b/tests/mouse_wheel.rs
@@ -74,6 +74,7 @@ fn mouse_wheel_single_axis_mocking() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
 
     app.send_input(input);
@@ -93,12 +94,14 @@ fn mouse_wheel_dual_axis_mocking() {
             value: Some(1.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
         y: SingleAxis {
             axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
             value: Some(0.),
             positive_low: 0.0,
             negative_low: 0.0,
+            inverted: false,
         },
     };
     app.send_input(input);
@@ -166,6 +169,7 @@ fn mouse_wheel_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -178,6 +182,7 @@ fn mouse_wheel_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -190,6 +195,7 @@ fn mouse_wheel_single_axis() {
         value: Some(1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -202,6 +208,7 @@ fn mouse_wheel_single_axis() {
         value: Some(-1.),
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -215,6 +222,7 @@ fn mouse_wheel_single_axis() {
         // Usually a small deadzone threshold will be set
         positive_low: 0.1,
         negative_low: 0.1,
+        inverted: false,
     };
     app.send_input(input);
     app.update();
@@ -227,6 +235,7 @@ fn mouse_wheel_single_axis() {
         value: None,
         positive_low: 0.0,
         negative_low: 0.0,
+        inverted: false,
     };
     app.send_input(input);
     app.update();