Repository: Leafwing-Studios/leafwing-input-manager
Pull request #164 (2022-07-10 20:59:56)

Release 0.5
This is a WIP PR; it will stay open until the next release. For plans, see the [milestone](https://github.com/Leafwing-Studios/leafwing-input-manager/milestone/6)!


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index d22dbfb..e4b90aa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
-name = "leafwing-input-manager"
+name = "leafwing_input_manager"
 description = "A powerfully direct stateful input manager for the Bevy game engine."
-version = "0.4.1"
+version = "0.5.0"
 authors = ["Leafwing Studios"]
 homepage = "https://leafwing-studios.com/"
 repository = "https://github.com/leafwing-studios/leafwing-input-manager"
@@ -19,20 +19,12 @@ members = ["./", "tools/ci", "macros"]
 
 [features]
 default = ['ui']
-ui = ['bevy_ui']
+ui = ['bevy/bevy_ui']
 
 [dependencies]
-leafwing_input_manager_macros = { path = "macros", version = "0.4.1" }
-
-bevy_app = {version = "0.7", default-features = false}
-bevy_core = {version = "0.7", default-features = false}
-bevy_transform = {version = "0.7", default-features = false}
-bevy_ecs = {version = "0.7", default-features = false}
-bevy_input = {version = "0.7", default-features = false, features = ["serialize"]}
-bevy_math = {version = "0.7", default-features = false}
-bevy_utils = {version = "0.7", default-features = false}
-bevy_ui = {version = "0.7", default-features = false, optional = true}
-bevy_window = {version = "0.7", default-features = false}
+leafwing_input_manager_macros = { path = "macros", version = "0.5" }
+
+bevy = {version = "0.8", default-features = false, features = ["serialize", "bevy_gilrs"]}
 
 petitset = {version = "0.2.1", features = ["serde_compat"]}
 derive_more = {version = "0.99", default-features = false, features = ["display", "error"]}
@@ -40,8 +32,8 @@ itertools = "0.10"
 serde = {version = "1.0", features = ["derive"]}
 
 [dev-dependencies]
-bevy = {version = "0.7", default-features = false, features = ["bevy_gilrs", "bevy_sprite", "bevy_text", "bevy_ui", "bevy_render", "bevy_core_pipeline", "x11"]}
-bevy_egui = {version="0.13", default-features = false}
+bevy = {version = "0.8", default-features = false, features = ["bevy_asset", "bevy_sprite", "bevy_text", "bevy_ui", "bevy_render", "bevy_core_pipeline", "x11"]}
+bevy_egui = "0.15.0"
 
 [lib]
 name = "leafwing_input_manager"
diff --git a/README.md b/README.md
index d8b2b00..c8029de 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,9 @@ and a single input can result in multiple actions being triggered, which can be
 
 ## Features
 
-- Full keyboard, mouse and joystick support for button-like inputs.
+- 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
 - 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
@@ -33,11 +35,6 @@ and a single input can result in multiple actions being triggered, which can be
 
 ## Limitations
 
-- The `Button` enum only includes `KeyCode`, `MouseButton` and `GamepadButtonType`.
-  - This is due to object-safety limitations on the types stored in `bevy::input::Input`
-  - Please file an issue if you would like something more exotic!
-- No built-in support for non-button input types (e.g. gestures or analog sticks).
-  - All methods on `ActionState` are `pub`: it's designed to be hooked into and extended.
 - Gamepads must be manually assigned to each input map: read from the `Gamepads` resource and use `InputMap::set_gamepad`.
 
 ## Instructions
diff --git a/RELEASE-CHECKLIST.md b/RELEASE-CHECKLIST.md
new file mode 100644
index 0000000..9096f8b
--- /dev/null
+++ b/RELEASE-CHECKLIST.md
@@ -0,0 +1,12 @@
+# LWIM Release Checklist
+
+## Adding a new input kind
+
+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`.
+
+## Before release
+
+1. Ensure no tests (other than ones in the README) are ignored.
+2. Manually verify that all examples work.
diff --git a/RELEASES.md b/RELEASES.md
index 74a90a9..2f4c788 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,5 +1,56 @@
 # Release Notes
 
+## Version 0.5
+
+### Enhancements
+
+- Added gamepad axis support.
+  - Use the new `SingleAxis` and `DualAxis` types / variants.
+- Added mousewheel and mouse motion support.
+  - Use the new `SingleAxis` and `DualAxis` types / variants when you care about the continous values.
+  - Use the new `MouseWheelDirection` enum as an `InputKind`.
+- Added `SingleAxis` and `DualAxis` structs that can be supplied to an `InputMap` to trigger on axis inputs.
+- Added `VirtualDPad` struct that can be supplied to an `InputMap` to trigger on four direction-representing inputs.
+- Added `ActionState::action_axis_pair()` which can return an `AxisPair` containing the analog values of a `SingleAxis`, `DualAxis`, or `VirtualDPad`.
+- Added `ActionState::action_value()` which represents the floating point value of any action:
+  - `1.0` or `0.0` for pressed or unpressed button-like inputs
+  - a value (typically) in the range `-1.0..=1.0` for a single axis representing its analog input
+  - or a value (typically) in the range `0.0..=1.0` for a dual axis representing the magnitude (length) of its vector.
+
+### Usability
+
+- If no gamepad is registered to a specific `InputMap`, inputs from any gamepad in the `Gamepads` resource will be used.
+- Removed the `ActionState::reasons_pressed` API.
+  - This API was quite complex, not terribly useful and had nontrivial performance overhead.
+  - This was not needed for axislike inputs in the end.
+- Added `Direction::try_new()` to fallibly create a new `Direction` struct (which cannot be created from the zero vector).
+- Removed the `InputMode` enum.
+  - This was poorly motivated and had no internal usages.
+  - This could not accurately represent more complex compound input types.
+- `ButtonKind` was renamed to `InputKind` to reflect the new non-button input types.
+- Renamed `AxisPair` to `DualAxisData`.
+  - `DualAxisData::new` now takes two `f32` values for ergonomic reasons.
+  - Use `DualAxisData::from_xy` to construct this directly from a `Vec2` as before.
+- Rotation is now measured from the positive x axis in a counterclockwise direction. This applies to both `Rotation` and `Direction`.
+  - This increases consistency with `glam` and makes trigonometry easier.
+- Added `Direction::try_from` which never panics; consider using this in place of `Direction::new`.
+- Converting from a `Direction` (which uses a `Vec2` of `f32`'s internally) to a `Rotation` (which uses exact decidegrees) now has special cases to ensure all eight cardinal directions result in exact degrees.
+  - For example, a unit vector pointing to the Northeast now always converts to a `Direction` with exactly 1350 decidegrees.
+  - Rounding errors may still occur when converting from arbitrary directions to the other 3592 discrete decidegrees.
+- `InputStreams` and `MutableInputStreams` no longer store e.g. `Option<Res<Input<MouseButton>>>`, and instead simply store `Res<Input<MouseButton>>`
+  - This makes them much easier to work with and dramatically simplifies internal logic.
+- `InputStreams::from_world` no longer requires `&mut World`, as it does not require mutable access to any resources.
+- Renamed `InputMocking::send_input_to_gamepad` and `InputMocking::release_input_for_gamepad` to `InputMocking::send_input_as_gamepad` and `InputMocking::send_input_as_gamepad`.
+- Added the `guess_gamepad` method to `InputStreams` and `MutableInputStreams`, which attempts to find an appropriate gamepad to use.
+- `InputMocking::pressed` and `pressed_for_gamepad` no longer require `&mut self`.
+- `UserInput::raw_inputs` now returns a `RawInputs` struct, rather than a tuple struct.
+- The `mouse` and `keyboard` fields on the two `InputStreams` types are now named `mouse_button` and `keycode` respectively.
+
+## Bug fixes
+
+- mocked inputs are now sent at the low-level `Events` form, rather than in their `Input` format.
+  - this ensures that user code that is reading these events directly can be tested accurately.
+
 ## Version 0.4.1
 
 ### Bug fixes
diff --git a/examples/arpg_indirection.rs b/examples/arpg_indirection.rs
index 17ad662..94db648 100644
--- a/examples/arpg_indirection.rs
+++ b/examples/arpg_indirection.rs
@@ -6,7 +6,7 @@
 //! between two distinct [`ActionState`] components.
 
 use bevy::prelude::*;
-use bevy_utils::HashMap;
+use bevy::utils::HashMap;
 use leafwing_input_manager::plugin::InputManagerSystem;
 use leafwing_input_manager::prelude::*;
 
diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs
new file mode 100644
index 0000000..4558a32
--- /dev/null
+++ b/examples/axis_inputs.rs
@@ -0,0 +1,79 @@
+use bevy::prelude::*;
+use leafwing_input_manager::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        // This plugin maps inputs to an input-type agnostic action-state
+        // We need to provide it with an enum which stores the possible actions a player could take
+        .add_plugin(InputManagerPlugin::<Action>::default())
+        // Spawn an entity with Player, InputMap, and ActionState components
+        .add_startup_system(spawn_player)
+        // Read the ActionState in your systems using queries!
+        .add_system(move_player)
+        .run();
+}
+
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)]
+enum Action {
+    Move,
+    Throttle,
+    Rudder,
+}
+
+#[derive(Component)]
+struct Player;
+
+fn spawn_player(mut commands: Commands) {
+    commands
+        .spawn()
+        .insert(Player)
+        .insert_bundle(InputManagerBundle::<Action> {
+            // Stores "which actions are currently activated"
+            action_state: ActionState::default(),
+            // Describes how to convert from player inputs into those actions
+            input_map: InputMap::default()
+                // Configure the left stick as a dual-axis
+                .insert(DualAxis::left_stick(), Action::Move)
+                // Let's bind the right gamepad trigger to the throttle action
+                .insert(GamepadButtonType::RightTrigger2, Action::Throttle)
+                // And we'll use the right stick's x axis as a rudder control
+                .insert(
+                    // This will trigger if the axis is moved 10% or more in either direction.
+                    SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1),
+                    Action::Rudder,
+                )
+                .build(),
+        });
+}
+
+// Query for the `ActionState` component in your game logic systems!
+fn move_player(query: Query<&ActionState<Action>, With<Player>>) {
+    let action_state = query.single();
+
+    // Each action has a button-like state of its own that you can check
+    if action_state.pressed(Action::Move) {
+        // We're working with gamepads, so we want to defensively ensure that we're using the clamped values
+        let axis_pair = action_state.clamped_axis_pair(Action::Move).unwrap();
+        println!("Move:");
+        println!("   distance: {}", axis_pair.length());
+        println!("          x: {}", axis_pair.x());
+        println!("          y: {}", axis_pair.y());
+    }
+
+    if action_state.pressed(Action::Throttle) {
+        // Note that some gamepad buttons are also tied to axes, so even though we used a
+        // GamepadbuttonType::RightTrigger2 binding to trigger the throttle action, we can get a
+        // variable value here if you have a variable right trigger on your gamepad.
+        //
+        // If you don't have a variable trigger, this will just return 0.0 when not pressed and 1.0
+        // when pressed.
+        let value = action_state.clamped_value(Action::Throttle);
+        println!("Throttle: {}", value);
+    }
+
+    if action_state.pressed(Action::Rudder) {
+        let value = action_state.clamped_value(Action::Rudder);
+        println!("Rudder: {}", value);
+    }
+}
diff --git a/examples/binding_menu.rs b/examples/binding_menu.rs
index 982259a..0b7cc59 100644
--- a/examples/binding_menu.rs
+++ b/examples/binding_menu.rs
@@ -1,6 +1,6 @@
 use bevy::{
     ecs::system::SystemParam,
-    input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ElementState},
+    input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ButtonState},
     prelude::*,
 };
 use bevy_egui::{
@@ -8,10 +8,8 @@ use bevy_egui::{
     EguiContext, EguiPlugin,
 };
 use derive_more::Display;
-use leafwing_input_manager::{prelude::*, user_input::InputButton};
-
+use leafwing_input_manager::{prelude::*, user_input::InputKind};
 const UI_MARGIN: f32 = 10.0;
-
 fn main() {
     App::new()
         .insert_resource(ControlSettings::default())
@@ -25,7 +23,6 @@ fn main() {
         .add_system(binding_window_system)
         .run();
 }
-
 fn spawn_player_system(mut commands: Commands, control_settings: Res<ControlSettings>) {
     commands.spawn().insert(control_settings.input.clone());
     commands.insert_resource(InputMap::<UiAction>::new([(
@@ -34,7 +31,6 @@ fn spawn_player_system(mut commands: Commands, control_settings: Res<ControlSett
     )]));
     commands.insert_resource(ActionState::<UiAction>::default());
 }
-
 fn controls_window_system(
     mut commands: Commands,
     mut egui: ResMut<EguiContext>,
@@ -43,7 +39,6 @@ fn controls_window_system(
 ) {
     let main_window = windows.get_primary().unwrap();
     let window_width_margin = egui.ctx_mut().style().spacing.window_margin.left * 2.0;
-
     Window::new("Settings")
         .anchor(Align2::CENTER_CENTER, (0.0, 0.0))
         .collapsible(false)
@@ -52,7 +47,6 @@ fn controls_window_system(
         .show(egui.ctx_mut(), |ui| {
             const INPUT_VARIANTS: usize = 3;
             const COLUMNS_COUNT: usize = INPUT_VARIANTS + 1;
-
             Grid::new("Control grid")
                 .num_columns(COLUMNS_COUNT)
                 .striped(true)
@@ -63,13 +57,15 @@ fn controls_window_system(
                         let inputs = control_settings.input.get(action);
                         for index in 0..INPUT_VARIANTS {
                             let button_text = match inputs.get_at(index) {
-                                Some(UserInput::Single(InputButton::Gamepad(gamepad_button))) => {
+                                Some(UserInput::Single(InputKind::GamepadButton(
+                                    gamepad_button,
+                                ))) => {
                                     format!("🎮 {:?}", gamepad_button)
                                 }
-                                Some(UserInput::Single(InputButton::Keyboard(keycode))) => {
+                                Some(UserInput::Single(InputKind::Keyboard(keycode))) => {
                                     format!("🖮 {:?}", keycode)
                                 }
-                                Some(UserInput::Single(InputButton::Mouse(mouse_button))) => {
+                                Some(UserInput::Single(InputKind::Mouse(mouse_button))) => {
                                     format!("🖱 {:?}", mouse_button)
                                 }
                                 _ => "Empty".to_string(),
@@ -84,7 +80,6 @@ fn controls_window_system(
             ui.expand_to_include_rect(ui.available_rect_before_wrap());
         });
 }
-
 fn buttons_system(
     mut egui: ResMut<EguiContext>,
     mut control_settings: ResMut<ControlSettings>,
@@ -103,7 +98,6 @@ fn buttons_system(
             })
         });
 }
-
 fn binding_window_system(
     mut commands: Commands,
     mut egui: ResMut<EguiContext>,
@@ -116,7 +110,6 @@ fn binding_window_system(
         Some(active_binding) => active_binding,
         None => return,
     };
-
     Window::new(format!("Binding \"{}\"", active_binding.action))
         .anchor(Align2::CENTER_CENTER, (0.0, 0.0))
         .collapsible(false)
@@ -174,7 +167,6 @@ fn binding_window_system(
             }
         });
 }
-
 #[derive(Actionlike, PartialEq, Clone, Copy, Display)]
 pub(crate) enum ControlAction {
     // Movement
@@ -183,7 +175,6 @@ pub(crate) enum ControlAction {
     Left,
     Right,
     Jump,
-
     // Abilities activation
     BaseAttack,
     Ability1,
@@ -191,16 +182,13 @@ pub(crate) enum ControlAction {
     Ability3,
     Ultimate,
 }
-
 #[derive(Actionlike, PartialEq, Clone, Copy)]
 pub(crate) enum UiAction {
     Back,
 }
-
 struct ControlSettings {
     input: InputMap<ControlAction>,
 }
-
 impl Default for ControlSettings {
     fn default() -> Self {
         let mut input = InputMap::default();
@@ -215,17 +203,14 @@ impl Default for ControlSettings {
             .insert(KeyCode::E, ControlAction::Ability2)
             .insert(KeyCode::LShift, ControlAction::Ability3)
             .insert(KeyCode::R, ControlAction::Ultimate);
-
         Self { input }
     }
 }
-
 struct ActiveBinding {
     action: ControlAction,
     index: usize,
     conflict: Option<BindingConflict>,
 }
-
 impl ActiveBinding {
     fn new(action: ControlAction, index: usize) -> Self {
         Self {
@@ -235,12 +220,10 @@ impl ActiveBinding {
         }
     }
 }
-
 struct BindingConflict {
     action: ControlAction,
-    input_button: InputButton,
+    input_button: InputKind,
 }
-
 /// Helper for collecting input
 #[derive(SystemParam)]
 struct InputEvents<'w, 's> {
@@ -248,31 +231,31 @@ struct InputEvents<'w, 's> {
     mouse_buttons: EventReader<'w, 's, MouseButtonInput>,
     gamepad_events: EventReader<'w, 's, GamepadEvent>,
 }
-
 impl InputEvents<'_, '_> {
-    fn input_button(&mut self) -> Option<InputButton> {
+    fn input_button(&mut self) -> Option<InputKind> {
         if let Some(keyboard_input) = self.keys.iter().next() {
-            if keyboard_input.state == ElementState::Released {
+            if keyboard_input.state == ButtonState::Released {
                 if let Some(key_code) = keyboard_input.key_code {
                     return Some(key_code.into());
                 }
             }
         }
-
         if let Some(mouse_input) = self.mouse_buttons.iter().next() {
-            if mouse_input.state == ElementState::Released {
+            if mouse_input.state == ButtonState::Released {
                 return Some(mouse_input.button.into());
             }
         }
-
-        if let Some(GamepadEvent(_, event_type)) = self.gamepad_events.iter().next() {
+        if let Some(GamepadEvent {
+            gamepad: _,
+            event_type,
+        }) = self.gamepad_events.iter().next()
+        {
             if let GamepadEventType::ButtonChanged(button, strength) = event_type.to_owned() {
                 if strength <= 0.5 {
                     return Some(button.into());
                 }
             }
         }
-
         None
     }
 }
diff --git a/examples/consuming_actions.rs b/examples/consuming_actions.rs
index a7a8185..17a5fa6 100644
--- a/examples/consuming_actions.rs
+++ b/examples/consuming_actions.rs
@@ -1,7 +1,7 @@
 //! 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 bevy_ecs::system::Resource;
 use leafwing_input_manager::prelude::*;
 
 use menu_mocking::*;
diff --git a/examples/mouse_motion.rs b/examples/mouse_motion.rs
new file mode 100644
index 0000000..3d2bada
--- /dev/null
+++ b/examples/mouse_motion.rs
@@ -0,0 +1,47 @@
+use bevy::prelude::*;
+use leafwing_input_manager::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_plugin(InputManagerPlugin::<CameraMovement>::default())
+        .add_startup_system(setup)
+        .add_system(pan_camera)
+        .run()
+}
+
+#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq)]
+enum CameraMovement {
+    Pan,
+}
+
+fn setup(mut commands: Commands) {
+    commands
+        .spawn()
+        .insert_bundle(Camera2dBundle::default())
+        .insert_bundle(InputManagerBundle::<CameraMovement> {
+            input_map: InputMap::default()
+                // This will capture the total continous value, for direct use
+                // Note that you can also use discrete gesture-like motion, via the `MouseMotionDirection` enum
+                .insert(DualAxis::mouse_motion(), CameraMovement::Pan)
+                .build(),
+            action_state: ActionState::default(),
+        });
+
+    commands.spawn().insert_bundle(SpriteBundle {
+        transform: Transform::from_scale(Vec3::new(100., 100., 1.)),
+        ..default()
+    });
+}
+
+fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), With<Camera2d>>) {
+    const CAMERA_PAN_RATE: f32 = 0.5;
+
+    let (mut camera_transform, action_state) = query.single_mut();
+
+    let camera_pan_vector = action_state.axis_pair(CameraMovement::Pan).unwrap();
+
+    // Because we're moving the camera, not the object, we want to pan in the opposite direction
+    camera_transform.translation.x -= CAMERA_PAN_RATE * camera_pan_vector.x();
+    camera_transform.translation.y -= CAMERA_PAN_RATE * camera_pan_vector.y();
+}
diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs
new file mode 100644
index 0000000..c5feb6f
--- /dev/null
+++ b/examples/mouse_wheel.rs
@@ -0,0 +1,76 @@
+use bevy::prelude::*;
+use leafwing_input_manager::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_plugin(InputManagerPlugin::<CameraMovement>::default())
+        .add_startup_system(setup)
+        .add_system(zoom_camera)
+        .add_system(pan_camera.after(zoom_camera))
+        .run()
+}
+
+#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq)]
+enum CameraMovement {
+    Zoom,
+    PanLeft,
+    PanRight,
+}
+
+fn setup(mut commands: Commands) {
+    commands
+        .spawn()
+        .insert_bundle(Camera2dBundle::default())
+        .insert_bundle(InputManagerBundle::<CameraMovement> {
+            input_map: InputMap::default()
+                // This will capture the total continous value, for direct use
+                .insert(SingleAxis::mouse_wheel_y(), CameraMovement::Zoom)
+                // This will return a binary button-like output
+                .insert(MouseWheelDirection::Left, CameraMovement::PanLeft)
+                .insert(MouseWheelDirection::Right, CameraMovement::PanRight)
+                // Alternatively, you could model this as a virtual Dpad,
+                // which is extremely useful when you want to model 4-directional buttonlike inputs using the mouse wheel
+                // .insert(VirtualDpad::mouse_wheel(), Pan)
+                // Or even a continous `DualAxis`!
+                // .insert(DualAxis::mouse_wheel(), Pan)
+                .build(),
+            action_state: ActionState::default(),
+        });
+
+    commands.spawn().insert_bundle(SpriteBundle {
+        transform: Transform::from_scale(Vec3::new(100., 100., 1.)),
+        ..default()
+    });
+}
+
+fn zoom_camera(
+    mut query: Query<(&mut OrthographicProjection, &ActionState<CameraMovement>), With<Camera2d>>,
+) {
+    const CAMERA_ZOOM_RATE: f32 = 0.05;
+
+    let (mut camera_projection, action_state) = query.single_mut();
+    // Here, we use the `action_value` method to extract the total net amount that the mouse wheel has travelled
+    // Up and right axis movements are always positive by default
+    let zoom_delta = action_state.value(CameraMovement::Zoom);
+
+    // We want to zoom in when we use mouse wheel up
+    // so we increase the scale proportionally
+    // Note that the projections scale should always be positive (or our images will flip)
+    camera_projection.scale *= 1. - zoom_delta * CAMERA_ZOOM_RATE;
+}
+
+fn pan_camera(mut query: Query<(&mut Transform, &ActionState<CameraMovement>), With<Camera2d>>) {
+    const CAMERA_PAN_RATE: f32 = 10.;
+
+    let (mut camera_transform, action_state) = query.single_mut();
+
+    // When using the `MouseWheelDirection` type, mouse wheel inputs can be treated like simple buttons
+    if action_state.pressed(CameraMovement::PanLeft) {
+        camera_transform.translation.x -= CAMERA_PAN_RATE;
+    }
+
+    if action_state.pressed(CameraMovement::PanRight) {
+        camera_transform.translation.x += CAMERA_PAN_RATE;
+    }
+}
diff --git a/examples/multiplayer.rs b/examples/multiplayer.rs
index 19db799..e70bf62 100644
--- a/examples/multiplayer.rs
+++ b/examples/multiplayer.rs
@@ -41,14 +41,16 @@ impl PlayerBundle {
             // This is a quick and hacky solution:
             // you should coordinate with the `Gamepads` resource to determine the correct gamepad for each player
             // and gracefully handle disconnects
-            .set_gamepad(Gamepad(0))
+            // 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(),
             Player::Two => InputMap::new([
                 (KeyCode::Left, Action::Left),
                 (KeyCode::Right, Action::Right),
                 (KeyCode::Up, Action::Jump),
             ])
-            .set_gamepad(Gamepad(1))
+            .set_gamepad(Gamepad { id: 1 })
             .build(),
         };
 
diff --git a/examples/press_duration.rs b/examples/press_duration.rs
index 82a4cf0..cca8c64 100644
--- a/examples/press_duration.rs
+++ b/examples/press_duration.rs
@@ -80,7 +80,7 @@ fn spawn_player(mut commands: Commands) {
 }
 
 fn spawn_camera(mut commands: Commands) {
-    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
+    commands.spawn_bundle(Camera2dBundle::default());
 }
 
 /// The longer you hold, the faster you dash when released!
diff --git a/examples/send_actions_over_network.rs b/examples/send_actions_over_network.rs
index 0163e31..19295ae 100644
--- a/examples/send_actions_over_network.rs
+++ b/examples/send_actions_over_network.rs
@@ -6,13 +6,12 @@
 //! 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::input::InputPlugin;
 use bevy::prelude::*;
-use bevy_ecs::event::{Events, ManualEventReader};
-use bevy_input::InputPlugin;
 use leafwing_input_manager::action_state::ActionDiff;
 use leafwing_input_manager::prelude::*;
 use leafwing_input_manager::systems::{generate_action_diffs, process_action_diffs};
-use leafwing_input_manager::MockInput;
 
 use std::fmt::Debug;
 
diff --git a/examples/single_player.rs b/examples/single_player.rs
index 456f56e..db0d8d9 100644
--- a/examples/single_player.rs
+++ b/examples/single_player.rs
@@ -75,11 +75,6 @@ impl PlayerBundle {
         use ArpgAction::*;
         let mut input_map = InputMap::default();
 
-        // This is a quick and hacky solution:
-        // you should coordinate with the `Gamepads` resource to determine the correct gamepad for each player
-        // and gracefully handle disconnects
-        input_map.set_gamepad(Gamepad(0));
-
         // Movement
         input_map.insert(KeyCode::Up, Up);
         input_map.insert(GamepadButtonType::DPadUp, Up);
diff --git a/examples/ui_driven_actions.rs b/examples/ui_driven_actions.rs
index 0b552b1..89b3d1d 100644
--- a/examples/ui_driven_actions.rs
+++ b/examples/ui_driven_actions.rs
@@ -1,4 +1,4 @@
-//! Demonstrates how to connect `bevy_ui` buttons to [`ActionState`] components using the [`ActionStateDriver`] component on your button
+//! Demonstrates how to connect `bevy::ui` buttons to [`ActionState`] components using the [`ActionStateDriver`] component on your button
 
 use bevy::prelude::*;
 use leafwing_input_manager::prelude::*;
@@ -49,8 +49,7 @@ fn spawn_player(mut commands: Commands) {
 }
 
 fn spawn_cameras(mut commands: Commands) {
-    commands.spawn_bundle(UiCameraBundle::default());
-    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
+    commands.spawn_bundle(Camera2dBundle::default());
 }
 
 fn spawn_ui(mut commands: Commands, player_query: Query<Entity, With<Player>>) {
diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs
new file mode 100644
index 0000000..a17bec7
--- /dev/null
+++ b/examples/virtual_dpad.rs
@@ -0,0 +1,59 @@
+use bevy::prelude::*;
+use leafwing_input_manager::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        // This plugin maps inputs to an input-type agnostic action-state
+        // We need to provide it with an enum which stores the possible actions a player could take
+        .add_plugin(InputManagerPlugin::<Action>::default())
+        // Spawn an entity with Player, InputMap, and ActionState components
+        .add_startup_system(spawn_player)
+        // Read the ActionState in your systems using queries!
+        .add_system(move_player)
+        .run();
+}
+
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug)]
+enum Action {
+    Move,
+}
+
+#[derive(Component)]
+struct Player;
+
+fn spawn_player(mut commands: Commands) {
+    commands
+        .spawn()
+        .insert(Player)
+        .insert_bundle(InputManagerBundle::<Action> {
+            // Stores "which actions are currently activated"
+            action_state: ActionState::default(),
+            // Map some arbitrary keys into a virtual direction pad that triggers our move action
+            input_map: InputMap::new([(
+                VirtualDPad {
+                    up: KeyCode::W.into(),
+                    down: KeyCode::S.into(),
+                    left: KeyCode::A.into(),
+                    right: KeyCode::D.into(),
+                },
+                Action::Move,
+            )])
+            .build(),
+        });
+}
+
+// Query for the `ActionState` component in your game logic systems!
+fn move_player(query: Query<&ActionState<Action>, With<Player>>) {
+    let action_state = query.single();
+    // If any button in a virtual direction pad is pressed, then the action state is "pressed"
+    if action_state.pressed(Action::Move) {
+        // Virtual direction pads are one of the types which return an AxisPair. The values will be
+        // represented as `-1.0`, `0.0`, or `1.0` depending on the combination of buttons pressed.
+        let axis_pair = action_state.axis_pair(Action::Move).unwrap();
+        println!("Move:");
+        println!("   distance: {}", axis_pair.length());
+        println!("          x: {}", axis_pair.x());
+        println!("          y: {}", axis_pair.y());
+    }
+}
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index 071bc55..9f3f9b8 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -1,7 +1,8 @@
 [package]
 name = "leafwing_input_manager_macros"
 description = "Macros for the `leafwing-input-manager` crate"
-version = "0.4.1"
+version = "0.5.0"
+
 license = "MIT OR Apache-2.0"
 edition = "2021"
 authors = ["Leafwing Studios"]
diff --git a/src/action_state.rs b/src/action_state.rs
index 4f24ec0..458a6e2 100644
--- a/src/action_state.rs
+++ b/src/action_state.rs
@@ -1,11 +1,10 @@
 //! This module contains [`ActionState`] and its supporting methods and impls.
 
-use crate::buttonlike::ButtonState;
-use crate::user_input::UserInput;
 use crate::Actionlike;
+use crate::{axislike::DualAxisData, buttonlike::ButtonState};
 
-use bevy_ecs::{component::Component, entity::Entity};
-use bevy_utils::{Duration, Instant};
+use bevy::ecs::{component::Component, entity::Entity};
+use bevy::utils::{Duration, Instant};
 use serde::{Deserialize, Serialize};
 use std::marker::PhantomData;
 
@@ -16,8 +15,17 @@ use std::marker::PhantomData;
 pub struct ActionData {
     /// Is the action pressed or released?
     pub state: ButtonState,
-    /// What inputs were responsible for causing this action to be pressed?
-    pub reasons_pressed: Vec<UserInput>,
+    /// The "value" of the binding that triggered the action.
+    ///
+    /// See [`ActionState::action_value()`] for more details.
+    ///
+    /// **Warning:** this value may not be bounded as you might expect.
+    /// Consider clamping this to account for multiple triggering inputs.
+    pub value: f32,
+    /// The [`AxisPair`] of the binding that triggered the action.
+    ///
+    /// See [`ActionState::action_axis_pair()`] for more details.
+    pub axis_pair: Option<DualAxisData>,
     /// When was the button pressed / released, and how long has it been held for?
     pub timing: Timing,
     /// Was this action consumed by [`ActionState::consume`]?
@@ -34,7 +42,7 @@ pub struct ActionData {
 /// # Example
 /// ```rust
 /// use leafwing_input_manager::prelude::*;
-/// use bevy_utils::Instant;
+/// use bevy::utils::Instant;
 ///
 /// #[derive(Actionlike, PartialEq, Eq, Clone, Copy, Debug)]
 /// enum Action {
@@ -96,7 +104,8 @@ impl<A: Actionlike> ActionState<A> {
                 ButtonState::Released => self.release(action),
             }
 
-            self.action_data[i].reasons_pressed = action_data[i].reasons_pressed.clone();
+            self.action_data[i].axis_pair = action_data[i].axis_pair;
+            self.action_data[i].value = action_data[i].value;
         }
     }
 
