leafwing_input_manager/input_processing/single_axis/
range.rs

1//! Range processors for single-axis inputs
2
3use std::hash::{Hash, Hasher};
4
5use bevy::{math::FloatOrd, prelude::Reflect};
6use serde::{Deserialize, Serialize};
7
8use super::AxisProcessor;
9
10/// Specifies an acceptable min-max range for valid single-axis inputs,
11/// restricting all value stays within intended limits
12/// to avoid unexpected behavior caused by extreme inputs.
13///
14/// ```rust
15/// use leafwing_input_manager::prelude::*;
16///
17/// // Restrict values to [-2.0, 1.5].
18/// let bounds = AxisBounds::new(-2.0, 1.5);
19///
20/// // The ways to create an AxisProcessor.
21/// let processor = AxisProcessor::from(bounds);
22/// assert_eq!(processor, AxisProcessor::ValueBounds(bounds));
23///
24/// for value in -300..300 {
25///     let value = value as f32 * 0.01;
26///     assert_eq!(bounds.clamp(value), value.clamp(-2.0, 1.5));
27/// }
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
30#[must_use]
31pub struct AxisBounds {
32    /// The minimum value of valid inputs.
33    pub(crate) min: f32,
34
35    /// The maximum value of valid inputs.
36    pub(crate) max: f32,
37}
38
39impl AxisBounds {
40    /// Unlimited [`AxisBounds`].
41    pub const FULL_RANGE: Self = Self {
42        min: f32::MIN,
43        max: f32::MAX,
44    };
45
46    /// Creates an [`AxisBounds`] that restricts values to the given range `[min, max]`.
47    ///
48    /// # Requirements
49    ///
50    /// - `min` <= `max`.
51    ///
52    /// # Panics
53    ///
54    /// Panics if the requirements aren't met.
55    #[inline]
56    pub fn new(min: f32, max: f32) -> Self {
57        // PartialOrd for f32 ensures that NaN values are checked during comparisons.
58        assert!(min <= max);
59        Self { min, max }
60    }
61
62    /// Creates an [`AxisBounds`] that restricts values within the range `[-threshold, threshold]`.
63    ///
64    /// # Requirements
65    ///
66    /// - `threshold` >= `0.0`.
67    ///
68    /// # Panics
69    ///
70    /// Panics if the requirements aren't met.
71    #[doc(alias = "magnitude")]
72    #[inline]
73    pub fn symmetric(threshold: f32) -> Self {
74        Self::new(-threshold, threshold)
75    }
76
77    /// Creates an [`AxisBounds`] that restricts values to a minimum value.
78    #[inline]
79    pub const fn at_least(min: f32) -> Self {
80        Self {
81            min,
82            ..Self::FULL_RANGE
83        }
84    }
85
86    /// Creates an [`AxisBounds`] that restricts values to a maximum value.
87    #[inline]
88    pub const fn at_most(max: f32) -> Self {
89        Self {
90            max,
91            ..Self::FULL_RANGE
92        }
93    }
94
95    /// Returns the minimum and maximum bounds.
96    #[must_use]
97    #[inline]
98    pub fn min_max(&self) -> (f32, f32) {
99        (self.min(), self.max())
100    }
101
102    /// Returns the minimum bound.
103    #[must_use]
104    #[inline]
105    pub fn min(&self) -> f32 {
106        self.min
107    }
108
109    /// Returns the maximum bound.
110    #[must_use]
111    #[inline]
112    pub fn max(&self) -> f32 {
113        self.max
114    }
115
116    /// Is the given `input_value` within the bounds?
117    #[must_use]
118    #[inline]
119    pub fn contains(&self, input_value: f32) -> bool {
120        self.min <= input_value && input_value <= self.max
121    }
122
123    /// Clamps `input_value` within the bounds.
124    #[must_use]
125    #[inline]
126    pub fn clamp(&self, input_value: f32) -> f32 {
127        // clamp() includes checks if either bound is set to NaN,
128        // but the constructors guarantee that all bounds will not be NaN.
129        input_value.min(self.max).max(self.min)
130    }
131}
132
133impl Default for AxisBounds {
134    /// Creates an [`AxisBounds`] that restricts values to the range `[-1.0, 1.0]`.
135    #[inline]
136    fn default() -> Self {
137        Self {
138            min: -1.0,
139            max: 1.0,
140        }
141    }
142}
143
144impl From<AxisBounds> for AxisProcessor {
145    fn from(value: AxisBounds) -> Self {
146        Self::ValueBounds(value)
147    }
148}
149
150impl Eq for AxisBounds {}
151
152impl Hash for AxisBounds {
153    fn hash<H: Hasher>(&self, state: &mut H) {
154        FloatOrd(self.min).hash(state);
155        FloatOrd(self.max).hash(state);
156    }
157}
158
159/// Specifies an exclusion range for excluding single-axis inputs,
160/// helping filter out minor fluctuations and unintended movements.
161///
162/// In simple terms, this processor behaves like an [`AxisDeadZone`] without normalization.
163///
164/// # Examples
165///
166/// ```rust
167/// use leafwing_input_manager::prelude::*;
168///
169/// // Exclude values between -0.2 and 0.3
170/// let exclusion = AxisExclusion::new(-0.2, 0.3);
171///
172/// // The ways to create an AxisProcessor.
173/// let processor = AxisProcessor::from(exclusion);
174/// assert_eq!(processor, AxisProcessor::Exclusion(exclusion));
175///
176/// for value in -300..300 {
177///     let value = value as f32 * 0.01;
178///
179///     if -0.2 <= value && value <= 0.3 {
180///         assert!(exclusion.contains(value));
181///         assert_eq!(exclusion.exclude(value), 0.0);
182///     } else {
183///         assert!(!exclusion.contains(value));
184///         assert_eq!(exclusion.exclude(value), value);
185///     }
186/// }
187/// ```
188#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
189#[must_use]
190pub struct AxisExclusion {
191    /// The maximum negative value treated as zero.
192    pub(crate) negative_max: f32,
193
194    /// The minimum positive value treated as zero.
195    pub(crate) positive_min: f32,
196}
197
198impl AxisExclusion {
199    /// Zero-size [`AxisExclusion`], leaving values as is.
200    pub const ZERO: Self = Self {
201        negative_max: 0.0,
202        positive_min: 0.0,
203    };
204
205    /// Creates an [`AxisExclusion`] that ignores values within the range `[negative_max, positive_min]`.
206    ///
207    /// # Requirements
208    ///
209    /// - `negative_max` <= `0.0` <= `positive_min`.
210    ///
211    /// # Panics
212    ///
213    /// Panics if the requirements aren't met.
214    #[inline]
215    pub fn new(negative_max: f32, positive_min: f32) -> Self {
216        assert!(negative_max <= 0.0);
217        assert!(positive_min >= 0.0);
218        Self {
219            negative_max,
220            positive_min,
221        }
222    }
223
224    /// Creates an [`AxisExclusion`] that ignores values within the range `[-threshold, threshold]`.
225    ///
226    /// # Requirements
227    ///
228    /// - `threshold` >= `0.0`.
229    ///
230    /// # Panics
231    ///
232    /// Panics if the requirements aren't met.
233    #[doc(alias = "magnitude")]
234    #[inline]
235    pub fn symmetric(threshold: f32) -> Self {
236        Self::new(-threshold, threshold)
237    }
238
239    /// Creates an [`AxisExclusion`] that only passes positive values that greater than `positive_min`.
240    ///
241    /// # Requirements
242    ///
243    /// - `positive_min` >= `0.0`.
244    ///
245    /// # Panics
246    ///
247    /// Panics if the requirements aren't met.
248    #[inline]
249    pub fn only_positive(positive_min: f32) -> Self {
250        Self::new(f32::NEG_INFINITY, positive_min)
251    }
252
253    /// Creates an [`AxisExclusion`] that only passes negative values that less than `negative_max`.
254    ///
255    /// # Requirements
256    ///
257    /// - `negative_max` <= `0.0`.
258    ///
259    /// # Panics
260    ///
261    /// Panics if the requirements aren't met.
262    #[inline]
263    pub fn only_negative(negative_max: f32) -> Self {
264        Self::new(negative_max, f32::INFINITY)
265    }
266
267    /// Returns the minimum and maximum bounds.
268    #[must_use]
269    #[inline]
270    pub fn min_max(&self) -> (f32, f32) {
271        (self.negative_max, self.positive_min)
272    }
273
274    /// Returns the minimum bound.
275    #[must_use]
276    #[inline]
277    pub fn min(&self) -> f32 {
278        self.negative_max
279    }
280
281    /// Returns the maximum bounds.
282    #[must_use]
283    #[inline]
284    pub fn max(&self) -> f32 {
285        self.positive_min
286    }
287
288    /// Is `input_value` within the deadzone?
289    #[must_use]
290    #[inline]
291    pub fn contains(&self, input_value: f32) -> bool {
292        self.negative_max <= input_value && input_value <= self.positive_min
293    }
294
295    /// Excludes values within the specified range.
296    #[must_use]
297    #[inline]
298    pub fn exclude(&self, input_value: f32) -> f32 {
299        if self.contains(input_value) {
300            0.0
301        } else {
302            input_value
303        }
304    }
305
306    /// Creates an [`AxisDeadZone`] using `self` as the exclusion range.
307    #[inline]
308    pub fn scaled(self) -> AxisDeadZone {
309        AxisDeadZone::new(self.negative_max, self.positive_min)
310    }
311}
312
313impl Default for AxisExclusion {
314    /// Creates an [`AxisExclusion`] that ignores values within the range `[-0.1, 0.1]`.
315    #[inline]
316    fn default() -> Self {
317        Self {
318            negative_max: -0.1,
319            positive_min: 0.1,
320        }
321    }
322}
323
324impl From<AxisExclusion> for AxisProcessor {
325    fn from(value: AxisExclusion) -> Self {
326        Self::Exclusion(value)
327    }
328}
329
330impl Eq for AxisExclusion {}
331
332impl Hash for AxisExclusion {
333    fn hash<H: Hasher>(&self, state: &mut H) {
334        FloatOrd(self.negative_max).hash(state);
335        FloatOrd(self.positive_min).hash(state);
336    }
337}
338
339/// A scaled version of [`AxisExclusion`] with the bounds
340/// set to [`AxisBounds::magnitude(1.0)`](AxisBounds::default)
341/// that normalizes non-excluded input values into the "live zone",
342/// the remaining range within the bounds after dead zone exclusion.
343///
344/// # Examples
345///
346/// ```rust
347/// use bevy::prelude::*;
348/// use leafwing_input_manager::prelude::*;
349///
350/// // Exclude values between -0.2 and 0.3
351/// let deadzone = AxisDeadZone::new(-0.2, 0.3);
352///
353/// // Another way to create an AxisDeadzone.
354/// let exclusion = AxisExclusion::new(-0.2, 0.3);
355/// assert_eq!(exclusion.scaled(), deadzone);
356///
357/// // The ways to create an AxisProcessor.
358/// let processor = AxisProcessor::from(deadzone);
359/// assert_eq!(processor, AxisProcessor::DeadZone(deadzone));
360///
361/// // The bounds after normalization.
362/// let bounds = deadzone.bounds();
363/// assert_eq!(bounds.min(), -1.0);
364/// assert_eq!(bounds.max(), 1.0);
365///
366/// for value in -300..300 {
367///     let value = value as f32 * 0.01;
368///
369///     // Values within the dead zone are treated as zero.
370///     if -0.2 <= value && value <= 0.3 {
371///         assert!(deadzone.within_exclusion(value));
372///         assert_eq!(deadzone.normalize(value), 0.0);
373///     }
374///
375///     // Values within the live zone are scaled linearly.
376///     else if -1.0 <= value && value < -0.2 {
377///         assert!(deadzone.within_livezone_lower(value));
378///
379///         let expected = f32::inverse_lerp(-1.0, -0.2, value) - 1.0;
380///         assert!((deadzone.normalize(value) - expected).abs() <= f32::EPSILON);
381///     } else if 0.3 < value && value <= 1.0 {
382///         assert!(deadzone.within_livezone_upper(value));
383///
384///         let expected = f32::inverse_lerp(0.3, 1.0, value);
385///         assert!((deadzone.normalize(value) - expected).abs() <= f32::EPSILON);
386///     }
387///
388///     // Values outside the bounds are restricted to the range.
389///     else {
390///         assert!(!deadzone.within_bounds(value));
391///         assert_eq!(deadzone.normalize(value), value.clamp(-1.0, 1.0));
392///     }
393/// }
394/// ```
395#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
396#[must_use]
397pub struct AxisDeadZone {
398    /// The [`AxisExclusion`] used for normalization.
399    pub(crate) exclusion: AxisExclusion,
400
401    /// Pre-calculated reciprocal of the lower live zone size,
402    /// preventing division during normalization.
403    pub(crate) livezone_lower_recip: f32,
404
405    /// Pre-calculated reciprocal of the upper live zone size,
406    /// preventing division during normalization.
407    pub(crate) livezone_upper_recip: f32,
408}
409
410impl AxisDeadZone {
411    /// Zero-size [`AxisDeadZone`], only restricting values to the range `[-1.0, 1.0]`.
412    pub const ZERO: Self = Self {
413        exclusion: AxisExclusion::ZERO,
414        livezone_lower_recip: 1.0,
415        livezone_upper_recip: 1.0,
416    };
417
418    /// Creates an [`AxisDeadZone`] that excludes input values within the range `[negative_max, positive_min]`
419    /// and then normalizes non-excluded input values into the valid range `[-1.0, 1.0]`.
420    ///
421    /// # Requirements
422    ///
423    /// - `negative_max` <= `0.0` <= `positive_min`.
424    ///
425    /// # Panics
426    ///
427    /// Panics if the requirements aren't met.
428    #[inline]
429    pub fn new(negative_max: f32, positive_min: f32) -> Self {
430        let (bound_min, bound_max) = AxisBounds::default().min_max();
431        Self {
432            exclusion: AxisExclusion::new(negative_max, positive_min),
433            livezone_lower_recip: (negative_max - bound_min).recip(),
434            livezone_upper_recip: (bound_max - positive_min).recip(),
435        }
436    }
437
438    /// Creates an [`AxisDeadZone`] that excludes input values within the range `[-threshold, threshold]`
439    /// and then normalizes non-excluded input values into the valid range `[-1.0, 1.0]`.
440    ///
441    /// # Requirements
442    ///
443    /// - `threshold` >= `0.0`.
444    ///
445    /// # Panics
446    ///
447    /// Panics if the requirements aren't met.
448    #[doc(alias = "magnitude")]
449    #[inline]
450    pub fn symmetric(threshold: f32) -> Self {
451        Self::new(-threshold, threshold)
452    }
453
454    /// Creates an [`AxisDeadZone`] that only passes positive values that greater than `positive_min`
455    /// and then normalizes them into the valid range `[-1.0, 1.0]`.
456    ///
457    /// # Requirements
458    ///
459    /// - `positive_min` >= `0.0`.
460    ///
461    /// # Panics
462    ///
463    /// Panics if the requirements aren't met.
464    #[inline]
465    pub fn only_positive(positive_min: f32) -> Self {
466        Self::new(f32::NEG_INFINITY, positive_min)
467    }
468
469    /// Creates an [`AxisDeadZone`] that only passes negative values that less than `negative_max`
470    /// and then normalizes them into the valid range `[-1.0, 1.0]`.
471    ///
472    /// # Requirements
473    ///
474    /// - `negative_max` <= `0.0`.
475    ///
476    /// # Panics
477    ///
478    /// Panics if the requirements aren't met.
479    #[inline]
480    pub fn only_negative(negative_max: f32) -> Self {
481        Self::new(negative_max, f32::INFINITY)
482    }
483
484    /// Returns the [`AxisExclusion`] used by this deadzone.
485    #[inline]
486    pub fn exclusion(&self) -> AxisExclusion {
487        self.exclusion
488    }
489
490    /// Returns the [`AxisBounds`] used by this deadzone.
491    #[inline]
492    pub fn bounds(&self) -> AxisBounds {
493        AxisBounds::default()
494    }
495
496    /// Returns the minimum and maximum bounds of the lower live zone used for normalization.
497    ///
498    /// In simple terms, this returns `(bounds.min, exclusion.min)`.
499    #[must_use]
500    #[inline]
501    pub fn livezone_lower_min_max(&self) -> (f32, f32) {
502        (self.bounds().min(), self.exclusion.min())
503    }
504
505    /// Returns the minimum and maximum bounds of the upper live zone used for normalization.
506    ///
507    /// In simple terms, this returns `(exclusion.max, bounds.max)`.
508    #[must_use]
509    #[inline]
510    pub fn livezone_upper_min_max(&self) -> (f32, f32) {
511        (self.exclusion.max(), self.bounds().max())
512    }
513
514    /// Is the given `input_value` within the exclusion range?
515    #[must_use]
516    #[inline]
517    pub fn within_exclusion(&self, input_value: f32) -> bool {
518        self.exclusion.contains(input_value)
519    }
520
521    /// Is the given `input_value` within the bounds?
522    #[must_use]
523    #[inline]
524    pub fn within_bounds(&self, input_value: f32) -> bool {
525        self.bounds().contains(input_value)
526    }
527
528    /// Is the given `input_value` within the lower live zone?
529    #[must_use]
530    #[inline]
531    pub fn within_livezone_lower(&self, input_value: f32) -> bool {
532        let (min, max) = self.livezone_lower_min_max();
533        min <= input_value && input_value <= max
534    }
535
536    /// Is the given `input_value` within the upper live zone?
537    #[must_use]
538    #[inline]
539    pub fn within_livezone_upper(&self, input_value: f32) -> bool {
540        let (min, max) = self.livezone_upper_min_max();
541        min <= input_value && input_value <= max
542    }
543
544    /// Normalizes input values into the live zone.
545    #[must_use]
546    pub fn normalize(&self, input_value: f32) -> f32 {
547        // Clamp out-of-bounds values to [-1, 1],
548        // and then exclude values within the dead zone,
549        // and finally linearly scale the result to the live zone.
550        if input_value <= 0.0 {
551            let (bound, deadzone) = self.livezone_lower_min_max();
552            let clamped_input = input_value.max(bound);
553            let distance_to_deadzone = (clamped_input - deadzone).min(0.0);
554            distance_to_deadzone * self.livezone_lower_recip
555        } else {
556            let (deadzone, bound) = self.livezone_upper_min_max();
557            let clamped_input = input_value.min(bound);
558            let distance_to_deadzone = (clamped_input - deadzone).max(0.0);
559            distance_to_deadzone * self.livezone_upper_recip
560        }
561    }
562}
563
564impl Default for AxisDeadZone {
565    /// Creates an [`AxisDeadZone`] that excludes input values within the deadzone `[-0.1, 0.1]`.
566    #[inline]
567    fn default() -> Self {
568        AxisDeadZone::new(-0.1, 0.1)
569    }
570}
571
572impl From<AxisDeadZone> for AxisProcessor {
573    fn from(value: AxisDeadZone) -> Self {
574        Self::DeadZone(value)
575    }
576}
577
578impl Eq for AxisDeadZone {}
579
580impl Hash for AxisDeadZone {
581    fn hash<H: Hasher>(&self, state: &mut H) {
582        self.exclusion.hash(state);
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use bevy::prelude::FloatExt;
590
591    #[test]
592    fn test_axis_value_bounds() {
593        fn test_bounds(bounds: AxisBounds, min: f32, max: f32) {
594            assert_eq!(bounds.min(), min);
595            assert_eq!(bounds.max(), max);
596            assert_eq!(bounds.min_max(), (min, max));
597
598            let processor = AxisProcessor::ValueBounds(bounds);
599            assert_eq!(AxisProcessor::from(bounds), processor);
600
601            for value in -300..300 {
602                let value = value as f32 * 0.01;
603
604                assert_eq!(bounds.clamp(value), processor.process(value));
605
606                if min <= value && value <= max {
607                    assert!(bounds.contains(value));
608                } else {
609                    assert!(!bounds.contains(value));
610                }
611
612                assert_eq!(bounds.clamp(value), value.clamp(min, max));
613            }
614        }
615
616        let bounds = AxisBounds::FULL_RANGE;
617        test_bounds(bounds, f32::MIN, f32::MAX);
618
619        let bounds = AxisBounds::default();
620        test_bounds(bounds, -1.0, 1.0);
621
622        let bounds = AxisBounds::new(-2.0, 2.5);
623        test_bounds(bounds, -2.0, 2.5);
624
625        let bounds = AxisBounds::symmetric(2.0);
626        test_bounds(bounds, -2.0, 2.0);
627
628        let bounds = AxisBounds::at_least(-1.0);
629        test_bounds(bounds, -1.0, f32::MAX);
630
631        let bounds = AxisBounds::at_most(1.5);
632        test_bounds(bounds, f32::MIN, 1.5);
633    }
634
635    #[test]
636    fn test_axis_exclusion() {
637        fn test_exclusion(exclusion: AxisExclusion, min: f32, max: f32) {
638            assert_eq!(exclusion.min(), min);
639            assert_eq!(exclusion.max(), max);
640            assert_eq!(exclusion.min_max(), (min, max));
641
642            let processor = AxisProcessor::Exclusion(exclusion);
643            assert_eq!(AxisProcessor::from(exclusion), processor);
644
645            for value in -300..300 {
646                let value = value as f32 * 0.01;
647
648                assert_eq!(exclusion.exclude(value), processor.process(value));
649
650                if min <= value && value <= max {
651                    assert!(exclusion.contains(value));
652                    assert_eq!(exclusion.exclude(value), 0.0);
653                } else {
654                    assert!(!exclusion.contains(value));
655                    assert_eq!(exclusion.exclude(value), value);
656                }
657            }
658        }
659
660        let exclusion = AxisExclusion::ZERO;
661        test_exclusion(exclusion, 0.0, 0.0);
662
663        let exclusion = AxisExclusion::default();
664        test_exclusion(exclusion, -0.1, 0.1);
665
666        let exclusion = AxisExclusion::new(-2.0, 2.5);
667        test_exclusion(exclusion, -2.0, 2.5);
668
669        let exclusion = AxisExclusion::symmetric(1.5);
670        test_exclusion(exclusion, -1.5, 1.5);
671    }
672
673    #[test]
674    fn test_axis_deadzone() {
675        fn test_deadzone(deadzone: AxisDeadZone, min: f32, max: f32) {
676            let exclusion = deadzone.exclusion();
677            assert_eq!(exclusion.min_max(), (min, max));
678
679            assert_eq!(deadzone.livezone_lower_min_max(), (-1.0, min));
680            let width_recip = (min + 1.0).recip();
681            assert!((deadzone.livezone_lower_recip - width_recip).abs() <= f32::EPSILON);
682
683            assert_eq!(deadzone.livezone_upper_min_max(), (max, 1.0));
684            let width_recip = (1.0 - max).recip();
685            assert!((deadzone.livezone_upper_recip - width_recip).abs() <= f32::EPSILON);
686
687            assert_eq!(AxisExclusion::new(min, max).scaled(), deadzone);
688
689            let processor = AxisProcessor::DeadZone(deadzone);
690            assert_eq!(AxisProcessor::from(deadzone), processor);
691
692            for value in -300..300 {
693                let value = value as f32 * 0.01;
694
695                assert_eq!(deadzone.normalize(value), processor.process(value));
696
697                // Values within the dead zone are treated as zero.
698                if min <= value && value <= max {
699                    assert!(deadzone.within_exclusion(value));
700                    assert_eq!(deadzone.normalize(value), 0.0);
701                }
702                // Values within the live zone are scaled linearly.
703                else if -1.0 <= value && value < min {
704                    assert!(deadzone.within_livezone_lower(value));
705
706                    let expected = f32::inverse_lerp(-1.0, min, value) - 1.0;
707                    let delta = (deadzone.normalize(value) - expected).abs();
708                    assert!(delta <= f32::EPSILON);
709                } else if max < value && value <= 1.0 {
710                    assert!(deadzone.within_livezone_upper(value));
711
712                    let expected = f32::inverse_lerp(max, 1.0, value);
713                    let delta = (deadzone.normalize(value) - expected).abs();
714                    assert!(delta <= f32::EPSILON);
715                }
716                // Values outside the bounds are restricted to the nearest valid value.
717                else {
718                    assert!(!deadzone.within_bounds(value));
719                    assert_eq!(deadzone.normalize(value), value.clamp(-1.0, 1.0));
720                }
721            }
722        }
723
724        let deadzone = AxisDeadZone::ZERO;
725        test_deadzone(deadzone, 0.0, 0.0);
726
727        let deadzone = AxisDeadZone::default();
728        test_deadzone(deadzone, -0.1, 0.1);
729
730        let deadzone = AxisDeadZone::new(-0.2, 0.3);
731        test_deadzone(deadzone, -0.2, 0.3);
732
733        let deadzone = AxisDeadZone::symmetric(0.4);
734        test_deadzone(deadzone, -0.4, 0.4);
735    }
736}