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 get_value(&self, input_store: &CentralInputStore, gamepad: Entity) -> Option<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        Some(
211            self.processors
212                .iter()
213                .fold(value, |value, processor| processor.process(value)),
214        )
215    }
216
217    /// Sets the value of corresponding button based on the given `value`.
218    ///
219    /// When `value` is non-zero, set its absolute value to the value of:
220    /// - the negative button if the `value` is negative;
221    /// - the positive button if the `value` is positive.
222    fn set_value_as_gamepad(&self, world: &mut World, value: f32, gamepad: Option<Entity>) {
223        if value < 0.0 {
224            self.negative
225                .set_value_as_gamepad(world, value.abs(), gamepad);
226        } else if value > 0.0 {
227            self.positive.set_value_as_gamepad(world, value, gamepad);
228        }
229    }
230}
231
232impl WithAxisProcessingPipelineExt for VirtualAxis {
233    #[inline]
234    fn reset_processing_pipeline(mut self) -> Self {
235        self.processors.clear();
236        self
237    }
238
239    #[inline]
240    fn replace_processing_pipeline(
241        mut self,
242        processors: impl IntoIterator<Item = AxisProcessor>,
243    ) -> Self {
244        self.processors = processors.into_iter().collect();
245        self
246    }
247
248    #[inline]
249    fn with_processor(mut self, processor: impl Into<AxisProcessor>) -> Self {
250        self.processors.push(processor.into());
251        self
252    }
253}
254
255/// A virtual dual-axis control constructed from four [`Buttonlike`]s.
256/// Each button represents a specific direction (up, down, left, right),
257/// functioning similarly to a directional pad (D-pad) on both X and Y axes,
258/// and offering intermediate diagonals by means of two-button combinations.
259///
260/// By default, it reads from **any connected gamepad**.
261/// Use the [`InputMap::set_gamepad`](crate::input_map::InputMap::set_gamepad) for specific ones.
262///
263/// # Value Processing
264///
265/// You can customize how the values are processed using a pipeline of processors.
266/// See [`WithDualAxisProcessingPipelineExt`] for details.
267///
268/// The raw axis values are determined based on the state of the associated buttons:
269/// - `-1.0` if only the negative button is currently pressed (Down/Left).
270/// - `1.0` if only the positive button is currently pressed (Up/Right).
271/// - `0.0` if neither button is pressed, or both are pressed simultaneously.
272///
273/// ```rust
274/// use bevy::prelude::*;
275/// use bevy::input::InputPlugin;
276/// use leafwing_input_manager::user_input::testing_utils::FetchUserInput;
277/// use leafwing_input_manager::prelude::*;
278/// use leafwing_input_manager::plugin::CentralInputStorePlugin;
279///
280/// let mut app = App::new();
281/// app.add_plugins((InputPlugin, CentralInputStorePlugin));
282///
283/// // Define a virtual D-pad using the WASD keys
284/// let input = VirtualDPad::wasd();
285///
286/// // Pressing the W key activates the corresponding axis
287/// KeyCode::KeyW.press(app.world_mut());
288/// app.update();
289/// assert_eq!(app.read_dual_axis_values(input), Vec2::new(0.0, 1.0));
290///
291/// // You can configure a processing pipeline (e.g., doubling the Y value)
292/// let doubled = VirtualDPad::wasd().sensitivity_y(2.0);
293/// assert_eq!(app.read_dual_axis_values(doubled), Vec2::new(0.0, 2.0));
294/// ```
295#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
296#[must_use]
297pub struct VirtualDPad {
298    /// The button for the upward direction.
299    pub up: Box<dyn Buttonlike>,
300
301    /// The button for the downward direction.
302    pub down: Box<dyn Buttonlike>,
303
304    /// The button for the leftward direction.
305    pub left: Box<dyn Buttonlike>,
306
307    /// The button for the rightward direction.
308    pub right: Box<dyn Buttonlike>,
309
310    /// A processing pipeline that handles input values.
311    pub processors: Vec<DualAxisProcessor>,
312}
313
314impl VirtualDPad {
315    /// Creates a new [`VirtualDPad`] with four given [`Buttonlike`]s.
316    /// Each button represents a specific direction (up, down, left, right).
317    #[inline]
318    pub fn new(
319        up: impl Buttonlike,
320        down: impl Buttonlike,
321        left: impl Buttonlike,
322        right: impl Buttonlike,
323    ) -> Self {
324        Self {
325            up: Box::new(up),
326            down: Box::new(down),
327            left: Box::new(left),
328            right: Box::new(right),
329            processors: Vec::new(),
330        }
331    }
332
333    /// The [`VirtualDPad`] using the common arrow key mappings.
334    ///
335    /// - [`KeyCode::ArrowUp`] for upward direction.
336    /// - [`KeyCode::ArrowDown`] for downward direction.
337    /// - [`KeyCode::ArrowLeft`] for leftward direction.
338    /// - [`KeyCode::ArrowRight`] for rightward direction.
339    #[cfg(feature = "keyboard")]
340    #[inline]
341    pub fn arrow_keys() -> Self {
342        Self::new(
343            KeyCode::ArrowUp,
344            KeyCode::ArrowDown,
345            KeyCode::ArrowLeft,
346            KeyCode::ArrowRight,
347        )
348    }
349
350    /// The [`VirtualDPad`] using the common WASD key mappings.
351    ///
352    /// - [`KeyCode::KeyW`] for upward direction.
353    /// - [`KeyCode::KeyS`] for downward direction.
354    /// - [`KeyCode::KeyA`] for leftward direction.
355    /// - [`KeyCode::KeyD`] for rightward direction.
356    #[cfg(feature = "keyboard")]
357    #[inline]
358    pub fn wasd() -> Self {
359        Self::new(KeyCode::KeyW, KeyCode::KeyS, KeyCode::KeyA, KeyCode::KeyD)
360    }
361
362    /// The [`VirtualDPad`] using the common numpad key mappings.
363    ///
364    /// - [`KeyCode::Numpad8`] for upward direction.
365    /// - [`KeyCode::Numpad2`] for downward direction.
366    /// - [`KeyCode::Numpad4`] for leftward direction.
367    /// - [`KeyCode::Numpad6`] for rightward direction.
368    #[cfg(feature = "keyboard")]
369    #[inline]
370    pub fn numpad() -> Self {
371        Self::new(
372            KeyCode::Numpad8,
373            KeyCode::Numpad2,
374            KeyCode::Numpad4,
375            KeyCode::Numpad6,
376        )
377    }
378
379    /// Creates a new [`VirtualDPad`] using the common D-Pad button mappings.
380    ///
381    /// - [`GamepadButton::DPadUp`] for upward direction.
382    /// - [`GamepadButton::DPadDown`] for downward direction.
383    /// - [`GamepadButton::DPadLeft`] for leftward direction.
384    /// - [`GamepadButton::DPadRight`] for rightward direction.
385    #[cfg(feature = "gamepad")]
386    #[inline]
387    pub fn dpad() -> Self {
388        Self::new(
389            GamepadButton::DPadUp,
390            GamepadButton::DPadDown,
391            GamepadButton::DPadLeft,
392            GamepadButton::DPadRight,
393        )
394    }
395
396    /// Creates a new [`VirtualDPad`] using the common action pad button mappings.
397    ///
398    /// - [`GamepadButton::North`] for upward direction.
399    /// - [`GamepadButton::South`] for downward direction.
400    /// - [`GamepadButton::West`] for leftward direction.
401    /// - [`GamepadButton::East`] for rightward direction.
402    #[cfg(feature = "gamepad")]
403    #[inline]
404    pub fn action_pad() -> Self {
405        Self::new(
406            GamepadButton::North,
407            GamepadButton::South,
408            GamepadButton::West,
409            GamepadButton::East,
410        )
411    }
412}
413
414impl UserInput for VirtualDPad {
415    /// [`VirtualDPad`] acts as a dual-axis input.
416    #[inline]
417    fn kind(&self) -> InputControlKind {
418        InputControlKind::DualAxis
419    }
420
421    /// Returns the four [`GamepadButton`]s used by this D-pad.
422    #[inline]
423    fn decompose(&self) -> BasicInputs {
424        BasicInputs::Composite(vec![
425            self.up.clone(),
426            self.down.clone(),
427            self.left.clone(),
428            self.right.clone(),
429        ])
430    }
431}
432
433#[serde_typetag]
434impl DualAxislike for VirtualDPad {
435    /// Retrieves the current X and Y values of this D-pad after processing by the associated processors.
436    #[inline]
437    fn get_axis_pair(&self, input_store: &CentralInputStore, gamepad: Entity) -> Option<Vec2> {
438        let up = self.up.get_value(input_store, gamepad);
439        let down = self.down.get_value(input_store, gamepad);
440        let left = self.left.get_value(input_store, gamepad);
441        let right = self.right.get_value(input_store, gamepad);
442
443        if up.is_none() && down.is_none() && left.is_none() && right.is_none() {
444            return None;
445        }
446
447        let up = up.unwrap_or(0.0);
448        let down = down.unwrap_or(0.0);
449        let left = left.unwrap_or(0.0);
450        let right = right.unwrap_or(0.0);
451
452        let value = Vec2::new(right - left, up - down);
453        Some(
454            self.processors
455                .iter()
456                .fold(value, |value, processor| processor.process(value)),
457        )
458    }
459
460    /// Sets the value of corresponding button on each axis based on the given `value`.
461    ///
462    /// When `value` along an axis is non-zero, set its absolute value to the value of:
463    /// - the negative button of the axis if the `value` is negative;
464    /// - the positive button of the axis if the `value` is positive.
465    fn set_axis_pair_as_gamepad(&self, world: &mut World, value: Vec2, gamepad: Option<Entity>) {
466        let Vec2 { x, y } = value;
467
468        if x < 0.0 {
469            self.left.set_value_as_gamepad(world, x.abs(), gamepad);
470        } else if x > 0.0 {
471            self.right.set_value_as_gamepad(world, x, gamepad);
472        }
473
474        if y < 0.0 {
475            self.down.set_value_as_gamepad(world, y.abs(), gamepad);
476        } else if y > 0.0 {
477            self.up.set_value_as_gamepad(world, y, gamepad);
478        }
479    }
480}
481
482impl WithDualAxisProcessingPipelineExt for VirtualDPad {
483    #[inline]
484    fn reset_processing_pipeline(mut self) -> Self {
485        self.processors.clear();
486        self
487    }
488
489    #[inline]
490    fn replace_processing_pipeline(
491        mut self,
492        processor: impl IntoIterator<Item = DualAxisProcessor>,
493    ) -> Self {
494        self.processors = processor.into_iter().collect();
495        self
496    }
497
498    #[inline]
499    fn with_processor(mut self, processor: impl Into<DualAxisProcessor>) -> Self {
500        self.processors.push(processor.into());
501        self
502    }
503}
504
505/// A virtual triple-axis control constructed from six [`Buttonlike`]s.
506/// Each button represents a specific direction (up, down, left, right, forward, backward),
507/// functioning similarly to a three-dimensional directional pad (D-pad) on all X, Y, and Z axes,
508/// and offering intermediate diagonals by means of two/three-key combinations.
509///
510/// The raw axis values are determined based on the state of the associated buttons:
511/// - `-1.0` if only the negative button is currently pressed (Down/Left/Forward).
512/// - `1.0` if only the positive button is currently pressed (Up/Right/Backward).
513/// - `0.0` if neither button is pressed, or both are pressed simultaneously.
514#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
515#[must_use]
516pub struct VirtualDPad3D {
517    /// The button for the upward direction.
518    pub up: Box<dyn Buttonlike>,
519
520    /// The button for the downward direction.
521    pub down: Box<dyn Buttonlike>,
522
523    /// The button for the leftward direction.
524    pub left: Box<dyn Buttonlike>,
525
526    /// The button for the rightward direction.
527    pub right: Box<dyn Buttonlike>,
528
529    /// The button for the forward direction.
530    pub forward: Box<dyn Buttonlike>,
531
532    /// The button for the backward direction.
533    pub backward: Box<dyn Buttonlike>,
534}
535
536impl VirtualDPad3D {
537    /// Creates a new [`VirtualDPad3D`] with six given [`Buttonlike`]s.
538    /// Each button represents a specific direction (up, down, left, right, forward, backward).
539    #[inline]
540    pub fn new(
541        up: impl Buttonlike,
542        down: impl Buttonlike,
543        left: impl Buttonlike,
544        right: impl Buttonlike,
545        forward: impl Buttonlike,
546        backward: impl Buttonlike,
547    ) -> Self {
548        Self {
549            up: Box::new(up),
550            down: Box::new(down),
551            left: Box::new(left),
552            right: Box::new(right),
553            forward: Box::new(forward),
554            backward: Box::new(backward),
555        }
556    }
557}
558
559impl UserInput for VirtualDPad3D {
560    /// [`VirtualDPad3D`] acts as a virtual triple-axis input.
561    #[inline]
562    fn kind(&self) -> InputControlKind {
563        InputControlKind::TripleAxis
564    }
565
566    /// [`VirtualDPad3D`] represents a compositions of six [`Buttonlike`]s.
567    #[inline]
568    fn decompose(&self) -> BasicInputs {
569        BasicInputs::Composite(vec![
570            self.up.clone(),
571            self.down.clone(),
572            self.left.clone(),
573            self.right.clone(),
574            self.forward.clone(),
575            self.backward.clone(),
576        ])
577    }
578}
579
580#[serde_typetag]
581impl TripleAxislike for VirtualDPad3D {
582    /// Retrieves the current X, Y, and Z values of this D-pad.
583    #[inline]
584    fn get_axis_triple(&self, input_store: &CentralInputStore, gamepad: Entity) -> Option<Vec3> {
585        let up = self.up.get_value(input_store, gamepad);
586        let down = self.down.get_value(input_store, gamepad);
587        let left = self.left.get_value(input_store, gamepad);
588        let right = self.right.get_value(input_store, gamepad);
589        let forward = self.forward.get_value(input_store, gamepad);
590        let backward = self.backward.get_value(input_store, gamepad);
591
592        if up.is_none()
593            && down.is_none()
594            && left.is_none()
595            && right.is_none()
596            && forward.is_none()
597            && backward.is_none()
598        {
599            return None;
600        }
601
602        let up = up.unwrap_or(0.0);
603        let down = down.unwrap_or(0.0);
604        let left = left.unwrap_or(0.0);
605        let right = right.unwrap_or(0.0);
606        let forward = forward.unwrap_or(0.0);
607        let backward = backward.unwrap_or(0.0);
608
609        Some(Vec3::new(right - left, up - down, backward - forward))
610    }
611
612    /// Sets the value of corresponding button on each axis based on the given `value`.
613    ///
614    /// When `value` along an axis is non-zero, set its absolute value to the value of:
615    /// - the negative button of the axis if the `value` is negative;
616    /// - the positive button of the axis if the `value` is positive.
617    fn set_axis_triple_as_gamepad(&self, world: &mut World, value: Vec3, gamepad: Option<Entity>) {
618        let Vec3 { x, y, z } = value;
619
620        if x < 0.0 {
621            self.left.set_value_as_gamepad(world, x.abs(), gamepad);
622        } else if x > 0.0 {
623            self.right.set_value_as_gamepad(world, x, gamepad);
624        }
625
626        if y < 0.0 {
627            self.down.set_value_as_gamepad(world, y.abs(), gamepad);
628        } else if y > 0.0 {
629            self.up.set_value_as_gamepad(world, y, gamepad);
630        }
631
632        if z < 0.0 {
633            self.forward.set_value_as_gamepad(world, z.abs(), gamepad);
634        } else if z > 0.0 {
635            self.backward.set_value_as_gamepad(world, z, gamepad);
636        }
637    }
638}
639
640#[cfg(feature = "keyboard")]
641#[cfg(test)]
642mod tests {
643    use bevy::input::InputPlugin;
644    use bevy::prelude::*;
645
646    use crate::plugin::CentralInputStorePlugin;
647    use crate::prelude::updating::CentralInputStore;
648    use crate::prelude::*;
649
650    fn test_app() -> App {
651        let mut app = App::new();
652        app.add_plugins(InputPlugin)
653            .add_plugins(CentralInputStorePlugin);
654        app
655    }
656
657    #[test]
658    fn test_virtual() {
659        let x = VirtualAxis::horizontal_arrow_keys();
660        let xy = VirtualDPad::arrow_keys();
661        let xyz = VirtualDPad3D::new(
662            KeyCode::ArrowUp,
663            KeyCode::ArrowDown,
664            KeyCode::ArrowLeft,
665            KeyCode::ArrowRight,
666            KeyCode::KeyF,
667            KeyCode::KeyB,
668        );
669
670        // No inputs
671        let mut app = test_app();
672        app.update();
673        let inputs = app.world().resource::<CentralInputStore>();
674
675        let gamepad = Entity::PLACEHOLDER;
676
677        assert_eq!(x.value(inputs, gamepad), 0.0);
678        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::ZERO);
679        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::ZERO);
680
681        // Press arrow left
682        let mut app = test_app();
683        KeyCode::ArrowLeft.press(app.world_mut());
684        app.update();
685        let inputs = app.world().resource::<CentralInputStore>();
686
687        assert_eq!(x.value(inputs, gamepad), -1.0);
688        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(-1.0, 0.0));
689        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(-1.0, 0.0, 0.0));
690
691        // Press arrow up
692        let mut app = test_app();
693        KeyCode::ArrowUp.press(app.world_mut());
694        app.update();
695        let inputs = app.world().resource::<CentralInputStore>();
696
697        assert_eq!(x.value(inputs, gamepad), 0.0);
698        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(0.0, 1.0));
699        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(0.0, 1.0, 0.0));
700
701        // Press arrow right
702        let mut app = test_app();
703        KeyCode::ArrowRight.press(app.world_mut());
704        app.update();
705        let inputs = app.world().resource::<CentralInputStore>();
706
707        assert_eq!(x.value(inputs, gamepad), 1.0);
708        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(1.0, 0.0));
709        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(1.0, 0.0, 0.0));
710
711        // Press key B
712        let mut app = test_app();
713        KeyCode::KeyB.press(app.world_mut());
714        app.update();
715        let inputs = app.world().resource::<CentralInputStore>();
716
717        assert_eq!(x.value(inputs, gamepad), 0.0);
718        assert_eq!(xy.axis_pair(inputs, gamepad), Vec2::new(0.0, 0.0));
719        assert_eq!(xyz.axis_triple(inputs, gamepad), Vec3::new(0.0, 0.0, 1.0));
720    }
721}