leafwing_input_manager/user_input/
virtual_axial.rs

1//! This module contains [`VirtualAxis`], [`VirtualDPad`], and [`VirtualDPad3D`].
2
3use crate as leafwing_input_manager;
4use crate::clashing_inputs::BasicInputs;
5use crate::input_processing::{
6    AxisProcessor, DualAxisProcessor, WithAxisProcessingPipelineExt,
7    WithDualAxisProcessingPipelineExt,
8};
9use crate::prelude::updating::CentralInputStore;
10use crate::prelude::{Axislike, DualAxislike, TripleAxislike, UserInput};
11use crate::user_input::Buttonlike;
12use crate::InputControlKind;
13use bevy::math::{Vec2, Vec3};
14#[cfg(feature = "gamepad")]
15use bevy::prelude::GamepadButton;
16#[cfg(feature = "keyboard")]
17use bevy::prelude::KeyCode;
18use bevy::prelude::{Entity, Reflect, World};
19use leafwing_input_manager_macros::serde_typetag;
20use serde::{Deserialize, Serialize};
21
22/// A virtual single-axis control constructed from two [`Buttonlike`]s.
23/// One button represents the negative direction (left for the X-axis, down for the Y-axis),
24/// while the other represents the positive direction (right for the X-axis, up for the Y-axis).
25///
26/// # Value Processing
27///
28/// You can customize how the values are processed using a pipeline of processors.
29/// See [`WithAxisProcessingPipelineExt`] for details.
30///
31/// The raw value is determined based on the state of the associated buttons:
32/// - `-1.0` if only the negative button is currently pressed.
33/// - `1.0` if only the positive button is currently pressed.
34/// - `0.0` if neither button is pressed, or both are pressed simultaneously.
35///
36/// ```rust
37/// use bevy::prelude::*;
38/// use bevy::input::InputPlugin;
39/// use leafwing_input_manager::prelude::*;
40/// use leafwing_input_manager::user_input::testing_utils::FetchUserInput;
41/// use leafwing_input_manager::plugin::CentralInputStorePlugin;
42///
43/// let mut app = App::new();
44/// app.add_plugins((InputPlugin, CentralInputStorePlugin));
45///
46/// // Define a virtual Y-axis using arrow "up" and "down" keys
47/// let axis = VirtualAxis::vertical_arrow_keys();
48///
49/// // Pressing either key activates the input
50/// KeyCode::ArrowUp.press(app.world_mut());
51/// app.update();
52/// assert_eq!(app.read_axis_value(axis), 1.0);
53///
54/// // You can configure a processing pipeline (e.g., doubling the value)
55/// let doubled = VirtualAxis::vertical_arrow_keys().sensitivity(2.0);
56/// assert_eq!(app.read_axis_value(doubled), 2.0);
57/// ```
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
59#[must_use]
60pub struct VirtualAxis {
61    /// The button that represents the negative direction.
62    pub negative: Box<dyn Buttonlike>,
63
64    /// The button that represents the positive direction.
65    pub positive: Box<dyn Buttonlike>,
66
67    /// A processing pipeline that handles input values.
68    pub processors: Vec<AxisProcessor>,
69}
70
71impl VirtualAxis {
72    /// Creates a new [`VirtualAxis`] with two given [`Buttonlike`]s.
73    /// No processing is applied to raw data.
74    #[inline]
75    pub fn new(negative: impl Buttonlike, positive: impl Buttonlike) -> Self {
76        Self {
77            negative: Box::new(negative),
78            positive: Box::new(positive),
79            processors: Vec::new(),
80        }
81    }
82
83    /// The [`VirtualAxis`] using the vertical arrow key mappings.
84    ///
85    /// - [`KeyCode::ArrowDown`] for negative direction.
86    /// - [`KeyCode::ArrowUp`] for positive direction.
87    #[cfg(feature = "keyboard")]
88    #[inline]
89    pub fn vertical_arrow_keys() -> Self {
90        Self::new(KeyCode::ArrowDown, KeyCode::ArrowUp)
91    }
92
93    /// The [`VirtualAxis`] using the horizontal arrow key mappings.
94    ///
95    /// - [`KeyCode::ArrowLeft`] for negative direction.
96    /// - [`KeyCode::ArrowRight`] for positive direction.
97    #[cfg(feature = "keyboard")]
98    #[inline]
99    pub fn horizontal_arrow_keys() -> Self {
100        Self::new(KeyCode::ArrowLeft, KeyCode::ArrowRight)
101    }
102
103    /// The [`VirtualAxis`] using the common W/S key mappings.
104    ///
105    /// - [`KeyCode::KeyS`] for negative direction.
106    /// - [`KeyCode::KeyW`] for positive direction.
107    #[cfg(feature = "keyboard")]
108    #[inline]
109    pub fn ws() -> Self {
110        Self::new(KeyCode::KeyS, KeyCode::KeyW)
111    }
112
113    /// The [`VirtualAxis`] using the common A/D key mappings.
114    ///
115    /// - [`KeyCode::KeyA`] for negative direction.
116    /// - [`KeyCode::KeyD`] for positive direction.
117    #[cfg(feature = "keyboard")]
118    #[inline]
119    pub fn ad() -> Self {
120        Self::new(KeyCode::KeyA, KeyCode::KeyD)
121    }
122
123    /// The [`VirtualAxis`] using the vertical numpad key mappings.
124    ///
125    /// - [`KeyCode::Numpad2`] for negative direction.
126    /// - [`KeyCode::Numpad8`] for positive direction.
127    #[cfg(feature = "keyboard")]
128    #[inline]
129    pub fn vertical_numpad() -> Self {
130        Self::new(KeyCode::Numpad2, KeyCode::Numpad8)
131    }
132
133    /// The [`VirtualAxis`] using the horizontal numpad key mappings.
134    ///
135    /// - [`KeyCode::Numpad4`] for negative direction.
136    /// - [`KeyCode::Numpad6`] for positive direction.
137    #[cfg(feature = "keyboard")]
138    #[inline]
139    pub fn horizontal_numpad() -> Self {
140        Self::new(KeyCode::Numpad4, KeyCode::Numpad6)
141    }
142
143    /// The [`VirtualAxis`] using the horizontal D-Pad button mappings.
144    /// No processing is applied to raw data from the gamepad.
145    ///
146    /// - [`GamepadButton::DPadLeft`] for negative direction.
147    /// - [`GamepadButton::DPadRight`] for positive direction.
148    #[cfg(feature = "gamepad")]
149    #[inline]
150    pub fn dpad_x() -> Self {
151        Self::new(GamepadButton::DPadLeft, GamepadButton::DPadRight)
152    }
153
154    /// The [`VirtualAxis`] using the vertical D-Pad button mappings.
155    /// No processing is applied to raw data from the gamepad.
156    ///
157    /// - [`GamepadButton::DPadDown`] for negative direction.
158    /// - [`GamepadButton::DPadUp`] for positive direction.
159    #[cfg(feature = "gamepad")]
160    #[inline]
161    pub fn dpad_y() -> Self {
162        Self::new(GamepadButton::DPadDown, GamepadButton::DPadUp)
163    }
164
165    /// The [`VirtualAxis`] using the horizontal action pad button mappings.
166    /// No processing is applied to raw data from the gamepad.
167    ///
168    /// - [`GamepadButton::West`] for negative direction.
169    /// - [`GamepadButton::East`] for positive direction.
170    #[cfg(feature = "gamepad")]
171    #[inline]
172    pub fn action_pad_x() -> Self {
173        Self::new(GamepadButton::West, GamepadButton::East)
174    }
175
176    /// The [`VirtualAxis`] using the vertical action pad button mappings.
177    /// No processing is applied to raw data from the gamepad.
178    ///
179    /// - [`GamepadButton::South`] for negative direction.
180    /// - [`GamepadButton::North`] for positive direction.
181    #[cfg(feature = "gamepad")]
182    #[inline]
183    pub fn action_pad_y() -> Self {
184        Self::new(GamepadButton::South, GamepadButton::North)
185    }
186}
187
188impl UserInput for VirtualAxis {
189    /// [`VirtualAxis`] acts as a virtual axis input.
190    #[inline]
191    fn kind(&self) -> InputControlKind {
192        InputControlKind::Axis
193    }
194
195    /// [`VirtualAxis`] represents a compositions of two buttons.
196    #[inline]
197    fn decompose(&self) -> BasicInputs {
198        BasicInputs::Composite(vec![self.negative.clone(), self.positive.clone()])
199    }
200}
201
202#[serde_typetag]
203impl Axislike for VirtualAxis {
204    /// Retrieves the current value of this axis after processing by the associated processors.
205    #[inline]
206    fn value(&self, input_store: &CentralInputStore, gamepad: Entity) -> f32 {
207        let negative = self.negative.value(input_store, gamepad);
208        let positive = self.positive.value(input_store, gamepad);
209        let value = positive - negative;
210        self.processors
211            .iter()
212            .fold(value, |value, processor| processor.process(value))
213    }
214
215    /// Sets the value of corresponding button based on the given `value`.
216    ///
217    /// When `value` is non-zero, set its absolute value to the value of:
218    /// - the negative button if the `value` is negative;
219    /// - the positive button if the `value` is positive.
220    fn set_value_as_gamepad(&self, world: &mut World, value: f32, gamepad: Option<Entity>) {
221        if value < 0.0 {
222            self.negative
223                .set_value_as_gamepad(world, value.abs(), gamepad);
224        } else if value > 0.0 {
225            self.positive.set_value_as_gamepad(world, value, gamepad);
226        }
227    }
228}
229
230impl WithAxisProcessingPipelineExt for VirtualAxis {
231    #[inline]
232    fn reset_processing_pipeline(mut self) -> Self {
233        self.processors.clear();
234        self
235    }
236
237    #[inline]
238    fn replace_processing_pipeline(
239        mut self,
240        processors: impl IntoIterator<Item = AxisProcessor>,
241    ) -> Self {
242        self.processors = processors.into_iter().collect();
243        self
244    }
245
246    #[inline]
247    fn with_processor(mut self, processor: impl Into<AxisProcessor>) -> Self {
248        self.processors.push(processor.into());
249        self
250    }
251}
252
253/// A virtual dual-axis control constructed from four [`Buttonlike`]s.
254/// Each button represents a specific direction (up, down, left, right),
255/// functioning similarly to a directional pad (D-pad) on both X and Y axes,
256/// and offering intermediate diagonals by means of two-button combinations.
257///
258/// By default, it reads from **any connected gamepad**.
259/// Use the [`InputMap::set_gamepad`](crate::input_map::InputMap::set_gamepad) for specific ones.
260///
261/// # Value Processing
262///
263/// You can customize how the values are processed using a pipeline of processors.
264/// See [`WithDualAxisProcessingPipelineExt`] for details.
265///
266/// The raw axis values are determined based on the state of the associated buttons:
267/// - `-1.0` if only the negative button is currently pressed (Down/Left).
268/// - `1.0` if only the positive button is currently pressed (Up/Right).
269/// - `0.0` if neither button is pressed, or both are pressed simultaneously.
270///
271/// ```rust
272/// use bevy::prelude::*;
273/// use bevy::input::InputPlugin;
274/// use leafwing_input_manager::user_input::testing_utils::FetchUserInput;
275/// use leafwing_input_manager::prelude::*;
276/// use leafwing_input_manager::plugin::CentralInputStorePlugin;
277///
278/// let mut app = App::new();
279/// app.add_plugins((InputPlugin, CentralInputStorePlugin));
280///
281/// // Define a virtual D-pad using the WASD keys
282/// let input = VirtualDPad::wasd();
283///
284/// // Pressing the W key activates the corresponding axis
285/// KeyCode::KeyW.press(app.world_mut());
286/// app.update();
287/// assert_eq!(app.read_dual_axis_values(input), Vec2::new(0.0, 1.0));
288///
289/// // You can configure a processing pipeline (e.g., doubling the Y value)
290/// let doubled = VirtualDPad::wasd().sensitivity_y(2.0);
291/// assert_eq!(app.read_dual_axis_values(doubled), Vec2::new(0.0, 2.0));
292/// ```
293#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
294#[must_use]
295pub struct VirtualDPad {
296    /// The button for the upward direction.
297    pub up: Box<dyn Buttonlike>,
298
299    /// The button for the downward direction.
300    pub down: Box<dyn Buttonlike>,
301
302    /// The button for the leftward direction.
303    pub left: Box<dyn Buttonlike>,
304
305    /// The button for the rightward direction.
306    pub right: Box<dyn Buttonlike>,
307
308    /// A processing pipeline that handles input values.
309    pub processors: Vec<DualAxisProcessor>,
310}
311
312impl VirtualDPad {
313    /// Creates a new [`VirtualDPad`] with four given [`Buttonlike`]s.
314    /// Each button represents a specific direction (up, down, left, right).
315    #[inline]
316    pub fn new(
317        up: impl Buttonlike,
318        down: impl Buttonlike,
319        left: impl Buttonlike,
320        right: impl Buttonlike,
321    ) -> Self {
322        Self {
323            up: Box::new(up),
324            down: Box::new(down),
325            left: Box::new(left),
326            right: Box::new(right),
327            processors: Vec::new(),
328        }
329    }
330
331    /// The [`VirtualDPad`] using the common arrow key mappings.
332    ///
333    /// - [`KeyCode::ArrowUp`] for upward direction.
334    /// - [`KeyCode::ArrowDown`] for downward direction.
335    /// - [`KeyCode::ArrowLeft`] for leftward direction.
336    /// - [`KeyCode::ArrowRight`] for rightward direction.
337    #[cfg(feature = "keyboard")]
338    #[inline]
339    pub fn arrow_keys() -> Self {
340        Self::new(
341            KeyCode::ArrowUp,
342            KeyCode::ArrowDown,
343            KeyCode::ArrowLeft,
344            KeyCode::ArrowRight,
345        )
346    }
347
348    /// The [`VirtualDPad`] using the common WASD key mappings.
349    ///
350    /// - [`KeyCode::KeyW`] for upward direction.
351    /// - [`KeyCode::KeyS`] for downward direction.
352    /// - [`KeyCode::KeyA`] for leftward direction.
353    /// - [`KeyCode::KeyD`] for rightward direction.
354    #[cfg(feature = "keyboard")]
355    #[inline]
356    pub fn wasd() -> Self {
357        Self::new(KeyCode::KeyW, KeyCode::KeyS, KeyCode::KeyA, KeyCode::KeyD)
358    }
359
360    /// The [`VirtualDPad`] using the common numpad key mappings.
361    ///
362    /// - [`KeyCode::Numpad8`] for upward direction.
363    /// - [`KeyCode::Numpad2`] for downward direction.
364    /// - [`KeyCode::Numpad4`] for leftward direction.
365    /// - [`KeyCode::Numpad6`] for rightward direction.
366    #[cfg(feature = "keyboard")]
367    #[inline]
368    pub fn numpad() -> Self {
369        Self::new(
370            KeyCode::Numpad8,
371            KeyCode::Numpad2,
372            KeyCode::Numpad4,
373            KeyCode::Numpad6,
374        )
375    }
376
377    /// Creates a new [`VirtualDPad`] using the common D-Pad button mappings.
378    ///
379    /// - [`GamepadButton::DPadUp`] for upward direction.
380    /// - [`GamepadButton::DPadDown`] for downward direction.
381    /// - [`GamepadButton::DPadLeft`] for leftward direction.
382    /// - [`GamepadButton::DPadRight`] for rightward direction.
383    #[cfg(feature = "gamepad")]
384    #[inline]
385    pub fn dpad() -> Self {
386        Self::new(
387            GamepadButton::DPadUp,
388            GamepadButton::DPadDown,
389            GamepadButton::DPadLeft,
390            GamepadButton::DPadRight,
391        )
392    }
393
394    /// Creates a new [`VirtualDPad`] using the common action pad button mappings.
395    ///
396    /// - [`GamepadButton::North`] for upward direction.
397    /// - [`GamepadButton::South`] for downward direction.
398    /// - [`GamepadButton::West`] for leftward direction.
399    /// - [`GamepadButton::East`] for rightward direction.
400    #[cfg(feature = "gamepad")]
401    #[inline]
402    pub fn action_pad() -> Self {
403        Self::new(
404            GamepadButton::North,
405            GamepadButton::South,
406            GamepadButton::West,
407            GamepadButton::East,
408        )
409    }
410}
411
412impl UserInput for VirtualDPad {
413    /// [`VirtualDPad`] acts as a dual-axis input.
414    #[inline]
415    fn kind(&self) -> InputControlKind {
416        InputControlKind::DualAxis
417    }
418
419    /// Returns the four [`GamepadButton`]s used by this D-pad.
420    #[inline]
421    fn decompose(&self) -> BasicInputs {
422        BasicInputs::Composite(vec![
423            self.up.clone(),
424            self.down.clone(),
425            self.left.clone(),
426            self.right.clone(),
427        ])
428    }
429}
430
431#[serde_typetag]
432impl DualAxislike for VirtualDPad {
433    /// Retrieves the current X and Y values of this D-pad after processing by the associated processors.
434    #[inline]
435    fn axis_pair(&self, input_store: &CentralInputStore, gamepad: Entity) -> Vec2 {
436        let up = self.up.value(input_store, gamepad);
437        let down = self.down.value(input_store, gamepad);
438        let left = self.left.value(input_store, gamepad);
439        let right = self.right.value(input_store, gamepad);
440        let value = Vec2::new(right - left, up - down);
441        self.processors
442            .iter()
443            .fold(value, |value, processor| processor.process(value))
444    }
445
446    /// Sets the value of corresponding button on each axis based on the given `value`.
447    ///
448    /// When `value` along an axis is non-zero, set its absolute value to the value of:
449    /// - the negative button of the axis if the `value` is negative;
450    /// - the positive button of the axis if the `value` is positive.
451    fn set_axis_pair_as_gamepad(&self, world: &mut World, value: Vec2, gamepad: Option<Entity>) {
452        let Vec2 { x, y } = value;
453
454        if x < 0.0 {
455            self.left.set_value_as_gamepad(world, x.abs(), gamepad);
456        } else if x > 0.0 {
457            self.right.set_value_as_gamepad(world, x, gamepad);
458        }
459
460        if y < 0.0 {
461            self.down.set_value_as_gamepad(world, y.abs(), gamepad);
462        } else if y > 0.0 {
463            self.up.set_value_as_gamepad(world, y, gamepad);
464        }
465    }
466}
467
468impl WithDualAxisProcessingPipelineExt for VirtualDPad {
469    #[inline]
470    fn reset_processing_pipeline(mut self) -> Self {
471        self.processors.clear();
472        self
473    }
474
475    #[inline]
476    fn replace_processing_pipeline(
477        mut self,
478        processor: impl IntoIterator<Item = DualAxisProcessor>,
479    ) -> Self {
480        self.processors = processor.into_iter().collect();
481        self
482    }
483
484    #[inline]
485    fn with_processor(mut self, processor: impl Into<DualAxisProcessor>) -> Self {
486        self.processors.push(processor.into());
487        self
488    }
489}
490
491/// A virtual triple-axis control constructed from six [`Buttonlike`]s.
492/// Each button represents a specific direction (up, down, left, right, forward, backward),
493/// functioning similarly to a three-dimensional directional pad (D-pad) on all X, Y, and Z axes,
494/// and offering intermediate diagonals by means of two/three-key combinations.
495///
496/// The raw axis values are determined based on the state of the associated buttons:
497/// - `-1.0` if only the negative button is currently pressed (Down/Left/Forward).
498/// - `1.0` if only the positive button is currently pressed (Up/Right/Backward).
499/// - `0.0` if neither button is pressed, or both are pressed simultaneously.
500#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
501#[must_use]
502pub struct VirtualDPad3D {
503    /// The button for the upward direction.
504    pub up: Box<dyn Buttonlike>,
505
506    /// The button for the downward direction.
507    pub down: Box<dyn Buttonlike>,
508
509    /// The button for the leftward direction.
510    pub left: Box<dyn Buttonlike>,
511
512    /// The button for the rightward direction.
513    pub right: Box<dyn Buttonlike>,
514
515    /// The button for the forward direction.
516    pub forward: Box<dyn Buttonlike>,
517
518    /// The button for the backward direction.
519    pub backward: Box<dyn Buttonlike>,
520}
521
522impl VirtualDPad3D {
523    /// Creates a new [`VirtualDPad3D`] with six given [`Buttonlike`]s.
524    /// Each button represents a specific direction (up, down, left, right, forward, backward).
525    #[inline]
526    pub fn new(
527        up: impl Buttonlike,
528        down: impl Buttonlike,
529        left: impl Buttonlike,
530        right: impl Buttonlike,
531        forward: impl Buttonlike,
532        backward: impl Buttonlike,
533    ) -> Self {
534        Self {
535            up: Box::new(up),
536            down: Box::new(down),
537            left: Box::new(left),
538            right: Box::new(right),
539            forward: Box::new(forward),
540            backward: Box::new(backward),
541        }
542    }
543}
544
545impl UserInput for VirtualDPad3D {
546    /// [`VirtualDPad3D`] acts as a virtual triple-axis input.
547    #[inline]
548    fn kind(&self) -> InputControlKind {
549        InputControlKind::TripleAxis
550    }
551
552    /// [`VirtualDPad3D`] represents a compositions of six [`Buttonlike`]s.
553    #[inline]
554    fn decompose(&self) -> BasicInputs {
555        BasicInputs::Composite(vec![
556            self.up.clone(),
557            self.down.clone(),
558            self.left.clone(),
559            self.right.clone(),
560            self.forward.clone(),
561            self.backward.clone(),
562        ])
563    }
564}
565
566#[serde_typetag]
567impl TripleAxislike for VirtualDPad3D {
568    /// Retrieves the current X, Y, and Z values of this D-pad.
569    #[inline]
570    fn axis_triple(&self, input_store: &CentralInputStore, gamepad: Entity) -> Vec3 {
571        let up = self.up.value(input_store, gamepad);
572        let down = self.down.value(input_store, gamepad);
573        let left = self.left.value(input_store, gamepad);
574        let right = self.right.value(input_store, gamepad);
575        let forward = self.forward.value(input_store, gamepad);
576        let backward = self.backward.value(input_store, gamepad);
577        Vec3::new(right - left, up - down, backward - forward)
578    }
579
580    /// Sets the value of corresponding button on each axis based on the given `value`.
581    ///
582    /// When `value` along an axis is non-zero, set its absolute value to the value of:
583    /// - the negative button of the axis if the `value` is negative;
584    /// - the positive button of the axis if the `value` is positive.
585    fn set_axis_triple_as_gamepad(&self, world: &mut World, value: Vec3, gamepad: Option<Entity>) {
586        let Vec3 { x, y, z } = value;
587
588        if x < 0.0 {
589            self.left.set_value_as_gamepad(world, x.abs(), gamepad);
590        } else if x > 0.0 {
591            self.right.set_value_as_gamepad(world, x, gamepad);
592        }
593
594        if y < 0.0 {
595            self.down.set_value_as_gamepad(world, y.abs(), gamepad);
596        } else if y > 0.0 {
597            self.up.set_value_as_gamepad(world, y, gamepad);
598        }
599
600        if z < 0.0 {
601            self.forward.set_value_as_gamepad(world, z.abs(), gamepad);
602        } else if z > 0.0 {
603            self.backward.set_value_as_gamepad(world, z, gamepad);
604        }
605    }
606}
607
608#[cfg(feature = "keyboard")]
609#[cfg(test)]
610mod tests {
611    use bevy::input::InputPlugin;
612    use bevy::prelude::*;
613
614    use crate::plugin::CentralInputStorePlugin;
615    use crate::prelude::updating::CentralInputStore;
616    use crate::prelude::*;
617
618    fn test_app() -> App {
619        let mut app = App::new();
620        app.add_plugins(InputPlugin)
621            .add_plugins(CentralInputStorePlugin);
622        app
623    }
624
625    #[test]
626    fn test_virtual() {
627        let x = VirtualAxis::horizontal_arrow_keys();
628        let xy = VirtualDPad::arrow_keys();
629        let xyz = VirtualDPad3D::new(
630            KeyCode::ArrowUp,
631            KeyCode::ArrowDown,
632            KeyCode::ArrowLeft,
633            KeyCode::ArrowRight,
634            KeyCode::KeyF,
635            KeyCode::KeyB,
636        );
637
638        // No inputs
639        let mut app = test_app();
640        app.update();
641        let inputs = app.world().resource::<CentralInputStore>();
642
643        let gamepad = Entity::PLACEHOLDER;
644
645        assert_eq!(x.value(inputs, gamepad), 0.0);
646        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::ZERO);
647        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::ZERO);
648
649        // Press arrow left
650        let mut app = test_app();
651        KeyCode::ArrowLeft.press(app.world_mut());
652        app.update();
653        let inputs = app.world().resource::<CentralInputStore>();
654
655        assert_eq!(x.value(inputs, gamepad), -1.0);
656        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(-1.0, 0.0));
657        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(-1.0, 0.0, 0.0));
658
659        // Press arrow up
660        let mut app = test_app();
661        KeyCode::ArrowUp.press(app.world_mut());
662        app.update();
663        let inputs = app.world().resource::<CentralInputStore>();
664
665        assert_eq!(x.value(inputs, gamepad), 0.0);
666        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(0.0, 1.0));
667        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(0.0, 1.0, 0.0));
668
669        // Press arrow right
670        let mut app = test_app();
671        KeyCode::ArrowRight.press(app.world_mut());
672        app.update();
673        let inputs = app.world().resource::<CentralInputStore>();
674
675        assert_eq!(x.value(inputs, gamepad), 1.0);
676        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(1.0, 0.0));
677        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(1.0, 0.0, 0.0));
678
679        // Press key B
680        let mut app = test_app();
681        KeyCode::KeyB.press(app.world_mut());
682        app.update();
683        let inputs = app.world().resource::<CentralInputStore>();
684
685        assert_eq!(x.value(inputs, gamepad), 0.0);
686        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(0.0, 0.0));
687        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(0.0, 0.0, 1.0));
688    }
689}