@@ -111,7 +120,7 @@ impl<A: Actionlike> ActionState<A> {
     /// ```rust
     /// use leafwing_input_manager::prelude::*;
     /// use leafwing_input_manager::buttonlike::ButtonState;
-    /// use bevy_utils::Instant;
+    /// use bevy::utils::Instant;
     ///
     /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Debug)]
     /// enum Action {
@@ -181,6 +190,65 @@ impl<A: Actionlike> ActionState<A> {
         self.action_data[action.index()].clone()
     }
 
+    /// Get the value associated with the corresponding `action`
+    ///
+    /// Different kinds of bindings have different ways of calculating the value:
+    ///
+    /// - 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
+    /// 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.
+    /// - Dual axis inputs will return the magnitude of its [`AxisPair`] and will be in the range
+    /// `0.0..=1.0`.
+    /// - Chord inputs will return the value of its first input.
+    ///
+    /// If multiple inputs trigger the same game action at the same time, the value of each
+    /// triggering input will be added together.
+    ///
+    /// # Warning
+    ///
+    /// This value may not be bounded as you might expect.
+    /// Consider clamping this to account for multiple triggering inputs,
+    /// typically using the [`clamped_value`](Self::clamped_value) method instead.
+    pub fn value(&self, action: A) -> f32 {
+        self.action_data(action).value
+    }
+
+    /// Get the value associated with the corresponding `action`, clamped to `[-1.0, 1.0]`.
+    pub fn clamped_value(&self, action: A) -> f32 {
+        self.value(action).clamp(-1., 1.)
+    }
+
+    /// Get the [`DualAxisData`] from the binding that triggered the corresponding `action`.
+    ///
+    /// Only certain events such as [`VirtualDPad`][crate::user_input::VirtualDPad] and
+    /// [`DualAxis`][crate::user_input::DualAxis] provide an [`DualAxisData`], and this
+    /// will return [`None`] for other events.
+    ///
+    /// Chord inputs will return the [`DualAxisData`] of it's first input.
+    ///
+    /// 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.
+    ///
+    /// # Warning
+    ///
+    /// These values may not be bounded as you might expect.
+    /// Consider clamping this to account for multiple triggering inputs,
+    /// typically using the [`clamped_axis_pair`](Self::clamped_axis_pair) method instead.
+    pub fn axis_pair(&self, action: A) -> Option<DualAxisData> {
+        self.action_data(action).axis_pair
+    }
+
+    /// Get the [`DualAxisData`] associated with the corresponding `action`, clamped to `[-1.0, 1.0]`.
+    pub fn clamped_axis_pair(&self, action: A) -> Option<DualAxisData> {
+        self.axis_pair(action)
+            .map(|pair| DualAxisData::new(pair.x().clamp(-1.0, 1.0), pair.y().clamp(-1.0, 1.0)))
+    }
+
     /// Manually sets the [`ActionData`] of the corresponding `action`
     ///
     /// You should almost always use more direct methods, as they are simpler and less error-prone.
@@ -250,7 +318,6 @@ impl<A: Actionlike> ActionState<A> {
 
         if self.pressed(action) {
             self.action_data[index].timing.flip();
-            self.action_data[index].reasons_pressed = Vec::new();
         }
 
         self.action_data[index].state.release();
@@ -300,7 +367,6 @@ impl<A: Actionlike> ActionState<A> {
         // This is the only difference from action_state.release(action)
         self.action_data[index].consumed = true;
         self.action_data[index].state.release();
-        self.action_data[index].reasons_pressed = Vec::new();
         self.action_data[index].timing.flip();
     }
 
@@ -334,7 +400,7 @@ impl<A: Actionlike> ActionState<A> {
         self.action_data[action.index()].state.released()
     }
 
-    /// Was this `action` pressed since the last time [tick](ActionState::tick) was called?
+    /// Was this `action` released since the last time [tick](ActionState::tick) was called?
     #[inline]
     #[must_use]
     pub fn just_released(&self, action: A) -> bool {
@@ -369,46 +435,6 @@ impl<A: Actionlike> ActionState<A> {
             .collect()
     }
 
-    /// The reasons (in terms of [`UserInput`]) that the button was pressed
-    ///
-    /// If the button is currently released, the `Vec<UserInput`> returned will be empty
-    ///
-    /// # Example
-    ///
-    /// ```rust
-    /// use leafwing_input_manager::prelude::*;
-    /// use leafwing_input_manager::buttonlike::ButtonState;
-    /// use leafwing_input_manager::action_state::ActionData;
-    /// use bevy_input::keyboard::KeyCode;
-    ///
-    /// #[derive(Actionlike, Clone)]
-    /// enum PlatformerAction{
-    ///     Move,
-    ///     Jump,
-    /// }
-    ///
-    /// let mut action_state = ActionState::<PlatformerAction>::default();
-    ///
-    /// // Usually this will be done automatically for you, via [`ActionState::update`]
-    /// action_state.set_action_data(PlatformerAction::Jump,
-    ///   ActionData {
-    ///         state: ButtonState::JustPressed,
-    ///         // Manually setting the reason why this action was pressed
-    ///         reasons_pressed: vec![KeyCode::Space.into()],
-    ///         // For the sake of this example, we don't care about any other fields
-    ///         ..Default::default()
-    ///     }
-    /// );
-    ///
-    /// let reasons_jumped = action_state.reasons_pressed(PlatformerAction::Jump);
-    /// assert_eq!(reasons_jumped[0], KeyCode::Space.into());
-    /// ```
-    #[inline]
-    #[must_use]
-    pub fn reasons_pressed(&self, action: A) -> Vec<UserInput> {
-        self.action_data[action.index()].reasons_pressed.clone()
-    }
-
     /// The [`Instant`] that the action was last pressed or released
     ///
     /// If the action was pressed or released since the last time [`ActionState::tick`] was called
@@ -447,7 +473,7 @@ impl<A: Actionlike> Default for ActionState<A> {
 /// # Examples
 ///
 /// By default, [`update_action_state_from_interaction`](crate::systems::update_action_state_from_interaction) uses this component
-/// in order to connect `bevy_ui` buttons to the corresponding `ActionState`.
+/// in order to connect `bevy::ui` buttons to the corresponding `ActionState`.
 ///
 /// ```rust
 /// use bevy::prelude::*;
@@ -534,8 +560,6 @@ impl Timing {
     /// Flips the metaphorical hourglass, storing `current_duration` in `previous_duration` and resetting `instant_started`
     ///
     /// This method is called whenever actions are pressed or released
-    ///
-    /// FIXME: Ensure that the timing starts on the same frame that the input is flipped.
     pub fn flip(&mut self) {
         self.previous_duration = self.current_duration;
         self.current_duration = Duration::ZERO;
@@ -568,9 +592,11 @@ pub enum ActionDiff<A: Actionlike, ID: Eq + Clone + Component> {
     },
 }
 
+#[cfg(test)]
 mod tests {
     use crate as leafwing_input_manager;
-    use crate::Actionlike;
+    use crate::input_mocking::MockInput;
+    use leafwing_input_manager_macros::Actionlike;
 
     #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Debug)]
     enum Action {
@@ -584,9 +610,13 @@ mod tests {
         use crate::action_state::ActionState;
         use crate::clashing_inputs::ClashStrategy;
         use crate::input_map::InputMap;
-        use crate::user_input::InputStreams;
+        use crate::input_streams::InputStreams;
+        use bevy::input::InputPlugin;
         use bevy::prelude::*;
-        use bevy_utils::{Duration, Instant};
+        use bevy::utils::{Duration, Instant};
+
+        let mut app = App::new();
+        app.add_plugin(InputPlugin);
 
         // Action state
         let mut action_state = ActionState::<Action>::default();
@@ -595,11 +625,8 @@ mod tests {
         let mut input_map = InputMap::default();
         input_map.insert(KeyCode::R, Action::Run);
 
-        // Input streams
-        let mut keyboard_input_stream = Input::<KeyCode>::default();
-        let input_streams = InputStreams::from_keyboard(&keyboard_input_stream);
-
         // Starting state
+        let input_streams = InputStreams::from_world(&app.world, None);
         action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll));
 
         assert!(!action_state.pressed(Action::Run));
@@ -608,8 +635,10 @@ mod tests {
         assert!(!action_state.just_released(Action::Run));
 
         // Pressing
-        keyboard_input_stream.press(KeyCode::R);
-        let input_streams = InputStreams::from_keyboard(&keyboard_input_stream);
+        app.send_input(KeyCode::R);
+        // Process the input events into Input<KeyCode> data
+        app.update();
+        let input_streams = InputStreams::from_world(&app.world, None);
 
         action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll));
 
@@ -628,8 +657,9 @@ mod tests {
         assert!(!action_state.just_released(Action::Run));
 
         // Releasing
-        keyboard_input_stream.release(KeyCode::R);
-        let input_streams = InputStreams::from_keyboard(&keyboard_input_stream);
+        app.release_input(KeyCode::R);
+        app.update();
+        let input_streams = InputStreams::from_world(&app.world, None);
 
         action_state.update(input_map.which_pressed(&input_streams, ClashStrategy::PressAll));
 
@@ -651,7 +681,7 @@ mod tests {
     #[test]
     fn time_tick_ticks_away() {
         use crate::action_state::ActionState;
-        use bevy_utils::{Duration, Instant};
+        use bevy::utils::{Duration, Instant};
 
         let mut action_state = ActionState::<Action>::default();
 
@@ -672,11 +702,10 @@ mod tests {
         assert!(!action_state.just_pressed(Action::Jump));
     }
 
-    // FIXME: these tests are flaky because floats
     #[test]
     fn durations() {
         use crate::action_state::ActionState;
-        use bevy_utils::{Duration, Instant};
+        use bevy::utils::{Duration, Instant};
 
         let mut action_state = ActionState::<Action>::default();
 
diff --git a/src/axislike.rs b/src/axislike.rs
index d21b15e..c9607cf 100644
--- a/src/axislike.rs
+++ b/src/axislike.rs
@@ -1,98 +1,512 @@
-//! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equvalents)
-
-use crate::orientation::{Direction, Rotation};
-use bevy_math::Vec2;
-
-/// A high-level abstract user input that varies from -1 to 1, inclusive, along two axes
-///
-/// The neutral origin is always at 0, 0.
-/// When constructed; the magnitude is capped at 1, but direction is preserved.
-///
-/// 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, Clone, PartialEq)]
-pub struct AxisPair {
-    xy: Vec2,
-}
-
-// Constructors
-impl AxisPair {
-    /// Creates a new [`AxisPair`] from the provided (x,y) coordinates
-    ///
-    /// The direction is preserved, by the magnitude will be clamped to at most 1.
-    pub fn new(xy: Vec2) -> AxisPair {
-        let magnitude = xy.length();
-        if magnitude <= 1. {
-            AxisPair { xy }
-        } else {
-            AxisPair { xy: xy / magnitude }
-        }
-    }
-}
-
-// Methods
-impl AxisPair {
-    /// The value along the x-axis, ranging from -1 to 1
-    #[must_use]
-    #[inline]
-    pub fn x(&self) -> f32 {
-        self.xy.x
-    }
-
-    /// The value along the y-axis, ranging from -1 to 1
-    #[must_use]
-    #[inline]
-    pub fn y(&self) -> f32 {
-        self.xy.y
-    }
-
-    /// The (x, y) values, each ranging from -1 to 1
-    #[must_use]
-    #[inline]
-    pub fn xy(&self) -> Vec2 {
-        self.xy
-    }
-
-    /// The [`Direction`] that this axis is pointing towards, if any
-    ///
-    /// If the axis is neutral (x,y) = (0,0), a (0, 0) `Direction` will be returned
-    #[must_use]
-    #[inline]
-    pub fn direction(&self) -> Direction {
-        Direction::new(self.xy)
-    }
-
-    /// 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> {
-        match Rotation::from_xy(self.xy) {
-            Ok(rotation) => Some(rotation),
-            Err(_) => None,
-        }
-    }
-
-    /// How far from the origin is this axis's position?
-    ///
-    /// Always bounded between 0 and 1.
-    ///
-    /// If you only need to compare relative magnitudes, use `magnitude_squared` instead for faster computation.
-    #[must_use]
-    #[inline]
-    pub fn magnitude(&self) -> f32 {
-        self.xy.length()
-    }
-
-    /// The square of the axis' magnitude
-    ///
-    /// Always bounded between 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 magnitude_squared(&self) -> f32 {
-        self.xy.length_squared()
-    }
-}
+//! Tools for working with directional axis-like user inputs (gamesticks, D-Pads and emulated equvalents)
+
+use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection};
+use crate::orientation::{Direction, Rotation};
+use crate::user_input::InputKind;
+use bevy::input::{
+    gamepad::{GamepadAxisType, GamepadButtonType},
+    keyboard::KeyCode,
+};
+use bevy::math::Vec2;
+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)]
+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,
+    /// 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<f32>,
+}
+
+impl SingleAxis {
+    /// Creates a [`SingleAxis`] with both `positive_low` and `negative_low` set to `threshold`.
+    #[must_use]
+    pub fn symmetric(axis_type: impl Into<AxisType>, threshold: f32) -> SingleAxis {
+        SingleAxis {
+            axis_type: axis_type.into(),
+            positive_low: threshold,
+            negative_low: -threshold,
+            value: None,
+        }
+    }
+
+    /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`.
+    ///
+    /// All thresholds are set to 0.0.
+    /// Primarily useful for [input mocking](crate::MockInput).
+    #[must_use]
+    pub fn from_value(axis_type: impl Into<AxisType>, value: f32) -> SingleAxis {
+        SingleAxis {
+            axis_type: axis_type.into(),
+            positive_low: 0.0,
+            negative_low: 0.0,
+            value: Some(value),
+        }
+    }
+
+    /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseWheel`](bevy::input::mouse::MouseWheel) movement
+    #[must_use]
+    pub const fn mouse_wheel_x() -> SingleAxis {
+        SingleAxis {
+            axis_type: AxisType::MouseWheel(MouseWheelAxisType::X),
+            positive_low: 0.,
+            negative_low: 0.,
+            value: None,
+        }
+    }
+
+    /// Creates a [`SingleAxis`] corresponding to vertical [`MouseWheel`](bevy::input::mouse::MouseWheel) movement
+    #[must_use]
+    pub const fn mouse_wheel_y() -> SingleAxis {
+        SingleAxis {
+            axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
+            positive_low: 0.,
+            negative_low: 0.,
+            value: None,
+        }
+    }
+
+    /// Creates a [`SingleAxis`] corresponding to horizontal [`MouseMotion`](bevy::input::mouse::MouseMotion) movement
+    #[must_use]
+    pub const fn mouse_motion_x() -> SingleAxis {
+        SingleAxis {
+            axis_type: AxisType::MouseMotion(MouseMotionAxisType::X),
+            positive_low: 0.,
+            negative_low: 0.,
+            value: None,
+        }
+    }
+
+    /// Creates a [`SingleAxis`] corresponding to vertical [`MouseMotion`](bevy::input::mouse::MouseMotion) movement
+    #[must_use]
+    pub const fn mouse_motion_y() -> SingleAxis {
+        SingleAxis {
+            axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
+            positive_low: 0.,
+            negative_low: 0.,
+            value: None,
+        }
+    }
+}
+
+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)
+    }
+}
+impl Eq for SingleAxis {}
+impl std::hash::Hash for SingleAxis {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.axis_type.hash(state);
+        FloatOrd(self.positive_low).hash(state);
+        FloatOrd(self.negative_low).hash(state);
+    }
+}
+
+/// 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 [`AxisPair`] can be read with
+/// [`ActionState::action_axis_pair()`][crate::ActionState::action_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)]
+pub struct DualAxis {
+    /// The axis representing horizontal movement.
+    pub x: SingleAxis,
+    /// The axis representing vertical movement.
+    pub y: SingleAxis,
+}
+
+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;
+
+    /// Creates a [`DualAxis`] with both `positive_low` and `negative_low` in both axes set to `threshold`.
+    #[must_use]
+    pub fn symmetric(
+        x_axis_type: impl Into<AxisType>,
+        y_axis_type: impl Into<AxisType>,
+        threshold: f32,
+    ) -> DualAxis {
+        DualAxis {
+            x: SingleAxis::symmetric(x_axis_type, threshold),
+            y: SingleAxis::symmetric(y_axis_type, threshold),
+        }
+    }
+
+    /// Creates a [`SingleAxis`] with the specified `axis_type` and `value`.
+    ///
+    /// All thresholds are set to 0.0.
+    /// Primarily useful for [input mocking](crate::MockInput).
+    #[must_use]
+    pub fn from_value(
+        x_axis_type: impl Into<AxisType>,
+        y_axis_type: impl Into<AxisType>,
+        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),
+        }
+    }
+
+    /// Creates a [`DualAxis`] for the left analogue stick of the gamepad.
+    #[must_use]
+    pub fn left_stick() -> DualAxis {
+        DualAxis::symmetric(
+            GamepadAxisType::LeftStickX,
+            GamepadAxisType::LeftStickY,
+            Self::DEFAULT_DEADZONE,
+        )
+    }
+
+    /// Creates a [`DualAxis`] for the right analogue stick of the gamepad.
+    #[must_use]
+    pub fn right_stick() -> DualAxis {
+        DualAxis::symmetric(
+            GamepadAxisType::RightStickX,
+            GamepadAxisType::LeftStickY,
+            Self::DEFAULT_DEADZONE,
+        )
+    }
+
+    /// 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(),
+        }
+    }
+
+    /// 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(),
+        }
+    }
+}
+
+#[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an un-quoted item
+/// A virtual DPad that you can get an [`AxisPair`] 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)]
+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,
+}
+
+impl VirtualDPad {
+    /// Generates a [`VirtualDPad`] corresponding to the arrow keyboard keycodes
+    pub fn arrow_keys() -> VirtualDPad {
+        VirtualDPad {
+            up: InputKind::Keyboard(KeyCode::Up),
+            down: InputKind::Keyboard(KeyCode::Down),
+            left: InputKind::Keyboard(KeyCode::Left),
+            right: InputKind::Keyboard(KeyCode::Right),
+        }
+    }
+
+    /// Generates a [`VirtualDPad`] corresponding to the `WASD` keyboard keycodes
+    pub fn wasd() -> VirtualDPad {
+        VirtualDPad {
+            up: InputKind::Keyboard(KeyCode::W),
+            down: InputKind::Keyboard(KeyCode::S),
+            left: InputKind::Keyboard(KeyCode::A),
+            right: InputKind::Keyboard(KeyCode::D),
+        }
+    }
+
+    #[allow(clippy::doc_markdown)] // False alarm because it thinks DPad is an un-quoted 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),
+        }
+    }
+
+    /// Generates a [`VirtualDPad`] corresponding to the face buttons on a gamepad
+    ///
+    /// North corresponds to up, west corresponds to left, east corresponds to right, 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),
+        }
+    }
+
+    /// 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),
+        }
+    }
+
+    /// 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),
+        }
+    }
+}
+
+/// 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)]
+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 direction of motion of the mouse wheel.
+///
+/// Stored in the [`AxisType`] enum.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum MouseWheelAxisType {
+    /// Horizontal movement.
+    ///
+    /// This is much less common than the `Y` variant, and is only supported on some devices.
+    X,
+    /// Vertical movement.
+    ///
+    /// This is the standard behavior for a mouse wheel, used to scroll up and down pages.
+    Y,
+}
+
+/// The direction of motion of the mouse.
+///
+/// Stored in the [`AxisType`] enum.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum MouseMotionAxisType {
+    /// Horizontal movement.
+    X,
+    /// Vertical movement.
+    Y,
+}
+
+impl From<GamepadAxisType> for AxisType {
+    fn from(axis_type: GamepadAxisType) -> Self {
+        AxisType::Gamepad(axis_type)
+    }
+}
+
+impl From<MouseWheelAxisType> for AxisType {
+    fn from(axis_type: MouseWheelAxisType) -> Self {
+        AxisType::MouseWheel(axis_type)
+    }
+}
+
+impl From<MouseMotionAxisType> for AxisType {
+    fn from(axis_type: MouseMotionAxisType) -> Self {
+        AxisType::MouseMotion(axis_type)
+    }
+}
+
+impl TryFrom<AxisType> for GamepadAxisType {
+    type Error = AxisConversionError;
+
+    fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
+        match axis_type {
+            AxisType::Gamepad(inner) => Ok(inner),
+            _ => Err(AxisConversionError),
+        }
+    }
+}
+
+impl TryFrom<AxisType> for MouseWheelAxisType {
+    type Error = AxisConversionError;
+
+    fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
+        match axis_type {
+            AxisType::MouseWheel(inner) => Ok(inner),
+            _ => Err(AxisConversionError),
+        }
+    }
+}
+
+impl TryFrom<AxisType> for MouseMotionAxisType {
+    type Error = AxisConversionError;
+
+    fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
+        match axis_type {
+            AxisType::MouseMotion(inner) => Ok(inner),
+            _ => Err(AxisConversionError),
+        }
+    }
+}
+
+/// 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 combaination 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)]
+pub struct DualAxisData {
+    xy: Vec2,
+}
+
+// Constructors
+impl DualAxisData {
+    /// Creates a new [`AxisPair`] from the provided (x,y) coordinates
+    pub fn new(x: f32, y: f32) -> DualAxisData {
+        DualAxisData {
+            xy: Vec2::new(x, y),
+        }
+    }
+
+    /// Creates a new [`AxisPair`] directly from a [`Vec2`]
+    pub fn from_xy(xy: Vec2) -> DualAxisData {
+        DualAxisData { xy }
+    }
+
+    /// Merge the state of this [`AxisPair`] 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 [`AxisPair::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 [`Direction`] 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<Direction> {
+        // TODO: replace this quick-n-dirty hack once Direction::new no longer panics
+        if self.xy.length() > 0.00001 {
+            return Some(Direction::new(self.xy));
+        }
+        None
+    }
+
+    /// 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> {
+        match Rotation::from_xy(self.xy) {
+            Ok(rotation) => Some(rotation),
+            Err(_) => None,
+        }
+    }
+
+    /// 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<DualAxisData> for Vec2 {
+    fn from(data: DualAxisData) -> Vec2 {
+        data.xy
+    }
+}
diff --git a/src/buttonlike.rs b/src/buttonlike.rs
index 23eaa5f..e22173e 100644
--- a/src/buttonlike.rs
+++ b/src/buttonlike.rs
@@ -87,3 +87,33 @@ impl Default for ButtonState {
         ButtonState::Released
     }
 }
+
+/// 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)]
+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)]
+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 93c9af4..fa47a1f 100644
--- a/src/clashing_inputs.rs
+++ b/src/clashing_inputs.rs
@@ -1,8 +1,10 @@
 //! Handles clashing inputs into a [`InputMap`](crate::input_map::InputMap) in a configurable fashion.
 
 use crate::action_state::ActionData;
+use crate::axislike::VirtualDPad;
 use crate::input_map::InputMap;
-use crate::user_input::{InputButton, InputStreams, UserInput};
+use crate::input_streams::InputStreams;
+use crate::user_input::{InputKind, UserInput};
 use crate::Actionlike;
 
 use itertools::Itertools;
@@ -55,11 +57,18 @@ impl UserInput {
         match self {
             Single(self_button) => match other {
                 Single(_) => false,
-                Chord(other_set) => button_chord_clash(self_button, other_set),
+                Chord(other_chord) => button_chord_clash(self_button, other_chord),
+                VirtualDPad(other_dpad) => dpad_button_clash(other_dpad, self_button),
             },
-            Chord(self_set) => match other {
-                Single(other_button) => button_chord_clash(other_button, self_set),
-                Chord(other_set) => chord_chord_clash(self_set, other_set),
+            Chord(self_chord) => match other {
+                Single(other_button) => button_chord_clash(other_button, self_chord),
+                Chord(other_chord) => chord_chord_clash(self_chord, other_chord),
+                VirtualDPad(other_dpad) => dpad_chord_clash(other_dpad, self_chord),
+            },
+            VirtualDPad(self_dpad) => match other {
+                Single(other_button) => dpad_button_clash(self_dpad, other_button),
+                Chord(other_chord) => dpad_chord_clash(self_dpad, other_chord),
+                VirtualDPad(other_dpad) => dpad_dpad_clash(self_dpad, other_dpad),
             },
         }
     }
@@ -187,9 +196,9 @@ impl<A: Actionlike> Clash<A> {
     }
 }
 
-/// Does the `button` clash with the `chord`?
+// Does the `button` clash with the `chord`?
 #[must_use]
-fn button_chord_clash(button: &InputButton, chord: &PetitSet<InputButton, 8>) -> bool {
+fn button_chord_clash(button: &InputKind, chord: &PetitSet<InputKind, 8>) -> bool {
     if chord.len() <= 1 {
         return false;
     }
@@ -197,12 +206,47 @@ fn button_chord_clash(button: &InputButton, chord: &PetitSet<InputButton, 8>) ->
     chord.contains(button)
 }
 
+// Does the `dpad` clash with the `chord`?
+#[must_use]
+fn dpad_chord_clash(dpad: &VirtualDPad, chord: &PetitSet<InputKind, 8>) -> bool {
+    if chord.len() <= 1 {
+        return false;
+    }
+
+    for button in &[dpad.up, dpad.down, dpad.left, dpad.right] {
+        if chord.contains(button) {
+            return true;
+        }
+    }
+
+    false
+}
+
+fn dpad_button_clash(dpad: &VirtualDPad, button: &InputKind) -> bool {
+    for dpad_button in &[dpad.up, dpad.down, dpad.left, dpad.right] {
+        if button == dpad_button {
+            return true;
+        }
+    }
+
+    false
+}
+
+fn dpad_dpad_clash(dpad1: &VirtualDPad, dpad2: &VirtualDPad) -> bool {
+    for button1 in &[dpad1.up, dpad1.down, dpad1.left, dpad1.right] {
+        for button2 in &[dpad2.up, dpad2.down, dpad2.left, dpad2.right] {
+            if button1 == button2 {
+                return true;
+            }
+        }
+    }
+
+    false
+}
+
 /// Does the `chord_a` clash with `chord_b`?
 #[must_use]
-fn chord_chord_clash(
-    chord_a: &PetitSet<InputButton, 8>,
-    chord_b: &PetitSet<InputButton, 8>,
-) -> bool {
+fn chord_chord_clash(chord_a: &PetitSet<InputKind, 8>, chord_b: &PetitSet<InputKind, 8>) -> bool {
     if chord_a.len() <= 1 || chord_b.len() <= 1 {
         return false;
     }
@@ -279,6 +323,8 @@ fn resolve_clash<A: Actionlike>(
         }
     }
 
+    println!("real clash");
+
     // There's a real clash; resolve it according to the `clash_strategy`
     match clash_strategy {
         // Do nothing
@@ -315,8 +361,9 @@ fn resolve_clash<A: Actionlike>(
 mod tests {
     use super::*;
     use crate as leafwing_input_manager;
-    use crate::Actionlike;
-    use bevy_input::keyboard::KeyCode::*;
+    use bevy::app::App;
+    use bevy::input::keyboard::KeyCode::*;
+    use leafwing_input_manager_macros::Actionlike;
 
     #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug)]
     enum Action {
@@ -328,6 +375,8 @@ mod tests {
         CtrlOne,
         AltOne,
         CtrlAltOne,
+        MoveDPad,
+        CtrlUp,
     }
 
     fn test_input_map() -> InputMap<Action> {
@@ -343,11 +392,27 @@ mod tests {
         input_map.insert_chord([LControl, Key1], CtrlOne);
         input_map.insert_chord([LAlt, Key1], AltOne);
         input_map.insert_chord([LControl, LAlt, Key1], CtrlAltOne);
+        input_map.insert(
+            VirtualDPad {
+                up: Up.into(),
+                down: Down.into(),
+                left: Left.into(),
+                right: Right.into(),
+            },
+            MoveDPad,
+        );
+        input_map.insert_chord([LControl, Up], CtrlUp);
 
         input_map
     }
 
     mod basic_functionality {
+        use crate::axislike::VirtualDPad;
+        use crate::buttonlike::ButtonState;
+        use crate::input_mocking::MockInput;
+        use bevy::input::InputPlugin;
+        use Action::*;
+
         use super::*;
 
         #[test]
@@ -358,18 +423,44 @@ mod tests {
             let ab = UserInput::chord([A, B]);
             let bc = UserInput::chord([B, C]);
             let abc = UserInput::chord([A, B, C]);
+            let axyz_dpad: UserInput = VirtualDPad {
+                up: A.into(),
+                down: X.into(),
+                left: Y.into(),
+                right: Z.into(),
+            }
+            .into();
+            let abcd_dpad: UserInput = VirtualDPad {
+                up: A.into(),
+                down: B.into(),
+                left: C.into(),
+                right: D.into(),
+            }
+            .into();
+
+            let ctrl_up: UserInput = UserInput::chord([Up, LControl]);
+            let directions_dpad: UserInput = VirtualDPad {
+                up: Up.into(),
+                down: Down.into(),
+                left: Left.into(),
+                right: Right.into(),
+            }
+            .into();
 
             assert!(!a.clashes(&b));
             assert!(a.clashes(&ab));
             assert!(!c.clashes(&ab));
             assert!(!ab.clashes(&bc));
-            assert!(ab.clashes(&abc))
+            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));
         }
 
         #[test]
         fn button_chord_clash_construction() {
-            use Action::*;
-
             let input_map = test_input_map();
 
             let observed_clash = input_map.possible_clash(One, OneAndTwo).unwrap();
@@ -386,8 +477,6 @@ mod tests {
 
         #[test]
         fn chord_chord_clash_construction() {
-            use Action::*;
-
             let input_map = test_input_map();
 
             let observed_clash = input_map
@@ -406,8 +495,6 @@ mod tests {
 
         #[test]
         fn can_clash() {
-            use Action::*;
-
             let input_map = test_input_map();
 
             assert!(input_map.possible_clash(One, Two).is_none());
@@ -423,29 +510,29 @@ mod tests {
         fn clash_caching() {
             let mut input_map = test_input_map();
             // Possible clashes are cached upon initialization
-            assert_eq!(input_map.possible_clashes().len(), 12);
+            assert_eq!(input_map.possible_clashes().len(), 13);
 
             // Possible clashes are cached upon binding insertion
             input_map.insert(UserInput::chord([LControl, LAlt, Key1]), Action::Two);
-            assert_eq!(input_map.possible_clashes().len(), 15);
+            assert_eq!(input_map.possible_clashes().len(), 16);
 
             // Possible clashes are cached upon binding removal
             input_map.clear_action(Action::One);
-            assert_eq!(input_map.possible_clashes().len(), 9);
+            assert_eq!(input_map.possible_clashes().len(), 10);
         }
 
         #[test]
         fn resolve_prioritize_longest() {
-            use bevy::prelude::*;
-            use Action::*;
+            let mut app = App::new();
+            app.add_plugin(InputPlugin);
 
             let input_map = test_input_map();
             let simple_clash = input_map.possible_clash(One, OneAndTwo).unwrap();
-            let mut keyboard: Input<KeyCode> = Default::default();
-            keyboard.press(Key1);
-            keyboard.press(Key2);
+            app.send_input(Key1);
+            app.send_input(Key2);
+            app.update();
 
-            let input_streams = InputStreams::from_keyboard(&keyboard);
+            let input_streams = InputStreams::from_world(&app.world, None);
 
             assert_eq!(
                 resolve_clash(
@@ -469,9 +556,10 @@ mod tests {
             let chord_clash = input_map
                 .possible_clash(OneAndTwo, OneAndTwoAndThree)
                 .unwrap();
-            keyboard.press(Key3);
+            app.send_input(Key3);
+            app.update();
 
-            let input_streams = InputStreams::from_keyboard(&keyboard);
+            let input_streams = InputStreams::from_world(&app.world, None);
 
             assert_eq!(
                 resolve_clash(
@@ -485,17 +573,17 @@ mod tests {
 
         #[test]
         fn resolve_use_action_order() {
-            use bevy::prelude::*;
-            use Action::*;
+            let mut app = App::new();
+            app.add_plugin(InputPlugin);
 
             let input_map = test_input_map();
             let simple_clash = input_map.possible_clash(One, CtrlOne).unwrap();
             let reversed_clash = input_map.possible_clash(CtrlOne, One).unwrap();
-            let mut keyboard: Input<KeyCode> = Default::default();
-            keyboard.press(Key1);
-            keyboard.press(LControl);
+            app.send_input(Key1);
+            app.send_input(LControl);
+            app.update();
 
-            let input_streams = InputStreams::from_keyboard(&keyboard);
+            let input_streams = InputStreams::from_world(&app.world, None);
 
             assert_eq!(
                 resolve_clash(&simple_clash, ClashStrategy::UseActionOrder, &input_streams,),
@@ -514,15 +602,13 @@ mod tests {
 
         #[test]
         fn handle_clashes() {
-            use crate::buttonlike::ButtonState;
-            use bevy::prelude::*;
-            use Action::*;
-
+            let mut app = App::new();
+            app.add_plugin(InputPlugin);
             let input_map = test_input_map();
 
-            let mut keyboard: Input<KeyCode> = Default::default();
-            keyboard.press(Key1);
-            keyboard.press(Key2);
+            app.send_input(Key1);
+            app.send_input(Key2);
+            app.update();
 
             let mut action_data = vec![ActionData::default(); Action::N_VARIANTS];
             action_data[One.index()].state = ButtonState::JustPressed;
@@ -531,7 +617,7 @@ mod tests {
 
             input_map.handle_clashes(
                 &mut action_data,
-                &InputStreams::from_keyboard(&keyboard),
+                &InputStreams::from_world(&app.world, None),
                 ClashStrategy::PrioritizeLongest,
             );
 
@@ -541,20 +627,46 @@ mod tests {
             assert_eq!(action_data, expected);
         }
 
+        // Checks that a clash between a VirtualDPad and a chord choses the chord
         #[test]
-        fn which_pressed() {
-            use bevy::prelude::*;
-            use Action::*;
+        fn handle_clashes_dpad_chord() {
+            let mut app = App::new();
+            app.add_plugin(InputPlugin);
+            let input_map = test_input_map();
+
+            app.send_input(LControl);
+            app.send_input(Up);
+            app.update();
+
+            let mut action_data = vec![ActionData::default(); Action::N_VARIANTS];
+            action_data[MoveDPad.index()].state = ButtonState::JustPressed;
+            action_data[CtrlUp.index()].state = ButtonState::JustPressed;
 
+            input_map.handle_clashes(
+                &mut action_data,
+                &InputStreams::from_world(&app.world, None),
+                ClashStrategy::PrioritizeLongest,
+            );
+
+            let mut expected = vec![ActionData::default(); Action::N_VARIANTS];
+            expected[CtrlUp.index()].state = ButtonState::JustPressed;
+
+            assert_eq!(action_data, expected);
+        }
+
+        #[test]
+        fn which_pressed() {
+            let mut app = App::new();
+            app.add_plugin(InputPlugin);
             let input_map = test_input_map();
 
-            let mut keyboard: Input<KeyCode> = Default::default();
-            keyboard.press(Key1);
-            keyboard.press(Key2);
-            keyboard.press(LControl);
+            app.send_input(Key1);
+            app.send_input(Key2);
+            app.send_input(LControl);
+            app.update();
 
             let action_data = input_map.which_pressed(
-                &InputStreams::from_keyboard(&keyboard),
+                &InputStreams::from_world(&app.world, None),
                 ClashStrategy::PrioritizeLongest,
             );
 
diff --git a/src/display_impl.rs b/src/display_impl.rs
index 5a13554..b0e70f5 100644
--- a/src/display_impl.rs
+++ b/src/display_impl.rs
@@ -1,6 +1,7 @@
 //! Containment module for boring implmentations of the [`Display`] trait
 
-use crate::user_input::{InputButton, UserInput};
+use crate::axislike::VirtualDPad;
+use crate::user_input::{InputKind, UserInput};
 use std::fmt::Display;
 
 impl Display for UserInput {
@@ -17,16 +18,32 @@ impl Display for UserInput {
                 }
                 write!(f, "{string}")
             }
+            UserInput::VirtualDPad(VirtualDPad {
+                up,
+                down,
+                left,
+                right,
+            }) => {
+                write!(
+                    f,
+                    "VirtualDPad(up: {}, down: {}, left: {}, right: {})",
+                    up, down, left, right
+                )
+            }
         }
     }
 }
 
-impl Display for InputButton {
+impl Display for InputKind {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            InputButton::Gamepad(button) => write!(f, "{button:?}"),
-            InputButton::Mouse(button) => write!(f, "{button:?}"),
-            InputButton::Keyboard(button) => write!(f, "{button:?}"),
+            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:?}"),
+            InputKind::Keyboard(button) => write!(f, "{button:?}"),
         }
     }
 }
diff --git a/src/errors.rs b/src/errors.rs
index 62d2380..2e6b325 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -5,7 +5,7 @@ use derive_more::{Display, Error};
 /// The supplied vector-like struct was too close to zero to be converted into a rotation-like type
 ///
 /// This error is produced when attempting to convert into a rotation-like type
-/// such as a [`Rotation`] or [`Quat`](bevy_math::Quat) from a vector-like type
+/// such as a [`Rotation`] or [`Quat`](bevy::math::Quat) from a vector-like type
 /// such as a [`Vec2`].
 ///
 /// In almost all cases, the correct way to handle this error is to simply not change the rotation.
diff --git a/src/input_map.rs b/src/input_map.rs
index acd550c..67920c1 100644
--- a/src/input_map.rs
+++ b/src/input_map.rs
@@ -3,11 +3,12 @@
 use crate::action_state::ActionData;
 use crate::buttonlike::ButtonState;
 use crate::clashing_inputs::ClashStrategy;
-use crate::user_input::{InputButton, InputStreams, UserInput};
+use crate::input_streams::InputStreams;
+use crate::user_input::{InputKind, UserInput};
 use crate::Actionlike;
 
-use bevy_ecs::component::Component;
-use bevy_input::gamepad::Gamepad;
+use bevy::ecs::component::Component;
+use bevy::input::gamepad::Gamepad;
 
 use core::fmt::Debug;
 use petitset::PetitSet;
@@ -19,14 +20,11 @@ use std::marker::PhantomData;
 /// Multiple inputs can be mapped to the same action,
 /// and each input can be mapped to multiple actions.
 ///
-/// The provided input types must be one of [`GamepadButtonType`], [`KeyCode`] or [`MouseButton`].
+/// The provided input types must be able to be converted into a [`UserInput`].
 ///
 /// The maximum number of bindings (total) that can be stored for each action is 16.
 /// Insertions will silently fail if you have reached this cap.
 ///
-/// In addition, you can configure the per-mode cap for each [`InputMode`] using [`InputMap::new`] or [`InputMap::set_per_mode_cap`].
-/// This can be useful if your UI can only display one or two possible keybindings for each input mode.
-///
 /// By default, if two actions would be 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,
@@ -38,7 +36,7 @@ use std::marker::PhantomData;
 /// ```rust
 /// use bevy::prelude::*;
 /// use leafwing_input_manager::prelude::*;
-/// use leafwing_input_manager::user_input::InputButton;
+/// use leafwing_input_manager::user_input::InputKind;
 ///
 /// // You can Run!
 /// // But you can't Hide :(
@@ -51,7 +49,7 @@ use std::marker::PhantomData;
 /// // Construction
 /// let mut input_map = InputMap::new([
 ///    // Note that the type of your iterators must be homogenous;
-///    // you can use `InputButton` or `UserInput` if needed
+///    // you can use `InputKind` or `UserInput` if needed
 ///    // as unifiying types
 ///   (GamepadButtonType::South, Action::Run),
 ///   (GamepadButtonType::LeftTrigger, Action::Hide),
@@ -63,9 +61,13 @@ use std::marker::PhantomData;
 /// .insert(KeyCode::LShift, Action::Run)
 /// // Chords
 /// .insert_chord([KeyCode::LControl, KeyCode::R], Action::Run)
-/// .insert_chord([InputButton::Keyboard(KeyCode::H),
-///                InputButton::Gamepad(GamepadButtonType::South),
-///                InputButton::Mouse(MouseButton::Middle)],
+/// .insert_chord([InputKind::Keyboard(KeyCode::H),
+///                InputKind::GamepadButton(GamepadButtonType::South),
+///                InputKind::Mouse(MouseButton::Middle)],
+///            Action::Run)
+/// .insert_chord([InputKind::Keyboard(KeyCode::H),
+///                InputKind::GamepadButton(GamepadButtonType::South),
+///                InputKind::Mouse(MouseButton::Middle)],
 ///            Action::Hide);
 ///
 /// // Removal
@@ -94,7 +96,7 @@ impl<A: Actionlike> Default for InputMap<A> {
 
 // Constructors
 impl<A: Actionlike> InputMap<A> {
-    /// Creates a new [`InputMap`] from an iterator of `(action, user_input)` pairs
+    /// Creates a new [`InputMap`] from an iterator of `(user_input, action)` pairs
     ///
     /// To create an empty input map, use the [`Default::default`] method instead.
     ///
@@ -102,7 +104,7 @@ impl<A: Actionlike> InputMap<A> {
     /// ```rust
     /// use leafwing_input_manager::input_map::InputMap;
     /// use leafwing_input_manager::Actionlike;
-    /// use bevy_input::keyboard::KeyCode;
+    /// use bevy::input::keyboard::KeyCode;
     ///
     /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash)]
     /// enum Action {
@@ -139,7 +141,7 @@ impl<A: Actionlike> InputMap<A> {
     /// ```rust
     /// use leafwing_input_manager::prelude::*;
 
-    /// use bevy_input::keyboard::KeyCode;
+    /// use bevy::input::keyboard::KeyCode;
     ///
     /// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash)]
     /// enum Action {
@@ -159,7 +161,7 @@ impl<A: Actionlike> InputMap<A> {
 
 // Insertion
 impl<A: Actionlike> InputMap<A> {
-    /// Insert a mapping between `action` and `input`
+    /// Insert a mapping between `input` and `action`
     ///
     /// # Panics
     ///
@@ -172,7 +174,7 @@ impl<A: Actionlike> InputMap<A> {
         self
     }
 
-    /// Insert a mapping between `action` and `input` at the provided index
+    /// Insert a mapping between `input` and `action` at the provided index
     ///
     /// If a matching input already existed in the set, it will be moved to the supplied index. Any input that was previously there will be moved to the matching input’s original index.
     ///
@@ -187,7 +189,7 @@ impl<A: Actionlike> InputMap<A> {
         self
     }
 
-    /// Insert a mapping between `action` and the provided `inputs`
+    /// Insert a mapping between the provided `input_action_pairs`
     ///
     /// This method creates multiple distinct bindings.
     /// If you want to require multiple buttons to be pressed at once, use [`insert_chord`](Self::insert_chord).
@@ -198,16 +200,16 @@ impl<A: Actionlike> InputMap<A> {
     /// Panics if the map is full and any of `inputs` is not a duplicate.
     pub fn insert_multiple(
         &mut self,
-        inputs: impl IntoIterator<Item = (impl Into<UserInput>, A)>,
+        input_action_pairs: impl IntoIterator<Item = (impl Into<UserInput>, A)>,
     ) -> &mut Self {
-        for (action, input) in inputs {
+        for (action, input) in input_action_pairs {
             self.insert(action, input);
         }
 
         self
     }
 
-    /// Insert a mapping between `action` and the simultaneous combination of `buttons` provided
+    /// Insert a mapping between the simultaneous combination of `buttons` and the `action` provided
     ///
     /// Any iterator that can be converted into a [`Button`] can be supplied, but will be converted into a [`PetitSet`] for storage and use.
     /// Chords can also be added with the [insert](Self::insert) method, if the [`UserInput::Chord`] variant is constructed explicitly.
@@ -217,7 +219,7 @@ impl<A: Actionlike> InputMap<A> {
     /// Panics if the map is full and `buttons` is not a duplicate.
     pub fn insert_chord(
         &mut self,
-        buttons: impl IntoIterator<Item = impl Into<InputButton>>,
+        buttons: impl IntoIterator<Item = impl Into<InputKind>>,
         action: A,
     ) -> &mut Self {
         self.insert(UserInput::chord(buttons), action);
@@ -260,12 +262,21 @@ impl<A: Actionlike> InputMap<A> {
 // Configuration
 impl<A: Actionlike> InputMap<A> {
     /// Fetches the [Gamepad] associated with the entity controlled by this entity map
+    ///
+    /// If this is [`None`], input from any connected gamepad will be used.
     #[must_use]
     pub fn gamepad(&self) -> Option<Gamepad> {
         self.associated_gamepad
     }
 
     /// Assigns a particular [`Gamepad`] to the entity controlled by this input map
+    ///
+    /// 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.
     pub fn set_gamepad(&mut self, gamepad: Gamepad) -> &mut Self {
         self.associated_gamepad = Some(gamepad);
         self
@@ -312,11 +323,22 @@ impl<A: Actionlike> InputMap<A> {
             let mut inputs = Vec::new();
 
             for input in self.get(action.clone()).iter() {
+                let action = &mut action_data[action.index()];
+
+                // Merge axis pair into action data
+                let axis_pair = input_streams.input_axis_pair(input);
+                if let Some(axis_pair) = axis_pair {
+                    if let Some(current_axis_pair) = &mut action.axis_pair {
+                        *current_axis_pair = current_axis_pair.merged_with(axis_pair);
+                    } else {
+                        action.axis_pair = Some(axis_pair);
+                    }
+                }
+
                 if input_streams.input_pressed(input) {
                     inputs.push(input.clone());
-                    action_data[action.index()]
-                        .reasons_pressed
-                        .push(input.clone());
+
+                    action.value += input_streams.input_value(input);
                 }
             }
 
@@ -406,7 +428,7 @@ mod tests {
 
     #[test]
     fn insertion_idempotency() {
-        use bevy_input::keyboard::KeyCode;
+        use bevy::input::keyboard::KeyCode;
         use petitset::PetitSet;
 
         let mut input_map = InputMap::<Action>::default();
@@ -428,7 +450,7 @@ mod tests {
     #[test]
     fn multiple_insertion() {
         use crate::user_input::UserInput;
-        use bevy_input::keyboard::KeyCode;
+        use bevy::input::keyboard::KeyCode;
         use petitset::PetitSet;
 
         let mut input_map_1 = InputMap::<Action>::default();
@@ -451,7 +473,7 @@ mod tests {
     #[test]
     fn chord_singleton_coercion() {
         use crate::input_map::UserInput;
-        use bevy_input::keyboard::KeyCode;
+        use bevy::input::keyboard::KeyCode;
 
         // Single items in a chord should be coerced to a singleton
         let mut input_map_1 = InputMap::<Action>::default();
@@ -465,7 +487,7 @@ mod tests {
 
     #[test]
     fn input_clearing() {
-        use bevy_input::keyboard::KeyCode;
+        use bevy::input::keyboard::KeyCode;
 
         let mut input_map = InputMap::<Action>::default();
         input_map.insert(KeyCode::Space, Action::Run);
@@ -491,7 +513,7 @@ mod tests {
 
     #[test]
     fn merging() {
-        use bevy_input::{gamepad::GamepadButtonType, keyboard::KeyCode};
+        use bevy::input::{gamepad::GamepadButtonType, keyboard::KeyCode};
 
         let mut input_map = InputMap::default();
         let mut default_keyboard_map = InputMap::default();
@@ -512,164 +534,15 @@ mod tests {
 
     #[test]
     fn gamepad_swapping() {
-        use bevy_input::gamepad::Gamepad;
+        use bevy::input::gamepad::Gamepad;
 
         let mut input_map = InputMap::<Action>::default();
         assert_eq!(input_map.gamepad(), None);
 
-        input_map.set_gamepad(Gamepad(0));
-        assert_eq!(input_map.gamepad(), Some(Gamepad(0)));
+        input_map.set_gamepad(Gamepad { id: 0 });
+        assert_eq!(input_map.gamepad(), Some(Gamepad { id: 0 }));
 
         input_map.clear_gamepad();
         assert_eq!(input_map.gamepad(), None);
     }
-
-    #[test]
-    fn mock_inputs() {
-        use crate::input_map::InputButton;
-        use crate::user_input::InputStreams;
-        use bevy::prelude::*;
-
-        // Setting up the input map
-        let mut input_map = InputMap::<Action>::default();
-        input_map.set_gamepad(Gamepad(42));
-
-        // Gamepad
-        input_map.insert(GamepadButtonType::South, Action::Run);
-        input_map.insert_chord(
-            [GamepadButtonType::North, GamepadButtonType::South],
-            Action::Jump,
-        );
-
-        // Keyboard
-        input_map.insert(KeyCode::LShift, Action::Run);
-        input_map.insert(KeyCode::LShift, Action::Hide);
-
-        // Mouse
-        input_map.insert(MouseButton::Left, Action::Run);
-        input_map.insert(MouseButton::Other(42), Action::Jump);
-
-        // Cross-device chords
-        input_map.insert_chord(
-            [
-                InputButton::Keyboard(KeyCode::LControl),
-                InputButton::Mouse(MouseButton::Left),
-            ],
-            Action::Hide,
-        );
-
-        // Input streams
-        let mut gamepad_input_stream = Input::<GamepadButton>::default();
-        let mut keyboard_input_stream = Input::<KeyCode>::default();
-        let mut mouse_input_stream = Input::<MouseButton>::default();
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        // With no inputs, nothing should be detected
-        for action in Action::variants() {
-            assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll));
-        }
-
-        // Pressing the wrong gamepad
-        gamepad_input_stream.press(GamepadButton(Gamepad(0), GamepadButtonType::South));
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-        for action in Action::variants() {
-            assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll));
-        }
-
-        // Pressing the correct gamepad
-        gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::South));
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll));
-        assert!(!input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll));
-
-        // Chord
-        gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::South));
-        gamepad_input_stream.press(GamepadButton(Gamepad(42), GamepadButtonType::North));
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll));
-        assert!(input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll));
-
-        // Clearing inputs
-        gamepad_input_stream = Input::<GamepadButton>::default();
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        for action in Action::variants() {
-            assert!(!input_map.pressed(action, &input_streams, ClashStrategy::PressAll));
-        }
-
-        // Keyboard
-        keyboard_input_stream.press(KeyCode::LShift);
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll));
-        assert!(input_map.pressed(Action::Hide, &input_streams, ClashStrategy::PressAll));
-
-        keyboard_input_stream = Input::<KeyCode>::default();
-
-        // Mouse
-        mouse_input_stream.press(MouseButton::Left);
-        mouse_input_stream.press(MouseButton::Other(42));
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        assert!(input_map.pressed(Action::Run, &input_streams, ClashStrategy::PressAll));
-        assert!(input_map.pressed(Action::Jump, &input_streams, ClashStrategy::PressAll));
-
-        mouse_input_stream = Input::<MouseButton>::default();
-
-        // Cross-device chording
-        keyboard_input_stream.press(KeyCode::LControl);
-        mouse_input_stream.press(MouseButton::Left);
-
-        let input_streams = InputStreams {
-            gamepad: Some(&gamepad_input_stream),
-            keyboard: Some(&keyboard_input_stream),
-            mouse: Some(&mouse_input_stream),
-            associated_gamepad: Some(Gamepad(42)),
-        };
-
-        assert!(input_map.pressed(Action::Hide, &input_streams, ClashStrategy::PressAll));
-    }
 }
diff --git a/src/input_mocking.rs b/src/input_mocking.rs
index 2d79d32..54ebd8f 100644
--- a/src/input_mocking.rs
+++ b/src/input_mocking.rs
@@ -1,22 +1,37 @@
 //! Helpful utilities for testing input management by sending mock input events
-
-use crate::user_input::{InputStreams, MutableInputStreams, UserInput};
-use bevy_app::App;
-use bevy_ecs::event::Events;
-use bevy_ecs::system::{Res, ResMut, SystemState};
-use bevy_ecs::world::World;
+//!
+//! 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.
+
+use crate::axislike::{AxisType, MouseMotionAxisType, MouseWheelAxisType};
+use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection};
+use crate::input_streams::{InputStreams, MutableInputStreams};
+use crate::user_input::UserInput;
+
+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::{Gamepad, GamepadButton, GamepadEvent, Gamepads},
+use bevy::ecs::{component::Component, query::With, system::Query};
+use bevy::input::gamepad::GamepadEventRaw;
+use bevy::input::mouse::MouseScrollUnit;
+use bevy::input::ButtonState;
+use bevy::input::{
+    gamepad::{Gamepad, GamepadButton, GamepadEvent, GamepadEventType},
     keyboard::{KeyCode, KeyboardInput},
-    mouse::{MouseButton, MouseButtonInput, MouseWheel},
+    mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel},
     touch::{TouchInput, Touches},
     Input,
 };
+use bevy::math::Vec2;
 #[cfg(feature = "ui")]
-use bevy_ui::Interaction;
-use bevy_window::CursorMoved;
+use bevy::ui::Interaction;
+use bevy::window::CursorMoved;
 
 /// Send fake input events for testing purposes
 ///
@@ -26,19 +41,25 @@ use bevy_window::CursorMoved;
 /// # Examples
 /// ```rust
 /// use bevy::prelude::*;
-/// use leafwing_input_manager::MockInput;
+/// use bevy::input::InputPlugin;
+/// use leafwing_input_manager::input_mocking::MockInput;
 ///
-/// let mut world = World::new();
+/// // Remember to add InputPlugin so the resources will be there!
+/// let mut app = App::new();
+/// app.add_plugin(InputPlugin);
 ///
 /// // Pay respects!
-/// world.send_input(KeyCode::F);
+/// app.send_input(KeyCode::F);
+/// app.update();
 /// ```
 ///
 /// ```rust
 /// use bevy::prelude::*;
-/// use leafwing_input_manager::{MockInput, user_input::UserInput};
+/// use bevy::input::InputPlugin;
+/// use leafwing_input_manager::{input_mocking::MockInput, user_input::UserInput};
 ///
 /// let mut app = App::new();
+/// app.add_plugin(InputPlugin);
 ///
 /// // Send inputs one at a time
 /// let B_E_V_Y = [KeyCode::B, KeyCode::E, KeyCode::V, KeyCode::Y];
@@ -49,22 +70,32 @@ use bevy_window::CursorMoved;
 ///
 /// // Or use chords!
 /// app.send_input(UserInput::chord(B_E_V_Y));
+/// app.update();
 /// ```
 pub trait MockInput {
     /// Send the specified `user_input` directly
     ///
+    /// These are sent as the raw input events, and do not set the value of [`Input`] or [`Axis`] directly.
     /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called.
     ///
+    /// To send specific values for axislike inputs, set their `value` field.
+    ///
     /// Gamepad input will be sent by the first registed controller found.
     /// If none are found, gamepad input will be silently skipped.
+    ///
+    /// # Warning
+    ///
+    /// 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 [`Input`] and [`Axis`] data.
     fn send_input(&mut self, input: impl Into<UserInput>);
 
     /// Send the specified `user_input` directly, using the specified gamepad
     ///
     /// Note that inputs will continue to be pressed until explicitly released or [`MockInput::reset_inputs`] is called.
     ///
-    /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating inputs from
-    fn send_input_to_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>);
+    /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating.
+    fn send_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>);
 
     /// Releases the specified `user_input` directly
     ///
@@ -74,24 +105,20 @@ pub trait MockInput {
 
     /// Releases the specified `user_input` directly, using the specified gamepad
     ///
-    /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating inputs from
-    fn release_input_for_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>);
+    /// Provide the [`Gamepad`] identifier to control which gamepad you are emulating.
+    fn release_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>);
 
     /// Is the provided `user_input` pressed?
     ///
     /// This method is intended as a convenience for testing; check the [`Input`] resource directly,
     /// or use an [`InputMap`](crate::input_map::InputMap) in real code.
-    fn pressed(&mut self, input: impl Into<UserInput>) -> bool;
+    fn pressed(&self, input: impl Into<UserInput>) -> bool;
 
     /// Is the provided `user_input` pressed for the provided [`Gamepad`]?
     ///
     /// This method is intended as a convenience for testing; check the [`Input`] resource directly,
     /// or use an [`InputMap`](crate::input_map::InputMap) in real code.
-    fn pressed_for_gamepad(
-        &mut self,
-        input: impl Into<UserInput>,
-        gamepad: Option<Gamepad>,
-    ) -> bool;
+    fn pressed_for_gamepad(&self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) -> bool;
 
     /// Clears all user input streams, resetting them to their default state
     ///
@@ -102,171 +129,247 @@ pub trait MockInput {
     /// as well as any [`Interaction`] components and all input [`Events`].
     fn reset_inputs(&mut self);
 
-    /// Presses all `bevy_ui` buttons with the matching `Marker` component
+    /// Presses all `bevy::ui` buttons with the matching `Marker` component
     ///
     /// Changes their [`Interaction`] component to [`Interaction::Clicked`]
     #[cfg(feature = "ui")]
     fn click_button<Marker: Component>(&mut self);
 
-    /// Hovers over all `bevy_ui` buttons with the matching `Marker` component
+    /// Hovers over all `bevy::ui` buttons with the matching `Marker` component
     ///
     /// Changes their [`Interaction`] component to [`Interaction::Clicked`]
     #[cfg(feature = "ui")]
     fn hover_button<Marker: Component>(&mut self);
 }
 
-impl<'a> MutableInputStreams<'a> {
-    /// Send the specified `user_input` directly, using the specified gamepad
-    ///
-    /// Called by the methods of [`MockInput`].
-    pub fn send_user_input(&mut self, input: impl Into<UserInput>) {
+impl MockInput for MutableInputStreams<'_> {
+    fn send_input(&mut self, input: impl Into<UserInput>) {
+        self.send_input_as_gamepad(input, self.guess_gamepad());
+    }
+
+    fn send_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
         let input_to_send: UserInput = input.into();
-        let (gamepad_buttons, keyboard_buttons, mouse_buttons) = input_to_send.raw_inputs();
+        // Extract the raw inputs
+        let raw_inputs = input_to_send.raw_inputs();
+
+        // Keyboard buttons
+        for button in raw_inputs.keycodes {
+            self.keyboard_events.send(KeyboardInput {
+                scan_code: u32::MAX,
+                key_code: Some(button),
+                state: ButtonState::Pressed,
+            });
+        }
 
-        if let Some(ref mut gamepad_input) = self.gamepad {
-            for button in gamepad_buttons {
-                if let Some(associated_gamepad) = self.associated_gamepad {
-                    let gamepad_button = GamepadButton(associated_gamepad, button);
-                    gamepad_input.press(gamepad_button);
-                }
-            }
+        // Mouse buttons
+        for button in raw_inputs.mouse_buttons {
+            self.mouse_button_events.send(MouseButtonInput {
+                button,
+                state: ButtonState::Pressed,
+            });
         }
 
-        if let Some(ref mut keyboard_input) = self.keyboard {
-            for button in keyboard_buttons {
-                keyboard_input.press(button);
+        // Discrete mouse wheel events
+        for mouse_wheel_direction in raw_inputs.mouse_wheel {
+            match mouse_wheel_direction {
+                MouseWheelDirection::Left => self.mouse_wheel.send(MouseWheel {
+                    unit: MouseScrollUnit::Pixel,
+                    x: -1.0,
+                    y: 0.0,
+                }),
+                MouseWheelDirection::Right => self.mouse_wheel.send(MouseWheel {
+                    unit: MouseScrollUnit::Pixel,
+                    x: 1.0,
+                    y: 0.0,
+                }),
+                MouseWheelDirection::Up => self.mouse_wheel.send(MouseWheel {
+                    unit: MouseScrollUnit::Pixel,
+                    x: 0.0,
+                    y: 1.0,
+                }),
+                MouseWheelDirection::Down => self.mouse_wheel.send(MouseWheel {
+                    unit: MouseScrollUnit::Pixel,
+                    x: 0.0,
+                    y: -1.0,
+                }),
             }
         }
 
-        if let Some(ref mut mouse_input) = self.mouse {
-            for button in mouse_buttons {
-                mouse_input.press(button);
+        // Discrete mouse motion event
+        for mouse_motion_direction in raw_inputs.mouse_motion {
+            match mouse_motion_direction {
+                MouseMotionDirection::Up => self.mouse_motion.send(MouseMotion {
+                    delta: Vec2 { x: 0.0, y: 1.0 },
+                }),
+                MouseMotionDirection::Down => self.mouse_motion.send(MouseMotion {
+                    delta: Vec2 { x: 0.0, y: -1.0 },
+                }),
+                MouseMotionDirection::Right => self.mouse_motion.send(MouseMotion {
+                    delta: Vec2 { x: 1.0, y: 0.0 },
+                }),
+                MouseMotionDirection::Left => self.mouse_motion.send(MouseMotion {
+                    delta: Vec2 { x: -1.0, y: 0.0 },
+                }),
             }
         }
-    }
-
-    /// Releases the specified `user_input` directly, using the specified gamepad
-    ///
-    /// Called by the methods of [`MockInput`].
-    pub fn release_user_input(&mut self, input: impl Into<UserInput>) {
-        let input_to_release: UserInput = input.into();
-        let (gamepad_buttons, keyboard_buttons, mouse_buttons) = input_to_release.raw_inputs();
 
-        if let Some(ref mut gamepad_input) = self.gamepad {
-            for button in gamepad_buttons {
-                if let Some(associated_gamepad) = self.associated_gamepad {
-                    let gamepad_button = GamepadButton(associated_gamepad, button);
-                    gamepad_input.release(gamepad_button);
-                }
+        // Gamepad buttons
+        for button_type in raw_inputs.gamepad_buttons {
+            if let Some(gamepad) = gamepad {
+                self.gamepad_events.send(GamepadEventRaw {
+                    gamepad,
+                    event_type: GamepadEventType::ButtonChanged(button_type, 1.0),
+                });
             }
         }
 
-        if let Some(ref mut keyboard_input) = self.keyboard {
-            for button in keyboard_buttons {
-                keyboard_input.release(button);
+        // Axis data
+        for (outer_axis_type, maybe_position_data) in raw_inputs.axis_data {
+            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(GamepadEventRaw {
+                                gamepad,
+                                event_type: GamepadEventType::AxisChanged(axis_type, position_data),
+                            });
+                        }
+                    }
+                    AxisType::MouseWheel(axis_type) => {
+                        match axis_type {
+                            // FIXME: MouseScrollUnit is not recorded and is always assumed to be Pixel
+                            MouseWheelAxisType::X => self.mouse_wheel.send(MouseWheel {
+                                unit: MouseScrollUnit::Pixel,
+                                x: position_data,
+                                y: 0.0,
+                            }),
+                            MouseWheelAxisType::Y => self.mouse_wheel.send(MouseWheel {
+                                unit: MouseScrollUnit::Pixel,
+                                x: 0.0,
+                                y: position_data,
+                            }),
+                        }
+                    }
+                    AxisType::MouseMotion(axis_type) => match axis_type {
+                        MouseMotionAxisType::X => self.mouse_motion.send(MouseMotion {
+                            delta: Vec2 {
+                                x: position_data,
+                                y: 0.0,
+                            },
+                        }),
+                        MouseMotionAxisType::Y => self.mouse_motion.send(MouseMotion {
+                            delta: Vec2 {
+                                x: 0.0,
+                                y: position_data,
+                            },
+                        }),
+                    },
+                }
             }
         }
+    }
+
+    fn release_input(&mut self, input: impl Into<UserInput>) {
+        self.release_input_as_gamepad(input, self.guess_gamepad())
+    }
+
+    fn release_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
+        // Releasing axis-like inputs deliberately has no effect; it's unclear what this would do
 
-        if let Some(ref mut mouse_input) = self.mouse {
-            for button in mouse_buttons {
-                mouse_input.release(button);
+        let input_to_release: UserInput = input.into();
+        let raw_inputs = input_to_release.raw_inputs();
+
+        for button_type in raw_inputs.gamepad_buttons {
+            if let Some(gamepad) = gamepad {
+                self.gamepad_events.send(GamepadEventRaw {
+                    gamepad,
+                    event_type: GamepadEventType::ButtonChanged(button_type, 1.0),
+                });
             }
         }
-    }
-}
 
-impl MockInput for World {
-    fn send_input(&mut self, input: impl Into<UserInput>) {
-        let gamepad = if let Some(gamepads) = self.get_resource::<Gamepads>() {
-            gamepads.iter().next().copied()
-        } else {
-            None
-        };
+        for button in raw_inputs.keycodes {
+            self.keyboard_events.send(KeyboardInput {
+                scan_code: u32::MAX,
+                key_code: Some(button),
+                state: ButtonState::Released,
+            });
+        }
 
-        self.send_input_to_gamepad(input, gamepad);
+        for button in raw_inputs.mouse_buttons {
+            self.mouse_button_events.send(MouseButtonInput {
+                button,
+                state: ButtonState::Released,
+            });
+        }
     }
 
-    fn send_input_to_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
-        // You can make a system with this type signature if you'd like to mock user input
-        // in a non-exclusive system
-        let mut input_system_state: SystemState<(
-            Option<ResMut<Input<GamepadButton>>>,
-            Option<ResMut<Input<KeyCode>>>,
-            Option<ResMut<Input<MouseButton>>>,
-        )> = SystemState::new(self);
+    fn pressed(&self, input: impl Into<UserInput>) -> bool {
+        let input_streams: InputStreams = self.into();
+        input_streams.input_pressed(&input.into())
+    }
 
-        let (mut maybe_gamepad, mut maybe_keyboard, mut maybe_mouse) =
-            input_system_state.get_mut(self);
+    fn pressed_for_gamepad(&self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) -> bool {
+        let mut input_streams: InputStreams = self.into();
+        input_streams.associated_gamepad = gamepad;
 
-        let mut mutable_input_streams = MutableInputStreams {
-            gamepad: maybe_gamepad.as_deref_mut(),
-            keyboard: maybe_keyboard.as_deref_mut(),
-            mouse: maybe_mouse.as_deref_mut(),
-            associated_gamepad: gamepad,
-        };
+        input_streams.input_pressed(&input.into())
+    }
 
-        mutable_input_streams.send_user_input(input);
+    fn reset_inputs(&mut self) {
+        // WARNING: this *must* be updated when MutableInputStreams's fields change
+        // Note that we deliberately are not resetting either Gamepads or associated_gamepad
+        // as they are not actually input data
+        *self.gamepad_buttons = Default::default();
+        *self.gamepad_axes = Default::default();
+        *self.keycode = Default::default();
+        *self.mouse_button = Default::default();
+        *self.mouse_wheel = Default::default();
+        *self.mouse_motion = Default::default();
     }
 
-    fn release_input(&mut self, input: impl Into<UserInput>) {
-        let gamepad = if let Some(gamepads) = self.get_resource::<Gamepads>() {
-            gamepads.iter().next().copied()
-        } else {
-            None
-        };
+    #[cfg(feature = "ui")]
+    fn click_button<Marker: Component>(&mut self) {
+        panic!("Cannot use bevy_ui input mocking from `MutableInputStreams`, use an `App` or `World` instead.")
+    }
 
-        self.release_input_for_gamepad(input, gamepad);
+    #[cfg(feature = "ui")]
+    fn hover_button<Marker: Component>(&mut self) {
+        panic!("Cannot use bevy_ui input mocking from `MutableInputStreams`, use an `App` or `World` instead.")
     }
+}
 
-    fn release_input_for_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
-        let mut input_system_state: SystemState<(
-            Option<ResMut<Input<GamepadButton>>>,
-            Option<ResMut<Input<KeyCode>>>,
-            Option<ResMut<Input<MouseButton>>>,
-        )> = SystemState::new(self);
+impl MockInput for World {
+    fn send_input(&mut self, input: impl Into<UserInput>) {
+        let mut mutable_input_streams = MutableInputStreams::from_world(self, None);
 
-        let (mut maybe_gamepad, mut maybe_keyboard, mut maybe_mouse) =
-            input_system_state.get_mut(self);
+        mutable_input_streams.send_input(input);
+    }
 
-        let mut mutable_input_streams = MutableInputStreams {
-            gamepad: maybe_gamepad.as_deref_mut(),
-            keyboard: maybe_keyboard.as_deref_mut(),
-            mouse: maybe_mouse.as_deref_mut(),
-            associated_gamepad: gamepad,
-        };
+    fn send_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
+        let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad);
 
-        mutable_input_streams.release_user_input(input);
+        mutable_input_streams.send_input_as_gamepad(input, gamepad);
     }
 
-    fn pressed(&mut self, input: impl Into<UserInput>) -> bool {
-        let gamepad = if let Some(gamepads) = self.get_resource::<Gamepads>() {
-            gamepads.iter().next().copied()
-        } else {
-            None
-        };
+    fn release_input(&mut self, input: impl Into<UserInput>) {
+        let mut mutable_input_streams = MutableInputStreams::from_world(self, None);
 
-        self.pressed_for_gamepad(input, gamepad)
+        mutable_input_streams.release_input(input);
     }
 
-    fn pressed_for_gamepad(
-        &mut self,
-        input: impl Into<UserInput>,
-        gamepad: Option<Gamepad>,
-    ) -> bool {
-        let mut input_system_state: SystemState<(
-            Option<Res<Input<GamepadButton>>>,
-            Option<Res<Input<KeyCode>>>,
-            Option<Res<Input<MouseButton>>>,
-        )> = SystemState::new(self);
+    fn release_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
+        let mut mutable_input_streams = MutableInputStreams::from_world(self, gamepad);
+
+        mutable_input_streams.release_input_as_gamepad(input, gamepad);
+    }
 
-        let (maybe_gamepad, maybe_keyboard, maybe_mouse) = input_system_state.get(self);
+    fn pressed(&self, input: impl Into<UserInput>) -> bool {
+        self.pressed_for_gamepad(input, None)
+    }
 
-        let input_streams = InputStreams {
-            gamepad: maybe_gamepad.as_deref(),
-            keyboard: maybe_keyboard.as_deref(),
-            mouse: maybe_mouse.as_deref(),
-            associated_gamepad: gamepad,
-        };
+    fn pressed_for_gamepad(&self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) -> bool {
+        let input_streams = InputStreams::from_world(self, gamepad);
 
         input_streams.input_pressed(&input.into())
     }
@@ -339,27 +442,23 @@ impl MockInput for App {
         self.world.send_input(input);
     }
 
-    fn send_input_to_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
-        self.world.send_input_to_gamepad(input, gamepad);
+    fn send_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
+        self.world.send_input_as_gamepad(input, gamepad);
     }
 
     fn release_input(&mut self, input: impl Into<UserInput>) {
         self.world.release_input(input);
     }
 
-    fn release_input_for_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
-        self.world.release_input_for_gamepad(input, gamepad);
+    fn release_input_as_gamepad(&mut self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) {
+        self.world.release_input_as_gamepad(input, gamepad);
     }
 
-    fn pressed(&mut self, input: impl Into<UserInput>) -> bool {
+    fn pressed(&self, input: impl Into<UserInput>) -> bool {
         self.world.pressed(input)
     }
 
-    fn pressed_for_gamepad(
-        &mut self,
-        input: impl Into<UserInput>,
-        gamepad: Option<Gamepad>,
-    ) -> bool {
+    fn pressed_for_gamepad(&self, input: impl Into<UserInput>, gamepad: Option<Gamepad>) -> bool {
         self.world.pressed_for_gamepad(input, gamepad)
     }
 
@@ -380,69 +479,133 @@ impl MockInput for App {
 
 #[cfg(test)]
 mod test {
+    use crate::input_mocking::MockInput;
+    use bevy::{input::InputPlugin, prelude::*};
 
     #[test]
-    fn button_inputs() {
-        use crate::input_mocking::MockInput;
-        use bevy::prelude::*;
-
-        let mut world = World::new();
-        world.insert_resource(Input::<KeyCode>::default());
-        world.insert_resource(Input::<MouseButton>::default());
-        world.insert_resource(Input::<GamepadButton>::default());
-
-        // BLOCKED: cannot use the less artifical APIs due to
-        // https://github.com/bevyengine/bevy/issues/3808
-        let gamepad = Some(Gamepad(0));
+    fn ordinary_button_inputs() {
+        let mut app = App::new();
+        app.add_plugin(InputPlugin);
 
         // Test that buttons are unpressed by default
-        assert!(!world.pressed(KeyCode::Space));
-        assert!(!world.pressed(MouseButton::Right));
-        assert!(!world.pressed_for_gamepad(GamepadButtonType::North, gamepad));
+        assert!(!app.pressed(KeyCode::Space));
+        assert!(!app.pressed(MouseButton::Right));
 
         // Send inputs
-        world.send_input(KeyCode::Space);
-        world.send_input(MouseButton::Right);
-        world.send_input_to_gamepad(GamepadButtonType::North, gamepad);
+        app.send_input(KeyCode::Space);
+        app.send_input(MouseButton::Right);
+        app.update();
 
         // Verify that checking the resource value directly works
-        let keyboard_input: &Input<KeyCode> = world.resource();
+        let keyboard_input: &Input<KeyCode> = app.world.resource();
         assert!(keyboard_input.pressed(KeyCode::Space));
 
         // Test the convenient .pressed API
-        assert!(world.pressed(KeyCode::Space));
-        assert!(world.pressed(MouseButton::Right));
-        assert!(world.pressed_for_gamepad(GamepadButtonType::North, gamepad));
+        assert!(app.pressed(KeyCode::Space));
+        assert!(app.pressed(MouseButton::Right));
 
         // Test that resetting inputs works
-        world.reset_inputs();
+        app.reset_inputs();
+        app.update();
 
-        assert!(!world.pressed(KeyCode::Space));
-        assert!(!world.pressed(MouseButton::Right));
-        assert!(!world.pressed_for_gamepad(GamepadButtonType::North, gamepad));
+        assert!(!app.pressed(KeyCode::Space));
+        assert!(!app.pressed(MouseButton::Right));
+    }
+
+    #[test]
+    fn explicit_gamepad_button_inputs() {
+        let mut app = App::new();
+        app.add_plugin(InputPlugin);
+
+        let gamepad = Gamepad { id: 0 };
+        let mut gamepad_events = app.world.resource_mut::<Events<GamepadEvent>>();
+        gamepad_events.send(GamepadEvent {
+            gamepad,
+            event_type: GamepadEventType::Connected,
+        });
+        app.update();
+
+        // Test that buttons are unpressed by default
+        assert!(!app.pressed_for_gamepad(GamepadButtonType::North, Some(gamepad)));
+
+        // Send inputs
+        app.send_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::<Input<GamepadButton>>();
+        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)));
+    }
+
+    #[test]
+    fn implicit_gamepad_button_inputs() {
+        let mut app = App::new();
+        app.add_plugin(InputPlugin);
+
+        let gamepad = Gamepad { id: 0 };
+        let mut gamepad_events = app.world.resource_mut::<Events<GamepadEvent>>();
+        gamepad_events.send(GamepadEvent {
+            gamepad,
+            event_type: GamepadEventType::Connected,
+        });
+        app.update();
+
+        // Test that buttons are unpressed by default
+        assert!(!app.pressed(GamepadButtonType::North));
+
+        // Send inputs
+        app.send_input(GamepadButtonType::North);
+        app.update();
+
+        // Test the convenient .pressed API
+        assert!(app.pressed(GamepadButtonType::North));
+
+        // Test that resetting inputs works
+        app.reset_inputs();
+        app.update();
+
+        assert!(!app.pressed(GamepadButtonType::North));
     }
 
     #[test]
     #[cfg(feature = "ui")]
     fn ui_inputs() {
-        use crate::input_mocking::MockInput;
-        use bevy_ecs::prelude::*;
-        use bevy_ui::Interaction;
+        use bevy::ecs::prelude::*;
+        use bevy::ui::Interaction;
 
         #[derive(Component)]
         struct ButtonMarker;
 
-        let mut world = World::new();
+        let mut app = App::new();
+        app.add_plugin(InputPlugin);
+
         // Marked button
-        world.spawn().insert(Interaction::None).insert(ButtonMarker);
+        app.world
+            .spawn()
+            .insert(Interaction::None)
+            .insert(ButtonMarker);
         // Unmarked button
-        world.spawn().insert(Interaction::None);
+        app.world.spawn().insert(Interaction::None);
 
         // Click the button
-        world.click_button::<ButtonMarker>();
+        app.world.click_button::<ButtonMarker>();
+        app.update();
 
-        let mut interaction_query = world.query::<(&Interaction, Option<&ButtonMarker>)>();
-        for (interaction, maybe_marker) in interaction_query.iter(&world) {
+        let mut interaction_query = app.world.query::<(&Interaction, Option<&ButtonMarker>)>();
+        for (interaction, maybe_marker) in interaction_query.iter(&app.world) {
             match maybe_marker {
                 Some(_) => assert_eq!(*interaction, Interaction::Clicked),
                 None => assert_eq!(*interaction, Interaction::None),
@@ -450,18 +613,19 @@ mod test {
         }
 
         // Reset inputs
-        world.reset_inputs();
+        app.world.reset_inputs();
 
-        let mut interaction_query = world.query::<&Interaction>();
-        for interaction in interaction_query.iter(&world) {
+        let mut interaction_query = app.world.query::<&Interaction>();
+        for interaction in interaction_query.iter(&app.world) {
             assert_eq!(*interaction, Interaction::None)
         }
 
         // Hover over the button
-        world.hover_button::<ButtonMarker>();
+        app.hover_button::<ButtonMarker>();
+        app.update();
 
-        let mut interaction_query = world.query::<(&Interaction, Option<&ButtonMarker>)>();
-        for (interaction, maybe_marker) in interaction_query.iter(&world) {
+        let mut interaction_query = app.world.query::<(&Interaction, Option<&ButtonMarker>)>();
+        for (interaction, maybe_marker) in interaction_query.iter(&app.world) {
             match maybe_marker {
                 Some(_) => assert_eq!(*interaction, Interaction::Hovered),
                 None => assert_eq!(*interaction, Interaction::None),
@@ -469,10 +633,10 @@ mod test {
         }
 
         // Reset inputs
-        world.reset_inputs();
+        app.world.reset_inputs();
 
-        let mut interaction_query = world.query::<&Interaction>();
-        for interaction in interaction_query.iter(&world) {
+        let mut interaction_query = app.world.query::<&Interaction>();
+        for interaction in interaction_query.iter(&app.world) {
             assert_eq!(*interaction, Interaction::None)
         }
     }
diff --git a/src/input_streams.rs b/src/input_streams.rs
new file mode 100644
index 0000000..8b2c113
--- /dev/null
+++ b/src/input_streams.rs
@@ -0,0 +1,481 @@
+//! Unified input streams for working with [`bevy::input`] data.
+
+use bevy::input::{
+    gamepad::{Gamepad, GamepadAxis, GamepadButton, GamepadEventRaw, Gamepads},
+    keyboard::{KeyCode, KeyboardInput},
+    mouse::{MouseButton, MouseButtonInput, MouseMotion, MouseWheel},
+    Axis, Input,
+};
+use petitset::PetitSet;
+
+use bevy::ecs::prelude::{Events, ResMut, World};
+use bevy::ecs::system::SystemState;
+
+use crate::axislike::{
+    AxisType, DualAxisData, MouseMotionAxisType, MouseWheelAxisType, SingleAxis, VirtualDPad,
+};
+use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection};
+use crate::user_input::{InputKind, UserInput};
+
+/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap).
+///
+/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources.
+#[derive(Debug, Clone)]
+pub struct InputStreams<'a> {
+    /// A [`GamepadButton`] [`Input`] stream
+    pub gamepad_buttons: &'a Input<GamepadButton>,
+    /// A [`GamepadButton`] [`Axis`] stream
+    pub gamepad_button_axes: &'a Axis<GamepadButton>,
+    /// A [`GamepadAxis`] [`Axis`] stream
+    pub gamepad_axes: &'a Axis<GamepadAxis>,
+    /// A list of registered gamepads
+    pub gamepads: &'a Gamepads,
+    /// A [`KeyCode`] [`Input`] stream
+    pub keycode: &'a Input<KeyCode>,
+    /// A [`MouseButton`] [`Input`] stream
+    pub mouse_button: &'a Input<MouseButton>,
+    /// A [`MouseWheel`] event stream
+    pub mouse_wheel: &'a Events<MouseWheel>,
+    /// A [`MouseMotion`] event stream
+    pub mouse_motion: &'a Events<MouseMotion>,
+    /// The [`Gamepad`] that this struct will detect inputs from
+    pub associated_gamepad: Option<Gamepad>,
+}
+
+// Constructors
+impl<'a> InputStreams<'a> {
+    /// Construct an [`InputStreams`] from a [`World`]
+    pub fn from_world(world: &'a World, gamepad: Option<Gamepad>) -> Self {
+        let gamepad_buttons = world.resource::<Input<GamepadButton>>();
+        let gamepad_button_axes = world.resource::<Axis<GamepadButton>>();
+        let gamepad_axes = world.resource::<Axis<GamepadAxis>>();
+        let gamepads = world.resource::<Gamepads>();
+        let keyboard = world.resource::<Input<KeyCode>>();
+        let mouse = world.resource::<Input<MouseButton>>();
+        let mouse_wheel = world.resource::<Events<MouseWheel>>();
+        let mouse_motion = world.resource::<Events<MouseMotion>>();
+
+        InputStreams {
+            gamepad_buttons,
+            gamepad_button_axes,
+            gamepad_axes,
+            gamepads,
+            keycode: keyboard,
+            mouse_button: mouse,
+            mouse_wheel,
+            mouse_motion,
+            associated_gamepad: gamepad,
+        }
+    }
+}
+
+// Input checking
+impl<'a> InputStreams<'a> {
+    /// Guess which registered [`Gamepad`] should be used.
+    ///
+    /// If an associated gamepad is set, use that.
+    /// Otherwise use the first registered gamepad, if any.
+    pub fn guess_gamepad(&self) -> Option<Gamepad> {
+        match self.associated_gamepad {
+            Some(gamepad) => Some(gamepad),
+            None => self.gamepads.iter().next().copied(),
+        }
+    }
+
+    /// Is the `input` matched by the [`InputStreams`]?
+    pub fn input_pressed(&self, input: &UserInput) -> bool {
+        match input {
+            UserInput::Single(button) => self.button_pressed(*button),
+            UserInput::Chord(buttons) => self.all_buttons_pressed(buttons),
+            UserInput::VirtualDPad(VirtualDPad {
+                up,
+                down,
+                left,
+                right,
+            }) => {
+                for button in [up, down, left, right] {
+                    if self.button_pressed(*button) {
+                        return true;
+                    }
+                }
+                false
+            }
+        }
+    }
+
+    /// Is at least one of the `inputs` pressed?
+    #[must_use]
+    pub fn any_pressed(&self, inputs: &PetitSet<UserInput, 16>) -> bool {
+        for input in inputs.iter() {
+            if self.input_pressed(input) {
+                return true;
+            }
+        }
+        // If none of the inputs matched, return false
+        false
+    }
+
+    /// Is the `button` pressed?
+    #[must_use]
+    pub fn button_pressed(&self, button: InputKind) -> bool {
+        match button {
+            InputKind::DualAxis(_) => {
+                let axis_pair = self.input_axis_pair(&UserInput::Single(button)).unwrap();
+
+                axis_pair.length() != 0.0
+            }
+            InputKind::SingleAxis(_) => {
+                let value = self.input_value(&UserInput::Single(button));
+
+                value != 0.0
+            }
+            InputKind::GamepadButton(gamepad_button) => {
+                if let Some(gamepad) = self.guess_gamepad() {
+                    self.gamepad_buttons.pressed(GamepadButton {
+                        gamepad,
+                        button_type: gamepad_button,
+                    })
+                } else {
+                    false
+                }
+            }
+            InputKind::Keyboard(keycode) => self.keycode.pressed(keycode),
+            InputKind::Mouse(mouse_button) => self.mouse_button.pressed(mouse_button),
+            InputKind::MouseWheel(mouse_wheel_direction) => {
+                let mut total_mouse_wheel_movement = 0.0;
+
+                // FIXME: verify that this works and doesn't double count events
+                let mut event_reader = self.mouse_wheel.get_reader();
+
+                // PERF: this summing is computed for every individual input
+                // This should probably be computed once, and then cached / read
+                // Fix upstream!
+                for mouse_wheel_event in event_reader.iter(self.mouse_wheel) {
+                    total_mouse_wheel_movement += match mouse_wheel_direction {
+                        MouseWheelDirection::Up | MouseWheelDirection::Down => mouse_wheel_event.y,
+                        MouseWheelDirection::Left | MouseWheelDirection::Right => {
+                            mouse_wheel_event.x
+                        }
+                    }
+                }
+
+                match mouse_wheel_direction {
+                    MouseWheelDirection::Up | MouseWheelDirection::Right => {
+                        total_mouse_wheel_movement > 0.0
+                    }
+                    MouseWheelDirection::Down | MouseWheelDirection::Left => {
+                        total_mouse_wheel_movement < 0.0
+                    }
+                }
+            }
+            // CLEANUP: refactor to share code with MouseWheel
+            InputKind::MouseMotion(mouse_motion_direction) => {
+                let mut total_mouse_movement = 0.0;
+
+                // FIXME: verify that this works and doesn't double count events
+                let mut event_reader = self.mouse_motion.get_reader();
+
+                for mouse_motion_event in event_reader.iter(self.mouse_motion) {
+                    total_mouse_movement += match mouse_motion_direction {
+                        MouseMotionDirection::Up | MouseMotionDirection::Down => {
+                            mouse_motion_event.delta.y
+                        }
+                        MouseMotionDirection::Left | MouseMotionDirection::Right => {
+                            mouse_motion_event.delta.x
+                        }
+                    }
+                }
+
+                match mouse_motion_direction {
+                    MouseMotionDirection::Up | MouseMotionDirection::Right => {
+                        total_mouse_movement > 0.0
+                    }
+                    MouseMotionDirection::Down | MouseMotionDirection::Left => {
+                        total_mouse_movement < 0.0
+                    }
+                }
+            }
+        }
+    }
+
+    /// Are all of the `buttons` pressed?
+    #[must_use]
+    pub fn all_buttons_pressed(&self, buttons: &PetitSet<InputKind, 8>) -> bool {
+        for &button in buttons.iter() {
+            // If any of the appropriate inputs failed to match, the action is considered pressed
+            if !self.button_pressed(button) {
+                return false;
+            }
+        }
+        // If none of the inputs failed to match, return true
+        true
+    }
+
+    /// 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 reutrned data.
+    pub fn input_value(&self, input: &UserInput) -> f32 {
+        let use_button_value = || -> f32 {
+            if self.input_pressed(input) {
+                1.0
+            } else {
+                0.0
+            }
+        };
+
+        // 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, value: f32| -> f32 {
+            if value >= axis.negative_low && value <= axis.positive_low {
+                0.0
+            } else {
+                value
+            }
+        };
+
+        match input {
+            UserInput::Single(InputKind::SingleAxis(single_axis)) => {
+                match single_axis.axis_type {
+                    AxisType::Gamepad(axis_type) => {
+                        if let Some(gamepad) = self.guess_gamepad() {
+                            self.gamepad_axes
+                                .get(GamepadAxis { gamepad, axis_type })
+                                .unwrap_or_default()
+                        } else {
+                            0.0
+                        }
+                    }
+                    AxisType::MouseWheel(axis_type) => {
+                        let mut total_mouse_wheel_movement = 0.0;
+                        // FIXME: verify that this works and doesn't double count events
+                        let mut event_reader = self.mouse_wheel.get_reader();
+
+                        for mouse_wheel_event in event_reader.iter(self.mouse_wheel) {
+                            total_mouse_wheel_movement += match axis_type {
+                                MouseWheelAxisType::X => mouse_wheel_event.x,
+                                MouseWheelAxisType::Y => mouse_wheel_event.y,
+                            }
+                        }
+                        value_in_axis_range(single_axis, total_mouse_wheel_movement)
+                    }
+                    // CLEANUP: deduplicate code with MouseWheel
+                    AxisType::MouseMotion(axis_type) => {
+                        let mut total_mouse_motion_movement = 0.0;
+                        // FIXME: verify that this works and doesn't double count events
+                        let mut event_reader = self.mouse_motion.get_reader();
+
+                        for mouse_wheel_event in event_reader.iter(self.mouse_motion) {
+                            total_mouse_motion_movement += match axis_type {
+                                MouseMotionAxisType::X => mouse_wheel_event.delta.x,
+                                MouseMotionAxisType::Y => mouse_wheel_event.delta.y,
+                            }
+                        }
+                        value_in_axis_range(single_axis, total_mouse_motion_movement)
+                    }
+                }
+            }
+            UserInput::Single(InputKind::DualAxis(_)) => {
+                self.input_axis_pair(input).unwrap_or_default().length()
+            }
+            UserInput::VirtualDPad { .. } => {
+                self.input_axis_pair(input).unwrap_or_default().length()
+            }
+            // This is required because upstream bevy::input still waffles about whether triggers are buttons or axes
+            UserInput::Single(InputKind::GamepadButton(button_type)) => {
+                if let Some(gamepad) = self.guess_gamepad() {
+                    // Get the value from the registered gamepad
+                    self.gamepad_button_axes
+                        .get(GamepadButton {
+                            gamepad,
+                            button_type: *button_type,
+                        })
+                        .unwrap_or_else(use_button_value)
+                } else {
+                    0.0
+                }
+            }
+            _ => use_button_value(),
+        }
+    }
+
+    /// Get the axis pair associated to the user input.
+    ///
+    /// If `input` is not a [`DualAxis`] or [`VirtualDPad`], returns [`None`].
+    ///
+    /// See [`ActionState::action_axis_pair()`] for usage.
+    ///
+    /// # 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<DualAxisData> {
+        match input {
+            UserInput::Single(InputKind::DualAxis(dual_axis)) => {
+                let x = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.x)));
+                let y = self.input_value(&UserInput::Single(InputKind::SingleAxis(dual_axis.y)));
+
+                if x > dual_axis.x.positive_low
+                    || x < dual_axis.x.negative_low
+                    || y > dual_axis.y.positive_low
+                    || y < dual_axis.y.negative_low
+                {
+                    Some(DualAxisData::new(x, y))
+                } else {
+                    Some(DualAxisData::new(0.0, 0.0))
+                }
+            }
+            UserInput::VirtualDPad(VirtualDPad {
+                up,
+                down,
+                left,
+                right,
+            }) => {
+                let x = self.input_value(&UserInput::Single(*right)).abs()
+                    - self.input_value(&UserInput::Single(*left)).abs();
+                let y = self.input_value(&UserInput::Single(*up)).abs()
+                    - self.input_value(&UserInput::Single(*down)).abs();
+                Some(DualAxisData::new(x, y))
+            }
+            _ => None,
+        }
+    }
+}
+
+/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs.
+///
+/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources.
+// WARNING: If you update the fields of this type, you must also remember to update `InputMocking::reset_inputs`.
+#[derive(Debug)]
+pub struct MutableInputStreams<'a> {
+    /// A [`GamepadButton`] [`Input`] stream
+    pub gamepad_buttons: &'a mut Input<GamepadButton>,
+    /// A [`GamepadButton`] [`Axis`] stream
+    pub gamepad_button_axes: &'a mut Axis<GamepadButton>,
+    /// A [`GamepadAxis`] [`Axis`] stream
+    pub gamepad_axes: &'a mut Axis<GamepadAxis>,
+    /// A list of registered [`Gamepads`]
+    pub gamepads: &'a mut Gamepads,
+    /// Events used for mocking gamepad-related inputs
+    pub gamepad_events: &'a mut Events<GamepadEventRaw>,
+
+    /// A [`KeyCode`] [`Input`] stream
+    pub keycode: &'a mut Input<KeyCode>,
+    /// Events used for mocking keyboard-related inputs
+    pub keyboard_events: &'a mut Events<KeyboardInput>,
+
+    /// A [`MouseButton`] [`Input`] stream
+    pub mouse_button: &'a mut Input<MouseButton>,
+    /// Events used for mocking [`MouseButton`] inputs
+    pub mouse_button_events: &'a mut Events<MouseButtonInput>,
+    /// A [`MouseWheel`] event stream
+    pub mouse_wheel: &'a mut Events<MouseWheel>,
+    /// A [`MouseMotion`] event stream
+    pub mouse_motion: &'a mut Events<MouseMotion>,
+
+    /// The [`Gamepad`] that this struct will detect inputs from
+    pub associated_gamepad: Option<Gamepad>,
+}
+
+impl<'a> MutableInputStreams<'a> {
+    /// Construct a [`MutableInputStreams`] from the [`World`]
+    pub fn from_world(world: &'a mut World, gamepad: Option<Gamepad>) -> Self {
+        let mut input_system_state: SystemState<(
+            ResMut<Input<GamepadButton>>,
+            ResMut<Axis<GamepadButton>>,
+            ResMut<Axis<GamepadAxis>>,
+            ResMut<Gamepads>,
+            ResMut<Events<GamepadEventRaw>>,
+            ResMut<Input<KeyCode>>,
+            ResMut<Events<KeyboardInput>>,
+            ResMut<Input<MouseButton>>,
+            ResMut<Events<MouseButtonInput>>,
+            ResMut<Events<MouseWheel>>,
+            ResMut<Events<MouseMotion>>,
+        )> = SystemState::new(world);
+
+        let (
+            gamepad_buttons,
+            gamepad_button_axes,
+            gamepad_axes,
+            gamepads,
+            gamepad_events,
+            keyboard,
+            keyboard_events,
+            mouse,
+            mouse_button_events,
+            mouse_wheel,
+            mouse_motion,
+        ) = input_system_state.get_mut(world);
+
+        MutableInputStreams {
+            gamepad_buttons: gamepad_buttons.into_inner(),
+            gamepad_button_axes: gamepad_button_axes.into_inner(),
+            gamepad_axes: gamepad_axes.into_inner(),
+            gamepads: gamepads.into_inner(),
+            gamepad_events: gamepad_events.into_inner(),
+            keycode: keyboard.into_inner(),
+            keyboard_events: keyboard_events.into_inner(),
+            mouse_button: mouse.into_inner(),
+            mouse_button_events: mouse_button_events.into_inner(),
+            mouse_wheel: mouse_wheel.into_inner(),
+            mouse_motion: mouse_motion.into_inner(),
+            associated_gamepad: gamepad,
+        }
+    }
+
+    /// Guess which registered [`Gamepad`] should be used.
+    ///
+    /// If an associated gamepad is set, use that.
+    /// Otherwise use the first registered gamepad, if any.
+    pub fn guess_gamepad(&self) -> Option<Gamepad> {
+        match self.associated_gamepad {
+            Some(gamepad) => Some(gamepad),
+            None => self.gamepads.iter().next().copied(),
+        }
+    }
+}
+
+impl<'a> From<MutableInputStreams<'a>> for InputStreams<'a> {
+    fn from(mutable_streams: MutableInputStreams<'a>) -> Self {
+        InputStreams {
+            // This absurd-looking &*(foo) pattern convinces the compiler
+            // that we want a reference to the underlying data with the correct lifetime
+            gamepad_buttons: &*(mutable_streams.gamepad_buttons),
+            gamepad_button_axes: &*(mutable_streams.gamepad_button_axes),
+            gamepad_axes: &*(mutable_streams.gamepad_axes),
+            gamepads: &*(mutable_streams.gamepads),
+            keycode: &*(mutable_streams.keycode),
+            mouse_button: &*(mutable_streams.mouse_button),
+            mouse_wheel: &*(mutable_streams.mouse_wheel),
+            mouse_motion: &*(mutable_streams.mouse_motion),
+            associated_gamepad: mutable_streams.associated_gamepad,
+        }
+    }
+}
+
+impl<'a> From<&'a MutableInputStreams<'a>> for InputStreams<'a> {
+    fn from(mutable_streams: &'a MutableInputStreams<'a>) -> Self {
+        InputStreams {
+            // This absurd-looking &*(foo) pattern convinces the compiler
+            // that we want a reference to the underlying data with the correct lifetime
+            gamepad_buttons: &*(mutable_streams.gamepad_buttons),
+            gamepad_button_axes: &*(mutable_streams.gamepad_button_axes),
+            gamepad_axes: &*(mutable_streams.gamepad_axes),
+            gamepads: &*(mutable_streams.gamepads),
+            keycode: &*(mutable_streams.keycode),
+            mouse_button: &*(mutable_streams.mouse_button),
+            mouse_wheel: &*(mutable_streams.mouse_wheel),
+            mouse_motion: &*(mutable_streams.mouse_motion),
+            associated_gamepad: mutable_streams.associated_gamepad,
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 372429d..bcc1521 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,19 +5,18 @@
 
 use crate::action_state::ActionState;
 use crate::input_map::InputMap;
-use bevy_ecs::prelude::*;
+use bevy::ecs::prelude::*;
 use std::marker::PhantomData;
 
 pub mod action_state;
+pub mod axislike;
+pub mod buttonlike;
 pub mod clashing_inputs;
 mod display_impl;
 pub mod errors;
 pub mod input_map;
-mod input_mocking;
-// Re-export this at the root level for convenience
-pub use input_mocking::MockInput;
-pub mod axislike;
-pub mod buttonlike;
+pub mod input_mocking;
+pub mod input_streams;
 pub mod orientation;
 pub mod plugin;
 pub mod systems;
@@ -29,8 +28,11 @@ pub use leafwing_input_manager_macros::Actionlike;
 /// Everything you need to get started
 pub mod prelude {
     pub use crate::action_state::{ActionState, ActionStateDriver};
+    pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad};
+    pub use crate::buttonlike::MouseWheelDirection;
     pub use crate::clashing_inputs::ClashStrategy;
     pub use crate::input_map::InputMap;
+    pub use crate::input_mocking::MockInput;
     pub use crate::user_input::UserInput;
 
     pub use crate::plugin::InputManagerPlugin;
diff --git a/src/orientation.rs b/src/orientation.rs
index eec5d68..71fd5e9 100644
--- a/src/orientation.rs
+++ b/src/orientation.rs
@@ -7,8 +7,8 @@ pub use rotation_direction::RotationDirection;
 
 mod orientation_trait {
     use super::{Direction, Rotation, RotationDirection};
-    use bevy_math::Quat;
-    use bevy_transform::components::{GlobalTransform, Transform};
+    use bevy::math::Quat;
+    use bevy::transform::components::{GlobalTransform, Transform};
     use core::fmt::Debug;
 
     /// A type that can represent a orientation in 2D space
@@ -67,7 +67,7 @@ mod orientation_trait {
 
             let rotation_to = target_rotation - self_rotation;
 
-            if rotation_to <= Rotation::new(1800) {
+            if rotation_to.deci_degrees == 0 || rotation_to.deci_degrees >= 1800 {
                 RotationDirection::Clockwise
             } else {
                 RotationDirection::CounterClockwise
@@ -97,8 +97,8 @@ mod orientation_trait {
                     *self = target_orientation;
                 } else {
                     let delta_rotation = match self.rotation_direction(target_orientation) {
-                        RotationDirection::Clockwise => max_rotation,
-                        RotationDirection::CounterClockwise => -max_rotation,
+                        RotationDirection::CounterClockwise => max_rotation,
+                        RotationDirection::Clockwise => -max_rotation,
                     };
                     let current_rotation: Rotation = (*self).into();
                     let new_rotation: Rotation = current_rotation + delta_rotation;
@@ -194,8 +194,8 @@ mod rotation_direction {
         #[must_use]
         pub fn sign(self) -> isize {
             match self {
-                RotationDirection::Clockwise => 1,
-                RotationDirection::CounterClockwise => -1,
+                RotationDirection::Clockwise => -1,
+                RotationDirection::CounterClockwise => 1,
             }
         }
 
@@ -220,43 +220,44 @@ mod rotation_direction {
 
 mod rotation {
     use crate::errors::NearlySingularConversion;
-    use bevy_ecs::prelude::Component;
-    use bevy_math::Vec2;
+    use bevy::ecs::prelude::Component;
+    use bevy::math::Vec2;
     use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
     use derive_more::Display;
+    use std::f32::consts::TAU;
 
     /// A discretized 2-dimensional rotation
     ///
-    /// Internally, these are stored in normalized tenths of a degree, and so can be cleanly added and reversed
-    /// without accumulating error.
+    /// Internally, these are stored in tenths of a degree, and so can be cleanly added
+    /// and reversed without accumulating error.
     ///
     /// # Example
     /// ```rust
     /// use leafwing_input_manager::orientation::{Rotation, Direction, Orientation};
-    /// use core::f32::consts::{PI, TAU};
+    /// use core::f32::consts::{FRAC_PI_2, PI, TAU};
     ///
-    /// let three_o_clock = Rotation::from_degrees(90.0);
-    /// let six_o_clock = Rotation::from_radians(PI);
-    /// let nine_o_clock = Rotation::from_degrees(-90.0);
+    /// let east = Rotation::from_radians(0.0);
+    /// let north = Rotation::from_radians(FRAC_PI_2);
+    /// let west = Rotation::from_radians(PI);
     ///
     /// Rotation::default().assert_approx_eq(Rotation::from_radians(0.0));
     /// Rotation::default().assert_approx_eq(Rotation::from_radians(TAU));
     /// Rotation::default().assert_approx_eq(500.0 * Rotation::from_radians(TAU));
     ///
-    /// (three_o_clock + six_o_clock).assert_approx_eq(nine_o_clock);
-    /// (nine_o_clock - three_o_clock).assert_approx_eq(six_o_clock);
-    /// (2.0 * nine_o_clock).assert_approx_eq(six_o_clock);
-    /// (six_o_clock / 2.0).assert_approx_eq(three_o_clock);
+    /// (north + north).assert_approx_eq(west);
+    /// (west - east).assert_approx_eq(west);
+    /// (2.0 * north).assert_approx_eq(west);
+    /// (west / 2.0).assert_approx_eq(north);
     ///
-    /// six_o_clock.assert_approx_eq(Rotation::SOUTH);
+    /// north.assert_approx_eq(Rotation::NORTH);
     ///
-    /// Direction::from(nine_o_clock).assert_approx_eq(Direction::WEST);
+    /// Direction::from(west).assert_approx_eq(Direction::WEST);
     /// ```
     #[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Default, Display)]
     pub struct Rotation {
         /// Tenths of a degree, measured clockwise from midnight (x=0, y=1)
         ///
-        /// 3600 make up a full circle.
+        /// 3600 make up a full circle
         pub(crate) deci_degrees: u16,
     }
 
@@ -290,23 +291,23 @@ mod rotation {
         pub const FULL_CIRCLE: u16 = 3600;
 
         /// The direction that points straight up
-        pub const NORTH: Rotation = Rotation { deci_degrees: 0 };
+        pub const NORTH: Rotation = Rotation { deci_degrees: 900 };
 
         /// The direction that points straight right
-        pub const EAST: Rotation = Rotation { deci_degrees: 900 };
+        pub const EAST: Rotation = Rotation { deci_degrees: 0 };
         /// The direction that points straight down
-        pub const SOUTH: Rotation = Rotation { deci_degrees: 1800 };
+        pub const SOUTH: Rotation = Rotation { deci_degrees: 2700 };
         /// The direction that points straight left
-        pub const WEST: Rotation = Rotation { deci_degrees: 2700 };
+        pub const WEST: Rotation = Rotation { deci_degrees: 1800 };
 
         /// The direction that points halfway between up and right
         pub const NORTHEAST: Rotation = Rotation { deci_degrees: 450 };
         /// The direction that points halfway between down and right
-        pub const SOUTHEAST: Rotation = Rotation { deci_degrees: 1350 };
+        pub const SOUTHEAST: Rotation = Rotation { deci_degrees: 3150 };
         /// The direction that points halfway between down and left
         pub const SOUTHWEST: Rotation = Rotation { deci_degrees: 2250 };
         /// The direction that points halfway between left and up
-        pub const NORTHWEST: Rotation = Rotation { deci_degrees: 3150 };
+        pub const NORTHWEST: Rotation = Rotation { deci_degrees: 1350 };
     }
 
     // Conversion methods
@@ -318,7 +319,7 @@ mod rotation {
         ///
         /// # Example
         /// ```rust
-        /// use bevy_math::Vec2;
+        /// use bevy::math::Vec2;
         /// use leafwing_input_manager::orientation::Rotation;
         ///
         /// assert_eq!(Rotation::from_xy(Vec2::new(0.0, 1.0)), Ok(Rotation::NORTH));
@@ -328,7 +329,7 @@ mod rotation {
             if xy.length_squared() < f32::EPSILON * f32::EPSILON {
                 Err(NearlySingularConversion)
             } else {
-                let radians = f32::atan2(xy.x, xy.y);
+                let radians = f32::atan2(xy.y, xy.x);
                 Ok(Rotation::from_radians(radians))
             }
         }
@@ -338,30 +339,29 @@ mod rotation {
         #[must_use]
         pub fn into_xy(self) -> Vec2 {
             let radians = self.into_radians();
-            Vec2::new(radians.sin(), radians.cos())
+            Vec2::new(radians.cos(), radians.sin())
         }
 
-        /// Construct a [`Direction`](crate::orientation::Direction) from radians, measured clockwise from midnight
+        /// Construct a [`Direction`](crate::orientation::Direction) from radians,
+        /// measured counterclockwise from the positive x axis
         #[must_use]
         #[inline]
         pub fn from_radians(radians: impl Into<f32>) -> Rotation {
-            use std::f32::consts::TAU;
-
-            let normalized_radians: f32 = radians.into().rem_euclid(TAU);
+            let normalized_radians = radians.into().rem_euclid(TAU);
 
             Rotation {
-                deci_degrees: (normalized_radians * 3600. / TAU) as u16,
+                deci_degrees: (normalized_radians * (3600. / TAU)) as u16,
             }
         }
 
-        /// Converts this direction into radians, measured clockwise from midnight
+        /// Converts this direction into radians, measured counterclockwise from the positive x axis
         #[inline]
         #[must_use]
         pub fn into_radians(self) -> f32 {
-            self.deci_degrees as f32 * std::f32::consts::TAU / 3600.
+            self.deci_degrees as f32 * TAU / 3600.
         }
 
-        /// Construct a [`Direction`](crate::orientation::Direction) from degrees, measured clockwise from midnight
+        /// Construct a [`Direction`](crate::orientation::Direction) from degrees, measured counterclockwise from the positive x axis
         #[must_use]
         #[inline]
         pub fn from_degrees(degrees: impl Into<f32>) -> Rotation {
@@ -372,7 +372,7 @@ mod rotation {
             }
         }
 
-        /// Converts this direction into degrees, measured clockwise from midnight
+        /// Converts this direction into degrees, measured counterclockwise from the positive x axis
         #[inline]
         #[must_use]
         pub fn into_degrees(self) -> f32 {
@@ -407,7 +407,7 @@ mod rotation {
     impl SubAssign for Rotation {
         fn sub_assign(&mut self, rhs: Self) {
             // Be sure to avoid overflow when subtracting
-            if self.deci_degrees > rhs.deci_degrees {
+            if self.deci_degrees >= rhs.deci_degrees {
                 self.deci_degrees = self.deci_degrees - rhs.deci_degrees;
             } else {
                 self.deci_degrees = Rotation::FULL_CIRCLE - (rhs.deci_degrees - self.deci_degrees);
@@ -454,20 +454,20 @@ mod rotation {
 }
 
 mod direction {
-    use bevy_ecs::prelude::Component;
-    use bevy_math::{const_vec2, Vec2, Vec3};
+    use bevy::ecs::prelude::Component;
+    use bevy::math::{Vec2, Vec3};
     use core::ops::{Add, Div, Mul, Neg, Sub};
     use derive_more::Display;
     use std::f32::consts::SQRT_2;
 
     /// A 2D unit vector that represents a direction
     ///
-    /// Its magnitude is always one.
+    /// Its magnitude is always `1.0`.
     ///
     /// # Example
     /// ```rust
     /// use leafwing_input_manager::orientation::Direction;
-    /// use bevy_math::Vec2;
+    /// use bevy::math::Vec2;
     ///
     /// assert_eq!(Direction::NORTH.unit_vector(), Vec2::new(0.0, 1.0));
     /// assert_eq!(Direction::try_from(Vec2::ONE), Ok(Direction::NORTHEAST));
@@ -481,35 +481,31 @@ mod direction {
     }
 
     impl Default for Direction {
-        /// [`Direction::NORTH`] is the default direction,
+        /// [`Direction::EAST`] is the default direction,
         /// as it is consistent with the default [`Rotation`]
         fn default() -> Direction {
-            Direction::NORTH
+            Direction::EAST
         }
     }
 
     impl Direction {
         /// Creates a new [`Direction`] from a [`Vec2`]
         ///
-        /// The [`Vec2`] will be normalized to have a magnitude of 1.
+        /// The [`Vec2`] stored internally will be normalized to have a magnitude of `1.0`.
         ///
         /// # Panics
-        /// Panics if the supplied vector has length zero.
+        ///
+        /// Panics if the length of the supplied vector has length zero or cannot be determined.
+        /// Use [`try_from`](TryFrom) to get a [`Result`] instead.
         #[must_use]
         #[inline]
         pub fn new(vec2: Vec2) -> Self {
-            if vec2.length_squared() == 0.0 {
-                panic!("Supplied a Vec2 with length 0 to a Direction.")
-            };
-
-            Self {
-                unit_vector: vec2.normalize(),
-            }
+            Self::try_from(vec2).unwrap()
         }
 
         /// Returns the raw underlying [`Vec2`] unit vector of this direction
         ///
-        /// This will always have a magnitude of 1, unless it is [`Direction::NEUTRAL`]
+        /// This will always have a length of `1.0`
         #[must_use]
         #[inline]
         pub const fn unit_vector(&self) -> Vec2 {
@@ -521,36 +517,36 @@ mod direction {
     impl Direction {
         /// The direction that points straight up
         pub const NORTH: Direction = Direction {
-            unit_vector: const_vec2!([0.0, 1.0]),
+            unit_vector: Vec2::new(0.0, 1.0),
         };
         /// The direction that points straight right
         pub const EAST: Direction = Direction {
-            unit_vector: const_vec2!([1.0, 0.0]),
+            unit_vector: Vec2::new(1.0, 0.0),
         };
         /// The direction that points straight down
         pub const SOUTH: Direction = Direction {
-            unit_vector: const_vec2!([0.0, -1.0]),
+            unit_vector: Vec2::new(0.0, -1.0),
         };
         /// The direction that points straight left
         pub const WEST: Direction = Direction {
-            unit_vector: const_vec2!([-1.0, 0.0]),
+            unit_vector: Vec2::new(-1.0, 0.0),
         };
 
         /// The direction that points halfway between up and right
         pub const NORTHEAST: Direction = Direction {
-            unit_vector: const_vec2!([SQRT_2 / 2.0, SQRT_2 / 2.0]),
+            unit_vector: Vec2::new(SQRT_2 / 2.0, SQRT_2 / 2.0),
         };
         /// The direction that points halfway between down and right
         pub const SOUTHEAST: Direction = Direction {
-            unit_vector: const_vec2!([SQRT_2 / 2.0, -SQRT_2 / 2.0]),
+            unit_vector: Vec2::new(SQRT_2 / 2.0, -SQRT_2 / 2.0),
         };
         /// The direction that points halfway between down and left
         pub const SOUTHWEST: Direction = Direction {
-            unit_vector: const_vec2!([-SQRT_2 / 2.0, -SQRT_2 / 2.0]),
+            unit_vector: Vec2::new(-SQRT_2 / 2.0, -SQRT_2 / 2.0),
         };
         /// The direction that points halfway between left and up
         pub const NORTHWEST: Direction = Direction {
-            unit_vector: const_vec2!([-SQRT_2 / 2.0, SQRT_2 / 2.0]),
+            unit_vector: Vec2::new(-SQRT_2 / 2.0, SQRT_2 / 2.0),
         };
     }
 
@@ -572,38 +568,38 @@ mod direction {
     impl Mul<f32> for Direction {
         type Output = Vec2;
 
-        fn mul(self, rhs: f32) -> Self::Output {
-            Vec2::new(self.unit_vector.x * rhs, self.unit_vector.y * rhs)
+        fn mul(self, rhs: f32) -> Vec2 {
+            self.unit_vector * rhs
         }
     }
 
     impl Mul<Direction> for f32 {
         type Output = Vec2;
 
-        fn mul(self, rhs: Direction) -> Self::Output {
-            Vec2::new(self * rhs.unit_vector.x, self * rhs.unit_vector.y)
+        fn mul(self, rhs: Direction) -> Vec2 {
+            self * rhs.unit_vector
         }
     }
 
     impl Div<f32> for Direction {
         type Output = Vec2;
 
-        fn div(self, rhs: f32) -> Self::Output {
-            Vec2::new(self.unit_vector.x / rhs, self.unit_vector.y / rhs)
+        fn div(self, rhs: f32) -> Vec2 {
+            self.unit_vector / rhs
         }
     }
 
     impl Div<Direction> for f32 {
         type Output = Vec2;
 
-        fn div(self, rhs: Direction) -> Self::Output {
-            Vec2::new(self / rhs.unit_vector.x, self / rhs.unit_vector.y)
+        fn div(self, rhs: Direction) -> Vec2 {
+            self / rhs.unit_vector
         }
     }
 
     impl From<Direction> for Vec3 {
         fn from(direction: Direction) -> Vec3 {
-            Vec3::new(direction.unit_vector.x, direction.unit_vector.y, 0.0)
+            direction.unit_vector.extend(0.0)
         }
     }
 
@@ -621,8 +617,8 @@ mod direction {
 mod conversions {
     use super::{Direction, Rotation};
     use crate::errors::NearlySingularConversion;
-    use bevy_math::{Quat, Vec2, Vec3};
-    use bevy_transform::components::{GlobalTransform, Transform};
+    use bevy::math::{Quat, Vec2, Vec3};
+    use bevy::transform::components::{GlobalTransform, Transform};
 
     impl From<Rotation> for Direction {
         fn from(rotation: Rotation) -> Direction {
@@ -634,8 +630,23 @@ mod conversions {
 
     impl From<Direction> for Rotation {
         fn from(direction: Direction) -> Rotation {
-            let radians = f32::atan2(direction.unit_vector().x, direction.unit_vector().y);
-            Rotation::from_radians(radians)
+            let radians = direction.unit_vector.y.atan2(direction.unit_vector.x);
+            // This dirty little trick helps us nudge the two (of eight) cardinal directions onto
+            // the correct decidegree. 32-bit floating point math rounds to the wrong decidegree,
+            // which usually isn't a big deal, but can result in unexpected surprises when people
+            // are dealing only with cardinal directions. The underlying problem is that f32 values
+            // for 1.0 and -1.0 can't be represented exactly, so our unit vectors start with an
+            // approximate value and both `atan2` above and `from_radians` below magnify the
+            // imprecision. So, we cheat.
+            const APPROX_SOUTH: f32 = -1.5707964;
+            const APPROX_NORTHWEST: f32 = 2.3561945;
+            if radians == APPROX_NORTHWEST {
+                Rotation::new(1350)
+            } else if radians == APPROX_SOUTH {
+                Rotation::new(2700)
+            } else {
+                Rotation::from_radians(radians)
+            }
         }
     }
 
@@ -657,12 +668,9 @@ mod conversions {
         type Error = NearlySingularConversion;
 
         fn try_from(vec2: Vec2) -> Result<Direction, NearlySingularConversion> {
-            if vec2.length_squared() == 0.0 {
-                Err(NearlySingularConversion)
-            } else {
-                Ok(Direction {
-                    unit_vector: vec2.normalize(),
-                })
+            match vec2.try_normalize() {
+                Some(unit_vector) => Ok(Direction { unit_vector }),
+                None => Err(NearlySingularConversion),
             }
         }
     }
@@ -682,21 +690,15 @@ mod conversions {
 
     impl From<Rotation> for Quat {
         fn from(rotation: Rotation) -> Self {
-            // This is needed to ensure the rotation direction is correct
-            Quat::from_rotation_z(-rotation.into_radians())
+            Quat::from_rotation_z(rotation.into_radians())
         }
     }
 
     impl From<Quat> for Direction {
         fn from(quaternion: Quat) -> Self {
-            let vec2 = quaternion.mul_vec3(Vec3::Y).truncate();
-
-            if vec2 == Vec2::ZERO {
-                Direction::default()
-            } else {
-                Direction {
-                    unit_vector: vec2.normalize(),
-                }
+            match quaternion.mul_vec3(Vec3::X).truncate().try_normalize() {
+                Some(unit_vector) => Direction { unit_vector },
+                None => Default::default(),
             }
         }
     }
@@ -716,7 +718,7 @@ mod conversions {
 
     impl From<GlobalTransform> for Direction {
         fn from(transform: GlobalTransform) -> Self {
-            transform.rotation.into()
+            transform.to_scale_rotation_translation().1.into()
         }
     }
 
@@ -740,7 +742,7 @@ mod conversions {
 
     impl From<GlobalTransform> for Rotation {
         fn from(transform: GlobalTransform) -> Self {
-            transform.rotation.into()
+            transform.to_scale_rotation_translation().1.into()
         }
     }
 
@@ -756,3 +758,60 @@ mod conversions {
         }
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn directions_end_up_even() {
+        let north_rot: Rotation = Direction::NORTH.into();
+        assert_eq!(
+            north_rot,
+            Rotation::new(900),
+            "we want north to end up exact in decidegrees"
+        );
+        let northeast_rot: Rotation = Direction::NORTHEAST.into();
+        assert_eq!(
+            northeast_rot,
+            Rotation::new(450),
+            "we want northeast to end up exact in decidegrees"
+        );
+        let northwest_rot: Rotation = Direction::NORTHWEST.into();
+        assert_eq!(
+            northwest_rot,
+            Rotation::new(1350),
+            "we want northwest to end up exact in decidegrees"
+        );
+        let south_rot: Rotation = Direction::SOUTH.into();
+        assert_eq!(
+            south_rot,
+            Rotation::new(2700),
+            "we want south to end up exact in decidegrees"
+        );
+        let southeast_rot: Rotation = Direction::SOUTHEAST.into();
+        assert_eq!(
+            southeast_rot,
+            Rotation::new(3150),
+            "we want southeast to end up exact in decidegrees"
+        );
+        let southwest_rot: Rotation = Direction::SOUTHWEST.into();
+        assert_eq!(
+            southwest_rot,
+            Rotation::new(2250),
+            "we want southwest to end up exact in decidegrees"
+        );
+        let east_rot: Rotation = Direction::EAST.into();
+        assert_eq!(
+            east_rot,
+            Rotation::new(0),
+            "we want east to end up exact in decidegrees"
+        );
+        let west_rot: Rotation = Direction::WEST.into();
+        assert_eq!(
+            west_rot,
+            Rotation::new(1800),
+            "we want west to end up exact in decidegrees"
+        );
+    }
+}
diff --git a/src/plugin.rs b/src/plugin.rs
index 256bbb7..f95b5c4 100644
--- a/src/plugin.rs
+++ b/src/plugin.rs
@@ -6,11 +6,11 @@ use core::hash::Hash;
 use core::marker::PhantomData;
 use std::fmt::Debug;
 
-use bevy_app::{App, CoreStage, Plugin};
-use bevy_ecs::prelude::*;
-use bevy_input::InputSystem;
+use bevy::app::{App, CoreStage, Plugin};
+use bevy::ecs::prelude::*;
+use bevy::input::InputSystem;
 #[cfg(feature = "ui")]
-use bevy_ui::UiSystem;
+use bevy::ui::UiSystem;
 
 /// A [`Plugin`] that collects [`Input`](bevy::input::Input) from disparate sources, producing an [`ActionState`](crate::action_state::ActionState) that can be conveniently checked
 ///
diff --git a/src/systems.rs b/src/systems.rs
index f1f43a4..430b17f 100644
--- a/src/systems.rs
+++ b/src/systems.rs
@@ -6,18 +6,23 @@ use crate::{
     action_state::{ActionDiff, ActionState},
     clashing_inputs::ClashStrategy,
     input_map::InputMap,
+    input_streams::InputStreams,
     plugin::ToggleActions,
-    user_input::InputStreams,
     Actionlike,
 };
 
-use bevy_core::Time;
-use bevy_ecs::{prelude::*, schedule::ShouldRun};
-use bevy_input::{gamepad::GamepadButton, keyboard::KeyCode, mouse::MouseButton, Input};
-use bevy_utils::Instant;
+use bevy::ecs::{prelude::*, schedule::ShouldRun};
+use bevy::input::{
+    gamepad::{GamepadAxis, GamepadButton, Gamepads},
+    keyboard::KeyCode,
+    mouse::{MouseButton, MouseMotion, MouseWheel},
+    Axis, Input,
+};
+use bevy::time::Time;
+use bevy::utils::Instant;
 
 #[cfg(feature = "ui")]
-use bevy_ui::Interaction;
+use bevy::ui::Interaction;
 
 /// Advances actions timer.
 ///
@@ -54,25 +59,38 @@ pub fn tick_action_state<A: Actionlike>(
 /// Missing resources will be ignored, and treated as if none of the corresponding inputs were pressed
 #[allow(clippy::too_many_arguments)]
 pub fn update_action_state<A: Actionlike>(
-    maybe_gamepad_input_stream: Option<Res<Input<GamepadButton>>>,
-    maybe_keyboard_input_stream: Option<Res<Input<KeyCode>>>,
-    maybe_mouse_input_stream: Option<Res<Input<MouseButton>>>,
+    gamepad_buttons: Res<Input<GamepadButton>>,
+    gamepad_button_axes: Res<Axis<GamepadButton>>,
+    gamepad_axes: Res<Axis<GamepadAxis>>,
+    gamepads: Res<Gamepads>,
+    keycode: Res<Input<KeyCode>>,
+    mouse_button: Res<Input<MouseButton>>,
+    mouse_wheel: Res<Events<MouseWheel>>,
+    mouse_motion: Res<Events<MouseMotion>>,
     clash_strategy: Res<ClashStrategy>,
     mut action_state: Option<ResMut<ActionState<A>>>,
     mut input_map: Option<ResMut<InputMap<A>>>,
     mut query: Query<(&mut ActionState<A>, &InputMap<A>)>,
 ) {
-    let gamepad = maybe_gamepad_input_stream.as_deref();
-
-    let keyboard = maybe_keyboard_input_stream.as_deref();
-
-    let mouse = maybe_mouse_input_stream.as_deref();
+    let gamepad_buttons = gamepad_buttons.into_inner();
+    let gamepad_button_axes = gamepad_button_axes.into_inner();
+    let gamepad_axes = gamepad_axes.into_inner();
+    let gamepads = gamepads.into_inner();
+    let keycode = keycode.into_inner();
+    let mouse_button = mouse_button.into_inner();
+    let mouse_wheel = mouse_wheel.into_inner();
+    let mouse_motion = mouse_motion.into_inner();
 
     if let (Some(input_map), Some(action_state)) = (&mut input_map, &mut action_state) {
         let input_streams = InputStreams {
-            gamepad,
-            keyboard,
-            mouse,
+            gamepad_buttons,
+            gamepad_button_axes,
+            gamepad_axes,
+            gamepads,
+            keycode,
+            mouse_button,
+            mouse_wheel,
+            mouse_motion,
             associated_gamepad: input_map.gamepad(),
         };
 
@@ -81,9 +99,14 @@ pub fn update_action_state<A: Actionlike>(
 
     for (mut action_state, input_map) in query.iter_mut() {
         let input_streams = InputStreams {
-            gamepad,
-            keyboard,
-            mouse,
+            gamepad_buttons,
+            gamepad_button_axes,
+            gamepad_axes,
+            gamepads,
+            keycode,
+            mouse_button,
+            mouse_wheel,
+            mouse_motion,
             associated_gamepad: input_map.gamepad(),
         };
 
@@ -109,7 +132,7 @@ pub fn update_action_state_from_interaction<A: Actionlike>(
     }
 }
 
-/// Generates an [`Events`](bevy_ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`]
+/// Generates an [`Events`](bevy::ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`]
 ///
 /// The `ID` generic type should be a stable entity identifer,
 /// suitable to be sent across a network.
@@ -136,7 +159,7 @@ pub fn generate_action_diffs<A: Actionlike, ID: Eq + Clone + Component>(
     }
 }
 
-/// Generates an [`Events`](bevy_ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`]
+/// Generates an [`Events`](bevy::ecs::event::Events) stream of [`ActionDiff`] from [`ActionState`]
 ///
 /// The `ID` generic type should be a stable entity identifer,
 /// suitable to be sent across a network.
diff --git a/src/user_input.rs b/src/user_input.rs
index e0c5097..61ccbec 100644
--- a/src/user_input.rs
+++ b/src/user_input.rs
@@ -1,444 +1,573 @@
-//! Helpful abstractions over user inputs of all sorts
-
-use bevy_input::{
-    gamepad::{Gamepad, GamepadButton, GamepadButtonType},
-    keyboard::KeyCode,
-    mouse::MouseButton,
-    Input,
-};
-
-use bevy_utils::HashSet;
-use petitset::PetitSet;
-use serde::{Deserialize, Serialize};
-
-/// Some combination of user input, which may cross [`Input`] boundaries
-///
-/// Suitable for use in an [`InputMap`](crate::input_map::InputMap)
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub enum UserInput {
-    /// A single button
-    Single(InputButton),
-    /// A combination of buttons, pressed simultaneously
-    ///
-    /// Up to 8 (!!) buttons can be chorded together at once.
-    /// Chords are considered to belong to all of the [InputMode]s of their constituent buttons.
-    Chord(PetitSet<InputButton, 8>),
-}
-
-impl UserInput {
-    /// Creates a [`UserInput::Chord`] from an iterator of [`Button`]s
-    ///
-    /// If `buttons` has a length of 1, a [`UserInput::Single`] variant will be returned instead.
-    pub fn chord(buttons: impl IntoIterator<Item = impl Into<InputButton>>) -> Self {
-        // We can't just check the length unless we add an ExactSizeIterator bound :(
-        let mut length: u8 = 0;
-
-        let mut set: PetitSet<InputButton, 8> = PetitSet::default();
-        for button in buttons {
-            length += 1;
-            set.insert(button.into());
-        }
-
-        match length {
-            1 => UserInput::Single(set.into_iter().next().unwrap()),
-            _ => UserInput::Chord(set),
-        }
-    }
-
-    /// Which [`InputMode`]s does this input contain?
-    pub fn input_modes(&self) -> PetitSet<InputMode, 3> {
-        let mut set = PetitSet::default();
-        match self {
-            UserInput::Single(button) => {
-                set.insert((*button).into());
-            }
-            UserInput::Chord(buttons) => {
-                for &button in buttons.iter() {
-                    set.insert(button.into());
-                }
-            }
-        }
-        set
-    }
-
-    /// Does this [`UserInput`] match the provided [`InputMode`]?
-    ///
-    /// For [`UserInput::Chord`], this will be true if any of the buttons in the combination match.
-    pub fn matches_input_mode(&self, input_mode: InputMode) -> bool {
-        // This is slightly faster than using Self::input_modes
-        // As we can return early
-        match self {
-            UserInput::Single(button) => {
-                let button_mode: InputMode = (*button).into();
-                button_mode == input_mode
-            }
-            UserInput::Chord(set) => {
-                for button in set.iter() {
-                    let button_mode: InputMode = (*button).into();
-                    if button_mode == input_mode {
-                        return true;
-                    }
-                }
-                false
-            }
-        }
-    }
-
-    /// The number of buttons in the [`UserInput`]
-    pub fn len(&self) -> usize {
-        match self {
-            UserInput::Single(_) => 1,
-            UserInput::Chord(button_set) => button_set.len(),
-        }
-    }
-
-    /// 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([LControl.into(), LAlt.into()]);
-    /// let a: UserInput  = A.into();
-    /// let ctrl_a = UserInput::chord([LControl, A]);
-    /// let ctrl_alt_a = UserInput::chord([LControl, LAlt, A]);
-    ///
-    /// 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<InputButton>) -> usize {
-        match self {
-            UserInput::Single(button) => {
-                if buttons.contains(button) {
-                    1
-                } else {
-                    0
-                }
-            }
-            UserInput::Chord(chord_buttons) => {
-                let mut n_matching = 0;
-                for button in buttons.iter() {
-                    if chord_buttons.contains(button) {
-                        n_matching += 1;
-                    }
-                }
-
-                n_matching
-            }
-        }
-    }
-
-    /// Returns the raw inputs that make up this [`UserInput`]
-    pub fn raw_inputs(&self) -> (Vec<GamepadButtonType>, Vec<KeyCode>, Vec<MouseButton>) {
-        let mut gamepad_buttons: Vec<GamepadButtonType> = Vec::default();
-        let mut keyboard_buttons: Vec<KeyCode> = Vec::default();
-        let mut mouse_buttons: Vec<MouseButton> = Vec::default();
-
-        match self {
-            UserInput::Single(button) => match *button {
-                InputButton::Gamepad(variant) => gamepad_buttons.push(variant),
-                InputButton::Keyboard(variant) => keyboard_buttons.push(variant),
-                InputButton::Mouse(variant) => mouse_buttons.push(variant),
-            },
-            UserInput::Chord(button_set) => {
-                for button in button_set.iter() {
-                    match button {
-                        InputButton::Gamepad(variant) => gamepad_buttons.push(*variant),
-                        InputButton::Keyboard(variant) => keyboard_buttons.push(*variant),
-                        InputButton::Mouse(variant) => mouse_buttons.push(*variant),
-                    }
-                }
-            }
-        };
-
-        (gamepad_buttons, keyboard_buttons, mouse_buttons)
-    }
-}
-
-impl From<InputButton> for UserInput {
-    fn from(input: InputButton) -> Self {
-        UserInput::Single(input)
-    }
-}
-
-impl From<GamepadButtonType> for UserInput {
-    fn from(input: GamepadButtonType) -> Self {
-        UserInput::Single(InputButton::Gamepad(input))
-    }
-}
-
-impl From<KeyCode> for UserInput {
-    fn from(input: KeyCode) -> Self {
-        UserInput::Single(InputButton::Keyboard(input))
-    }
-}
-
-impl From<MouseButton> for UserInput {
-    fn from(input: MouseButton) -> Self {
-        UserInput::Single(InputButton::Mouse(input))
-    }
-}
-
-/// A button-like input type
-///
-/// See [`Button`] for the value-ful equivalent.
-/// Use the [`From`] or [`Into`] traits to convert from a [`InputButton`] to a [`InputMode`].
-///
-/// Unfortunately we cannot use a trait object here, as the types used by `Input`
-/// require traits that are not object-safe.
-///
-/// Please contact the maintainers if you need support for another type!
-#[non_exhaustive]
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum InputMode {
-    /// A gamepad
-    Gamepad,
-    /// A keyboard
-    Keyboard,
-    /// A mouse
-    Mouse,
-}
-
-impl InputMode {
-    /// Iterates over the possible [`InputModes`](InputMode)
-    pub fn iter() -> InputModeIter {
-        InputModeIter::default()
-    }
-}
-
-/// An iterator of [`InputModes`](InputMode)
-///
-/// Created by calling [`InputMode::iter`]
-#[derive(Debug, Clone, Default)]
-pub struct InputModeIter {
-    cursor: u8,
-}
-
-impl Iterator for InputModeIter {
-    type Item = InputMode;
-
-    fn next(&mut self) -> Option<InputMode> {
-        let item = match self.cursor {
-            0 => Some(InputMode::Gamepad),
-            1 => Some(InputMode::Keyboard),
-            2 => Some(InputMode::Mouse),
-            _ => None,
-        };
-        if self.cursor <= 2 {
-            self.cursor += 1;
-        }
-
-        item
-    }
-}
-
-impl From<InputButton> for InputMode {
-    fn from(button: InputButton) -> Self {
-        match button {
-            InputButton::Gamepad(_) => InputMode::Gamepad,
-            InputButton::Keyboard(_) => InputMode::Keyboard,
-            InputButton::Mouse(_) => InputMode::Mouse,
-        }
-    }
-}
-
-/// The values of a button-like input type
-///
-/// See [`InputMode`] for the value-less equivalent. Commonly stored in the [`UserInput`] enum.
-///
-/// Unfortunately we cannot use a trait object here, as the types used by `Input`
-/// require traits that are not object-safe.
-///
-/// Please contact the maintainers if you need support for another type!
-#[non_exhaustive]
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub enum InputButton {
-    /// A button on a gamepad
-    Gamepad(GamepadButtonType),
-    /// A button on a keyboard
-    Keyboard(KeyCode),
-    /// A button on a mouse
-    Mouse(MouseButton),
-}
-
-impl From<GamepadButtonType> for InputButton {
-    fn from(input: GamepadButtonType) -> Self {
-        InputButton::Gamepad(input)
-    }
-}
-
-impl From<KeyCode> for InputButton {
-    fn from(input: KeyCode) -> Self {
-        InputButton::Keyboard(input)
-    }
-}
-
-impl From<MouseButton> for InputButton {
-    fn from(input: MouseButton) -> Self {
-        InputButton::Mouse(input)
-    }
-}
-
-/// A collection of [`Input`] structs, which can be used to update an [`InputMap`](crate::input_map::InputMap).
-///
-/// Each of these streams is optional; if a stream does not exist, it is treated as if it were entirely unpressed.
-///
-/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources.
-#[derive(Debug, Clone)]
-pub struct InputStreams<'a> {
-    /// An optional [`GamepadButton`] [`Input`] stream
-    pub gamepad: Option<&'a Input<GamepadButton>>,
-    /// An optional [`KeyCode`] [`Input`] stream
-    pub keyboard: Option<&'a Input<KeyCode>>,
-    /// An optional [`MouseButton`] [`Input`] stream
-    pub mouse: Option<&'a Input<MouseButton>>,
-    /// The [`Gamepad`] that this struct will detect inputs from
-    pub associated_gamepad: Option<Gamepad>,
-}
-
-// Constructors
-impl<'a> InputStreams<'a> {
-    /// Construct [`InputStreams`] with only a [`GamepadButton`] input stream
-    pub fn from_gamepad(
-        gamepad_input_stream: &'a Input<GamepadButton>,
-        associated_gamepad: Gamepad,
-    ) -> Self {
-        Self {
-            gamepad: Some(gamepad_input_stream),
-            keyboard: None,
-            mouse: None,
-            associated_gamepad: Some(associated_gamepad),
-        }
-    }
-
-    /// Construct [`InputStreams`] with only a [`KeyCode`] input stream
-    pub fn from_keyboard(keyboard_input_stream: &'a Input<KeyCode>) -> Self {
-        Self {
-            gamepad: None,
-            keyboard: Some(keyboard_input_stream),
-            mouse: None,
-            associated_gamepad: None,
-        }
-    }
-
-    /// Construct [`InputStreams`] with only a [`GamepadButton`] input stream
-    pub fn from_mouse(mouse_input_stream: &'a Input<MouseButton>) -> Self {
-        Self {
-            gamepad: None,
-            keyboard: None,
-            mouse: Some(mouse_input_stream),
-            associated_gamepad: None,
-        }
-    }
-}
-
-// Input checking
-impl<'a> InputStreams<'a> {
-    /// Is the `input` matched by the [`InputStreams`]?
-    pub fn input_pressed(&self, input: &UserInput) -> bool {
-        match input {
-            UserInput::Single(button) => self.button_pressed(*button),
-            UserInput::Chord(buttons) => self.all_buttons_pressed(buttons),
-        }
-    }
-
-    /// Is at least one of the `inputs` pressed?
-    #[must_use]
-    pub fn any_pressed(&self, inputs: &PetitSet<UserInput, 16>) -> bool {
-        for input in inputs.iter() {
-            if self.input_pressed(input) {
-                return true;
-            }
-        }
-        // If none of the inputs matched, return false
-        false
-    }
-
-    /// Is the `button` pressed?
-    #[must_use]
-    pub fn button_pressed(&self, button: InputButton) -> bool {
-        match button {
-            InputButton::Gamepad(gamepad_button) => {
-                // If no gamepad is registered, we know for sure that no match was found
-                if let Some(gamepad) = self.associated_gamepad {
-                    if let Some(gamepad_stream) = self.gamepad {
-                        gamepad_stream.pressed(GamepadButton(gamepad, gamepad_button))
-                    } else {
-                        false
-                    }
-                } else {
-                    false
-                }
-            }
-            InputButton::Keyboard(keycode) => {
-                if let Some(keyboard_stream) = self.keyboard {
-                    keyboard_stream.pressed(keycode)
-                } else {
-                    false
-                }
-            }
-            InputButton::Mouse(mouse_button) => {
-                if let Some(mouse_stream) = self.mouse {
-                    mouse_stream.pressed(mouse_button)
-                } else {
-                    false
-                }
-            }
-        }
-    }
-
-    /// Are all of the `buttons` pressed?
-    #[must_use]
-    pub fn all_buttons_pressed(&self, buttons: &PetitSet<InputButton, 8>) -> bool {
-        for &button in buttons.iter() {
-            // If any of the appropriate inputs failed to match, the action is considered pressed
-            if !self.button_pressed(button) {
-                return false;
-            }
-        }
-        // If none of the inputs failed to match, return true
-        true
-    }
-}
-
-/// A mutable collection of [`Input`] structs, which can be used for mocking user inputs.
-///
-/// Each of these streams is optional; if a stream does not exist, inputs sent to them will be ignored.
-///
-/// These are typically collected via a system from the [`World`](bevy::prelude::World) as resources.
-#[derive(Debug)]
-pub struct MutableInputStreams<'a> {
-    /// An optional [`GamepadButton`] [`Input`] stream
-    pub gamepad: Option<&'a mut Input<GamepadButton>>,
-    /// An optional [`KeyCode`] [`Input`] stream
-    pub keyboard: Option<&'a mut Input<KeyCode>>,
-    /// An optional [`MouseButton`] [`Input`] stream
-    pub mouse: Option<&'a mut Input<MouseButton>>,
-    /// The [`Gamepad`] that this struct will detect inputs from
-    pub associated_gamepad: Option<Gamepad>,
-}
-
-impl<'a> From<MutableInputStreams<'a>> for InputStreams<'a> {
-    fn from(mutable_streams: MutableInputStreams<'a>) -> Self {
-        let gamepad = mutable_streams.gamepad.map(|mutable_ref| &*mutable_ref);
-
-        let keyboard = mutable_streams.keyboard.map(|mutable_ref| &*mutable_ref);
-
-        let mouse = mutable_streams.mouse.map(|mutable_ref| &*mutable_ref);
-
-        InputStreams {
-            gamepad,
-            keyboard,
-            mouse,
-            associated_gamepad: mutable_streams.associated_gamepad,
-        }
-    }
-}
+//! Helpful abstractions over user inputs of all sorts
+
+use bevy::input::{gamepad::GamepadButtonType, keyboard::KeyCode, mouse::MouseButton};
+
+use bevy::utils::HashSet;
+use petitset::PetitSet;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    axislike::{AxisType, DualAxis, SingleAxis, VirtualDPad},
+    buttonlike::{MouseMotionDirection, MouseWheelDirection},
+};
+
+/// Some combination of user input, which may cross [`Input`]-mode boundaries
+///
+/// Suitable for use in an [`InputMap`](crate::input_map::InputMap)
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum UserInput {
+    /// A single button
+    Single(InputKind),
+    /// A combination of buttons, pressed simultaneously
+    ///
+    /// Up to 8 (!!) buttons can be chorded together at once.
+    /// Chords are considered to belong to all of the [InputMode]s of their constituent buttons.
+    Chord(PetitSet<InputKind, 8>),
+    /// A virtual DPad that you can get an [`AxisPair`] from
+    VirtualDPad(VirtualDPad),
+}
+
+impl UserInput {
+    /// Creates a [`UserInput::Chord`] from an iterator of [`InputKind`]s
+    ///
+    /// If `inputs` has a length of 1, a [`UserInput::Single`] variant will be returned instead.
+    pub fn chord(inputs: impl IntoIterator<Item = impl Into<InputKind>>) -> Self {
+        // We can't just check the length unless we add an ExactSizeIterator bound :(
+        let mut length: u8 = 0;
+
+        let mut set: PetitSet<InputKind, 8> = PetitSet::default();
+        for button in inputs {
+            length += 1;
+            set.insert(button.into());
+        }
+
+        match length {
+            1 => UserInput::Single(set.into_iter().next().unwrap()),
+            _ => UserInput::Chord(set),
+        }
+    }
+
+    /// 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
+    pub fn len(&self) -> usize {
+        match self {
+            UserInput::Single(_) => 1,
+            UserInput::Chord(button_set) => button_set.len(),
+            UserInput::VirtualDPad { .. } => 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([LControl.into(), LAlt.into()]);
+    /// let a: UserInput  = A.into();
+    /// let ctrl_a = UserInput::chord([LControl, A]);
+    /// let ctrl_alt_a = UserInput::chord([LControl, LAlt, A]);
+    ///
+    /// 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<InputKind>) -> usize {
+        match self {
+            UserInput::Single(button) => {
+                if buttons.contains(button) {
+                    1
+                } else {
+                    0
+                }
+            }
+            UserInput::Chord(chord_buttons) => {
+                let mut n_matching = 0;
+                for button in buttons.iter() {
+                    if chord_buttons.contains(button) {
+                        n_matching += 1;
+                    }
+                }
+
+                n_matching
+            }
+            UserInput::VirtualDPad(VirtualDPad {
+                up,
+                down,
+                left,
+                right,
+            }) => {
+                let mut n_matching = 0;
+                for button in buttons.iter() {
+                    for dpad_button in [up, down, left, right] {
+                        if button == dpad_button {
+                            n_matching += 1;
+                        }
+                    }
+                }
+
+                n_matching
+            }
+        }
+    }
+
+    /// Returns the raw inputs that make up this [`UserInput`]
+    pub fn raw_inputs(&self) -> RawInputs {
+        let mut raw_inputs = RawInputs::default();
+
+        match self {
+            UserInput::Single(button) => match *button {
+                InputKind::DualAxis(dual_axis) => {
+                    raw_inputs
+                        .axis_data
+                        .push((dual_axis.x.axis_type, dual_axis.x.value));
+                    raw_inputs
+                        .axis_data
+                        .push((dual_axis.y.axis_type, dual_axis.y.value));
+                }
+                InputKind::SingleAxis(single_axis) => raw_inputs
+                    .axis_data
+                    .push((single_axis.axis_type, single_axis.value)),
+                InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button),
+                InputKind::Keyboard(button) => raw_inputs.keycodes.push(button),
+                InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button),
+                InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button),
+                InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button),
+            },
+            UserInput::Chord(button_set) => {
+                for button in button_set.iter() {
+                    match *button {
+                        InputKind::DualAxis(dual_axis) => {
+                            raw_inputs
+                                .axis_data
+                                .push((dual_axis.x.axis_type, dual_axis.x.value));
+                            raw_inputs
+                                .axis_data
+                                .push((dual_axis.y.axis_type, dual_axis.y.value));
+                        }
+                        InputKind::SingleAxis(single_axis) => raw_inputs
+                            .axis_data
+                            .push((single_axis.axis_type, single_axis.value)),
+                        InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button),
+                        InputKind::Keyboard(button) => raw_inputs.keycodes.push(button),
+                        InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button),
+                        InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button),
+                        InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button),
+                    }
+                }
+            }
+            UserInput::VirtualDPad(VirtualDPad {
+                up,
+                down,
+                left,
+                right,
+            }) => {
+                for button in [up, down, left, right] {
+                    match *button {
+                        InputKind::DualAxis(dual_axis) => {
+                            raw_inputs
+                                .axis_data
+                                .push((dual_axis.x.axis_type, dual_axis.x.value));
+                            raw_inputs
+                                .axis_data
+                                .push((dual_axis.y.axis_type, dual_axis.y.value));
+                        }
+                        InputKind::SingleAxis(single_axis) => raw_inputs
+                            .axis_data
+                            .push((single_axis.axis_type, single_axis.value)),
+                        InputKind::GamepadButton(button) => raw_inputs.gamepad_buttons.push(button),
+                        InputKind::Keyboard(button) => raw_inputs.keycodes.push(button),
+                        InputKind::Mouse(button) => raw_inputs.mouse_buttons.push(button),
+                        InputKind::MouseWheel(button) => raw_inputs.mouse_wheel.push(button),
+                        InputKind::MouseMotion(button) => raw_inputs.mouse_motion.push(button),
+                    }
+                }
+            }
+        };
+
+        raw_inputs
+    }
+}
+
+impl From<InputKind> for UserInput {
+    fn from(input: InputKind) -> Self {
+        UserInput::Single(input)
+    }
+}
+
+impl From<DualAxis> for UserInput {
+    fn from(input: DualAxis) -> Self {
+        UserInput::Single(InputKind::DualAxis(input))
+    }
+}
+
+impl From<SingleAxis> for UserInput {
+    fn from(input: SingleAxis) -> Self {
+        UserInput::Single(InputKind::SingleAxis(input))
+    }
+}
+
+impl From<VirtualDPad> for UserInput {
+    fn from(input: VirtualDPad) -> Self {
+        UserInput::VirtualDPad(input)
+    }
+}
+
+impl From<GamepadButtonType> for UserInput {
+    fn from(input: GamepadButtonType) -> Self {
+        UserInput::Single(InputKind::GamepadButton(input))
+    }
+}
+
+impl From<KeyCode> for UserInput {
+    fn from(input: KeyCode) -> Self {
+        UserInput::Single(InputKind::Keyboard(input))
+    }
+}
+
+impl From<MouseButton> for UserInput {
+    fn from(input: MouseButton) -> Self {
+        UserInput::Single(InputKind::Mouse(input))
+    }
+}
+
+impl From<MouseWheelDirection> for UserInput {
+    fn from(input: MouseWheelDirection) -> Self {
+        UserInput::Single(InputKind::MouseWheel(input))
+    }
+}
+
+impl From<MouseMotionDirection> for UserInput {
+    fn from(input: MouseMotionDirection) -> Self {
+        UserInput::Single(InputKind::MouseMotion(input))
+    }
+}
+
+/// The different kinds of supported input bindings.
+///
+/// See [`InputMode`] for the value-less equivalent. Commonly stored in the [`UserInput`] enum.
+///
+/// Unfortunately we cannot use a trait object here, as the types used by `Input`
+/// require traits that are not object-safe.
+///
+/// Please contact the maintainers if you need support for another type!
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum InputKind {
+    /// A button on a gamepad
+    GamepadButton(GamepadButtonType),
+    /// A single axis of continous motion
+    SingleAxis(SingleAxis),
+    /// Two paired axes of continous motion
+    DualAxis(DualAxis),
+    /// A button on a keyboard
+    Keyboard(KeyCode),
+    /// A button on a mouse
+    Mouse(MouseButton),
+    /// A discretized mousewheel movement
+    MouseWheel(MouseWheelDirection),
+    /// A discretized mouse movement
+    MouseMotion(MouseMotionDirection),
+}
+
+impl From<DualAxis> for InputKind {
+    fn from(input: DualAxis) -> Self {
+        InputKind::DualAxis(input)
+    }
+}
+
+impl From<SingleAxis> for InputKind {
+    fn from(input: SingleAxis) -> Self {
+        InputKind::SingleAxis(input)
+    }
+}
+
+impl From<GamepadButtonType> for InputKind {
+    fn from(input: GamepadButtonType) -> Self {
+        InputKind::GamepadButton(input)
+    }
+}
+
+impl From<KeyCode> for InputKind {
+    fn from(input: KeyCode) -> Self {
+        InputKind::Keyboard(input)
+    }
+}
+
+impl From<MouseButton> for InputKind {
+    fn from(input: MouseButton) -> Self {
+        InputKind::Mouse(input)
+    }
+}
+
+impl From<MouseWheelDirection> for InputKind {
+    fn from(input: MouseWheelDirection) -> Self {
+        InputKind::MouseWheel(input)
+    }
+}
+
+impl From<MouseMotionDirection> for InputKind {
+    fn from(input: MouseMotionDirection) -> Self {
+        InputKind::MouseMotion(input)
+    }
+}
+
+/// The basic input events that make up a [`UserInput`].
+///
+/// Obtained by calling [`UserInput::raw_inputs()`].
+#[derive(Default, Debug, Clone, PartialEq)]
+pub struct RawInputs {
+    /// Physical keyboard buttons
+    pub keycodes: Vec<KeyCode>,
+    /// Mouse buttons
+    pub mouse_buttons: Vec<MouseButton>,
+    /// Discretized mouse wheel inputs
+    pub mouse_wheel: Vec<MouseWheelDirection>,
+    /// Discretized mouse motion inputs
+    pub mouse_motion: Vec<MouseMotionDirection>,
+    /// Gamepad buttons, independent of a [`Gamepad`](bevy::input::gamepad::Gamepad)
+    pub gamepad_buttons: Vec<GamepadButtonType>,
+    /// 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<f32>)>,
+}
+
+#[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.x.value),
+                (axis.y.axis_type, axis.y.value),
+            ],
+            ..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::symmetric(GamepadAxisType::LeftZ, 0.)),
+        ]);
+
+        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);
+            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);
+            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::A;
+            let expected = RawInputs::from_keycode(button);
+            let raw = UserInput::from(button).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);
+            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);
+            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);
+            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);
+            let raw = UserInput::from(direction).raw_inputs();
+            assert_eq!(expected, raw)
+        }
+    }
+}


Test patch:
diff --git a/tests/clashes.rs b/tests/clashes.rs
index 428d6fc..62e35ad 100644
--- a/tests/clashes.rs
+++ b/tests/clashes.rs
@@ -1,8 +1,19 @@
+use bevy::ecs::system::SystemState;
+use bevy::input::InputPlugin;
 use bevy::prelude::*;
-use bevy_ecs::system::SystemState;
-use bevy_utils::HashSet;
+use bevy::utils::HashSet;
+use leafwing_input_manager::input_streams::InputStreams;
 use leafwing_input_manager::prelude::*;
-use leafwing_input_manager::user_input::InputStreams;
+
+fn test_app() -> App {
+    let mut app = App::new();
+
+    app.add_plugins(MinimalPlugins)
+        .add_plugin(InputPlugin)
+        .add_plugin(InputManagerPlugin::<Action>::default())
+        .add_startup_system(spawn_input_map);
+    app
+}
 
 #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug)]
 enum Action {
@@ -53,26 +64,23 @@ impl ClashTestExt for App {
     ) {
         let pressed_actions: HashSet<Action> = HashSet::from_iter(pressed_actions.into_iter());
         // SystemState is love, SystemState is life
-        let mut input_system_state: SystemState<(Query<&InputMap<Action>>, Res<Input<KeyCode>>)> =
+        let mut input_system_state: SystemState<Query<&InputMap<Action>>> =
             SystemState::new(&mut self.world);
 
-        let (input_map_query, keyboard) = input_system_state.get(&self.world);
-
-        let input_streams = InputStreams::from_keyboard(&*keyboard);
+        let input_map_query = input_system_state.get(&self.world);
 
         let input_map = input_map_query.single();
-
-        let keyboard_input = input_streams.keyboard.unwrap();
+        let keyboard_input = self.world.resource::<Input<KeyCode>>();
 
         for action in Action::variants() {
             if pressed_actions.contains(&action) {
                 assert!(
-                    input_map.pressed(action, &input_streams, clash_strategy),
+                    input_map.pressed(action, &InputStreams::from_world(&self.world, None), clash_strategy),
                     "{action:?} was incorrectly not pressed for {clash_strategy:?} when `Input<KeyCode>` was \n {keyboard_input:?}."
                 );
             } else {
                 assert!(
-                    !input_map.pressed(action, &input_streams, clash_strategy),
+                    !input_map.pressed(action, &InputStreams::from_world(&self.world, None), clash_strategy),
                     "{action:?} was incorrectly pressed for {clash_strategy:?} when `Input<KeyCode>` was \n {keyboard_input:?}"
                 );
             }
@@ -81,18 +89,11 @@ impl ClashTestExt for App {
 }
 
 #[test]
-fn input_clash_handling() {
-    use bevy_input::InputPlugin;
-    use leafwing_input_manager::MockInput;
+fn two_inputs_clash_handling() {
     use Action::*;
     use KeyCode::*;
 
-    let mut app = App::new();
-
-    app.add_plugins(MinimalPlugins)
-        .add_plugin(InputPlugin)
-        .add_plugin(InputManagerPlugin::<Action>::default())
-        .add_startup_system(spawn_input_map);
+    let mut app = test_app();
 
     // Two inputs
     app.send_input(Key1);
@@ -102,6 +103,14 @@ fn input_clash_handling() {
     app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, Two, OneAndTwo]);
     app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwo]);
     app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]);
+}
+
+#[test]
+fn three_inputs_clash_handling() {
+    use Action::*;
+    use KeyCode::*;
+
+    let mut app = test_app();
 
     // Three inputs
     app.reset_inputs();
@@ -116,6 +125,14 @@ fn input_clash_handling() {
     );
     app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [OneAndTwoAndThree]);
     app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]);
+}
+
+#[test]
+fn modifier_clash_handling() {
+    use Action::*;
+    use KeyCode::*;
+
+    let mut app = test_app();
 
     // Modifier
     app.reset_inputs();
@@ -134,6 +151,14 @@ fn input_clash_handling() {
         [CtrlOne, OneAndTwoAndThree],
     );
     app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One, Two]);
+}
+
+#[test]
+fn multiple_modifiers_clash_handling() {
+    use Action::*;
+    use KeyCode::*;
+
+    let mut app = test_app();
 
     // Multiple modifiers
     app.reset_inputs();
@@ -145,6 +170,14 @@ fn input_clash_handling() {
     app.assert_input_map_actions_eq(ClashStrategy::PressAll, [One, CtrlOne, AltOne, CtrlAltOne]);
     app.assert_input_map_actions_eq(ClashStrategy::PrioritizeLongest, [CtrlAltOne]);
     app.assert_input_map_actions_eq(ClashStrategy::UseActionOrder, [One]);
+}
+
+#[test]
+fn action_order_clash_handling() {
+    use Action::*;
+    use KeyCode::*;
+
+    let mut app = test_app();
 
     // Action order
     app.reset_inputs();
diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs
new file mode 100644
index 0000000..9b2e0d5
--- /dev/null
+++ b/tests/gamepad_axis.rs
@@ -0,0 +1,246 @@
+use bevy::input::gamepad::GamepadEventRaw;
+use bevy::input::InputPlugin;
+use bevy::prelude::*;
+use leafwing_input_manager::axislike::{AxisType, DualAxisData};
+use leafwing_input_manager::prelude::*;
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum ButtonlikeTestAction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum AxislikeTestAction {
+    X,
+    Y,
+    XY,
+}
+
+fn test_app() -> App {
+    let mut app = App::new();
+    app.add_plugins(MinimalPlugins)
+        .add_plugin(InputPlugin)
+        .add_plugin(InputManagerPlugin::<ButtonlikeTestAction>::default())
+        .add_plugin(InputManagerPlugin::<AxislikeTestAction>::default())
+        .init_resource::<ActionState<ButtonlikeTestAction>>()
+        .init_resource::<ActionState<AxislikeTestAction>>();
+
+    // WARNING: you MUST register your gamepad during tests, or all gamepad input mocking will fail
+    let mut gamepad_events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    gamepad_events.send(GamepadEventRaw {
+        // This MUST be consistent with any other mocked events
+        gamepad: Gamepad { id: 1 },
+        event_type: GamepadEventType::Connected,
+    });
+
+    // Ensure that the gamepad is picked up by the appropriate system
+    app.update();
+    // Ensure that the connection event is flushed through
+    app.update();
+
+    app
+}
+
+#[test]
+fn raw_gamepad_axis_events() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        SingleAxis::symmetric(GamepadAxisType::RightStickX, 0.1),
+        ButtonlikeTestAction::Up,
+    )]));
+
+    let mut events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    events.send(GamepadEventRaw {
+        gamepad: Gamepad { id: 1 },
+        event_type: GamepadEventType::AxisChanged(GamepadAxisType::RightStickX, 1.0),
+    });
+
+    app.update();
+    let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+    assert!(action_state.pressed(ButtonlikeTestAction::Up));
+}
+
+#[test]
+fn game_pad_single_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    assert_eq!(events.drain().count(), 0);
+
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    assert_eq!(events.drain().count(), 1);
+}
+
+#[test]
+fn game_pad_dual_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    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,
+        },
+        y: SingleAxis {
+            axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY),
+            value: Some(0.),
+            positive_low: 0.0,
+            negative_low: 0.0,
+        },
+    };
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<GamepadEventRaw>>();
+    // Dual axis events are split out
+    assert_eq!(events.drain().count(), 2);
+}
+
+#[test]
+fn game_pad_single_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (
+            SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.1),
+            AxislikeTestAction::X,
+        ),
+        (
+            SingleAxis::symmetric(GamepadAxisType::LeftStickY, 0.1),
+            AxislikeTestAction::Y,
+        ),
+    ]));
+
+    // +X
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // -X
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickX),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // +Y
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // -Y
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // 0
+    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,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+
+    // None
+    let input = SingleAxis {
+        axis_type: AxisType::Gamepad(GamepadAxisType::LeftStickY),
+        value: None,
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+}
+
+#[test]
+fn game_pad_dual_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        DualAxis::left_stick(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(DualAxis::from_value(
+        GamepadAxisType::LeftStickX,
+        GamepadAxisType::LeftStickY,
+        0.8,
+        0.0,
+    ));
+
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    assert_eq!(action_state.value(AxislikeTestAction::XY), 0.8);
+    assert_eq!(
+        action_state.axis_pair(AxislikeTestAction::XY).unwrap(),
+        DualAxisData::new(0.8, 0.0)
+    );
+}
+
+#[test]
+fn game_pad_virtualdpad() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        VirtualDPad::dpad(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(GamepadButtonType::DPadLeft);
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    // This should be 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 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 4ed451d..2e83441 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -1,8 +1,7 @@
 #![cfg(test)]
+use bevy::ecs::query::ChangeTrackers;
 use bevy::prelude::*;
-use bevy_ecs::query::ChangeTrackers;
 use leafwing_input_manager::prelude::*;
-use leafwing_input_manager::MockInput;
 
 #[derive(Actionlike, Clone, Copy, Debug)]
 enum Action {
@@ -49,8 +48,8 @@ fn spawn_player(mut commands: Commands) {
 
 #[test]
 fn do_nothing() {
-    use bevy_input::InputPlugin;
-    use bevy_utils::Duration;
+    use bevy::input::InputPlugin;
+    use bevy::utils::Duration;
 
     let mut app = App::new();
 
@@ -77,8 +76,6 @@ fn do_nothing() {
         assert!(action_state.released(Action::PayRespects));
         assert!(!action_state.just_released(Action::PayRespects));
 
-        assert_eq!(action_state.reasons_pressed(Action::PayRespects).len(), 0);
-
         assert_eq!(action_state.instant_started(Action::PayRespects), t0);
         assert_eq!(
             action_state.previous_duration(Action::PayRespects),
@@ -93,7 +90,7 @@ fn do_nothing() {
 
 #[test]
 fn action_state_change_detection() {
-    use bevy_input::InputPlugin;
+    use bevy::input::InputPlugin;
 
     let mut app = App::new();
 
@@ -127,7 +124,7 @@ fn action_state_change_detection() {
 
 #[test]
 fn disable_input() {
-    use bevy_input::InputPlugin;
+    use bevy::input::InputPlugin;
 
     let mut app = App::new();
 
@@ -168,8 +165,8 @@ fn disable_input() {
 #[test]
 #[cfg(feature = "ui")]
 fn action_state_driver() {
-    use bevy_input::InputPlugin;
-    use bevy_ui::Interaction;
+    use bevy::input::InputPlugin;
+    use bevy::ui::Interaction;
 
     let mut app = App::new();
 
@@ -239,8 +236,8 @@ fn action_state_driver() {
 
 #[test]
 fn duration() {
-    use bevy_input::InputPlugin;
-    use bevy_utils::Duration;
+    use bevy::input::InputPlugin;
+    use bevy::utils::Duration;
 
     const RESPECTFUL_DURATION: Duration = Duration::from_millis(5);
 
diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs
new file mode 100644
index 0000000..499e2e5
--- /dev/null
+++ b/tests/mouse_motion.rs
@@ -0,0 +1,289 @@
+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::prelude::*;
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum ButtonlikeTestAction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum AxislikeTestAction {
+    X,
+    Y,
+    XY,
+}
+
+fn test_app() -> App {
+    let mut app = App::new();
+    app.add_plugins(MinimalPlugins)
+        .add_plugin(InputPlugin)
+        .add_plugin(InputManagerPlugin::<ButtonlikeTestAction>::default())
+        .add_plugin(InputManagerPlugin::<AxislikeTestAction>::default())
+        .init_resource::<ActionState<ButtonlikeTestAction>>()
+        .init_resource::<ActionState<AxislikeTestAction>>();
+
+    app
+}
+
+#[test]
+fn raw_mouse_motion_events() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        SingleAxis::from_value(AxisType::MouseMotion(MouseMotionAxisType::Y), 1.0),
+        AxislikeTestAction::X,
+    )]));
+
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    events.send(MouseMotion {
+        delta: Vec2::new(0.0, 1.0),
+    });
+
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+}
+
+#[test]
+fn mouse_motion_discrete_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    assert_eq!(events.drain().count(), 0);
+
+    app.send_input(MouseMotionDirection::Up);
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+
+    assert_eq!(events.drain().count(), 1);
+}
+
+#[test]
+fn mouse_motion_single_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    assert_eq!(events.drain().count(), 0);
+
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::X),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    assert_eq!(events.drain().count(), 1);
+}
+
+#[test]
+fn mouse_motion_dual_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    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,
+        },
+        y: SingleAxis {
+            axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
+            value: Some(0.),
+            positive_low: 0.0,
+            negative_low: 0.0,
+        },
+    };
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<MouseMotion>>();
+    // Dual axis events are split out
+    assert_eq!(events.drain().count(), 2);
+}
+
+#[test]
+fn mouse_motion_buttonlike() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (MouseMotionDirection::Up, ButtonlikeTestAction::Up),
+        (MouseMotionDirection::Down, ButtonlikeTestAction::Down),
+        (MouseMotionDirection::Left, ButtonlikeTestAction::Left),
+        (MouseMotionDirection::Right, ButtonlikeTestAction::Right),
+    ]));
+
+    for action in ButtonlikeTestAction::variants() {
+        let input_map = app.world.resource::<InputMap<ButtonlikeTestAction>>();
+        // Get the first associated input
+        let input = input_map.get(action).get_at(0).unwrap().clone();
+
+        app.send_input(input.clone());
+        app.update();
+
+        let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+        assert!(action_state.pressed(action), "failed for {input:?}");
+    }
+}
+
+#[test]
+fn mouse_motion_buttonlike_cancels() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (MouseMotionDirection::Up, ButtonlikeTestAction::Up),
+        (MouseMotionDirection::Down, ButtonlikeTestAction::Down),
+        (MouseMotionDirection::Left, ButtonlikeTestAction::Left),
+        (MouseMotionDirection::Right, ButtonlikeTestAction::Right),
+    ]));
+
+    app.send_input(MouseMotionDirection::Up);
+    app.send_input(MouseMotionDirection::Down);
+
+    // Correctly flushes the world
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+
+    assert!(!action_state.pressed(ButtonlikeTestAction::Up));
+    assert!(!action_state.pressed(ButtonlikeTestAction::Down));
+}
+
+#[test]
+fn mouse_motion_single_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (SingleAxis::mouse_motion_x(), AxislikeTestAction::X),
+        (SingleAxis::mouse_motion_y(), AxislikeTestAction::Y),
+    ]));
+
+    // +X
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::X),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // -X
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::X),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // +Y
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // -Y
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // 0
+    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,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+
+    // None
+    let input = SingleAxis {
+        axis_type: AxisType::MouseMotion(MouseMotionAxisType::Y),
+        value: None,
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+}
+
+#[test]
+fn mouse_motion_dual_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        DualAxis::mouse_motion(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(DualAxis::from_value(
+        MouseMotionAxisType::X,
+        MouseMotionAxisType::Y,
+        5.0,
+        0.0,
+    ));
+
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    assert_eq!(action_state.value(AxislikeTestAction::XY), 5.0);
+    assert_eq!(
+        action_state.axis_pair(AxislikeTestAction::XY).unwrap(),
+        DualAxisData::new(5.0, 0.0)
+    );
+}
+
+#[test]
+fn mouse_motion_virtualdpad() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        VirtualDPad::mouse_motion(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(DualAxis::from_value(
+        MouseMotionAxisType::X,
+        MouseMotionAxisType::Y,
+        0.0,
+        -2.0,
+    ));
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    // This should be 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 unit length, because we're working with a VirtualDpad
+        DualAxisData::new(0.0, -1.0)
+    );
+}
diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs
new file mode 100644
index 0000000..40536b2
--- /dev/null
+++ b/tests/mouse_wheel.rs
@@ -0,0 +1,290 @@
+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::prelude::*;
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum ButtonlikeTestAction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+#[derive(Actionlike, Clone, Copy, Debug)]
+enum AxislikeTestAction {
+    X,
+    Y,
+    XY,
+}
+
+fn test_app() -> App {
+    let mut app = App::new();
+    app.add_plugins(MinimalPlugins)
+        .add_plugin(InputPlugin)
+        .add_plugin(InputManagerPlugin::<ButtonlikeTestAction>::default())
+        .add_plugin(InputManagerPlugin::<AxislikeTestAction>::default())
+        .init_resource::<ActionState<ButtonlikeTestAction>>()
+        .init_resource::<ActionState<AxislikeTestAction>>();
+
+    app
+}
+
+#[test]
+fn raw_mouse_wheel_events() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        MouseWheelDirection::Up,
+        ButtonlikeTestAction::Up,
+    )]));
+
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    events.send(MouseWheel {
+        unit: MouseScrollUnit::Pixel,
+        x: 0.0,
+        y: 10.0,
+    });
+
+    app.update();
+    let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+    assert!(action_state.pressed(ButtonlikeTestAction::Up));
+}
+
+#[test]
+fn mouse_wheel_discrete_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    assert_eq!(events.drain().count(), 0);
+
+    app.send_input(MouseWheelDirection::Up);
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+
+    assert_eq!(events.drain().count(), 1);
+}
+
+#[test]
+fn mouse_wheel_single_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    assert_eq!(events.drain().count(), 0);
+
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::X),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    assert_eq!(events.drain().count(), 1);
+}
+
+#[test]
+fn mouse_wheel_dual_axis_mocking() {
+    let mut app = test_app();
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    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,
+        },
+        y: SingleAxis {
+            axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
+            value: Some(0.),
+            positive_low: 0.0,
+            negative_low: 0.0,
+        },
+    };
+    app.send_input(input);
+    let mut events = app.world.resource_mut::<Events<MouseWheel>>();
+    // Dual axis events are split out
+    assert_eq!(events.drain().count(), 2);
+}
+
+#[test]
+fn mouse_wheel_buttonlike() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (MouseWheelDirection::Up, ButtonlikeTestAction::Up),
+        (MouseWheelDirection::Down, ButtonlikeTestAction::Down),
+        (MouseWheelDirection::Left, ButtonlikeTestAction::Left),
+        (MouseWheelDirection::Right, ButtonlikeTestAction::Right),
+    ]));
+
+    for action in ButtonlikeTestAction::variants() {
+        let input_map = app.world.resource::<InputMap<ButtonlikeTestAction>>();
+        // Get the first associated input
+        let input = input_map.get(action).get_at(0).unwrap().clone();
+
+        app.send_input(input.clone());
+        app.update();
+
+        let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+        assert!(action_state.pressed(action), "failed for {input:?}");
+    }
+}
+
+#[test]
+fn mouse_wheel_buttonlike_cancels() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (MouseWheelDirection::Up, ButtonlikeTestAction::Up),
+        (MouseWheelDirection::Down, ButtonlikeTestAction::Down),
+        (MouseWheelDirection::Left, ButtonlikeTestAction::Left),
+        (MouseWheelDirection::Right, ButtonlikeTestAction::Right),
+    ]));
+
+    app.send_input(MouseWheelDirection::Up);
+    app.send_input(MouseWheelDirection::Down);
+
+    // Correctly flushes the world
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<ButtonlikeTestAction>>();
+
+    assert!(!action_state.pressed(ButtonlikeTestAction::Up));
+    assert!(!action_state.pressed(ButtonlikeTestAction::Down));
+}
+
+#[test]
+fn mouse_wheel_single_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([
+        (SingleAxis::mouse_wheel_x(), AxislikeTestAction::X),
+        (SingleAxis::mouse_wheel_y(), AxislikeTestAction::Y),
+    ]));
+
+    // +X
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::X),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // -X
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::X),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::X));
+
+    // +Y
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
+        value: Some(1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // -Y
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
+        value: Some(-1.),
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(action_state.pressed(AxislikeTestAction::Y));
+
+    // 0
+    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,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+
+    // None
+    let input = SingleAxis {
+        axis_type: AxisType::MouseWheel(MouseWheelAxisType::Y),
+        value: None,
+        positive_low: 0.0,
+        negative_low: 0.0,
+    };
+    app.send_input(input);
+    app.update();
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+    assert!(!action_state.pressed(AxislikeTestAction::Y));
+}
+
+#[test]
+fn mouse_wheel_dual_axis() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        DualAxis::mouse_wheel(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(DualAxis::from_value(
+        MouseWheelAxisType::X,
+        MouseWheelAxisType::Y,
+        5.0,
+        0.0,
+    ));
+
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    assert_eq!(action_state.value(AxislikeTestAction::XY), 5.0);
+    assert_eq!(
+        action_state.axis_pair(AxislikeTestAction::XY).unwrap(),
+        DualAxisData::new(5.0, 0.0)
+    );
+}
+
+#[test]
+fn mouse_wheel_virtualdpad() {
+    let mut app = test_app();
+    app.insert_resource(InputMap::new([(
+        VirtualDPad::mouse_wheel(),
+        AxislikeTestAction::XY,
+    )]));
+
+    app.send_input(DualAxis::from_value(
+        MouseWheelAxisType::X,
+        MouseWheelAxisType::Y,
+        0.0,
+        -2.0,
+    ));
+    app.update();
+
+    let action_state = app.world.resource::<ActionState<AxislikeTestAction>>();
+
+    assert!(action_state.pressed(AxislikeTestAction::XY));
+    // This should be 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 unit length, because we're working with a VirtualDpad
+        DualAxisData::new(0.0, -1.0)
+    );
+}