ailloli_ui_core/
progress.rs

1//! Pure progress value mapping helpers.
2
3/// Numeric domain used by determinate progress indicators.
4///
5/// # Examples
6///
7/// ```
8/// use ailloli_ui_core::ProgressSpec;
9///
10/// let progress = ProgressSpec::new(0.0, 10.0);
11/// assert_eq!(progress.fraction_for_value(5.0), 0.5);
12/// ```
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct ProgressSpec {
15    /// Inclusive value represented by an empty indicator.
16    pub min: f32,
17    /// Inclusive value represented by a full indicator.
18    pub max: f32,
19}
20
21impl Default for ProgressSpec {
22    fn default() -> Self {
23        Self { min: 0.0, max: 1.0 }
24    }
25}
26
27impl ProgressSpec {
28    /// Creates an ascending progress domain with finite input bounds.
29    ///
30    /// Reversed bounds are swapped, equal bounds are expanded upward by adding
31    /// `1.0` to the maximum, and any non-finite input bound resets the whole
32    /// range to `0.0..=1.0`. At magnitudes where that increment is not
33    /// representable, equal bounds can remain equal.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use ailloli_ui_core::ProgressSpec;
39    ///
40    /// assert_eq!(ProgressSpec::new(10.0, 0.0), ProgressSpec { min: 0.0, max: 10.0 });
41    /// ```
42    pub fn new(min: f32, max: f32) -> Self {
43        Self { min, max }.sanitized()
44    }
45
46    /// Returns this specification normalized according to [`Self::new`].
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use ailloli_ui_core::ProgressSpec;
52    ///
53    /// let spec = ProgressSpec { min: 5.0, max: 5.0 }.sanitized();
54    /// assert_eq!(spec.max, 6.0);
55    /// ```
56    pub fn sanitized(self) -> Self {
57        let fallback = Self::default();
58        let (mut min, mut max) = if self.min.is_finite() && self.max.is_finite() {
59            (self.min, self.max)
60        } else {
61            (fallback.min, fallback.max)
62        };
63        if min > max {
64            std::mem::swap(&mut min, &mut max);
65        }
66        if min == max {
67            max = min + 1.0;
68        }
69        Self { min, max }
70    }
71
72    /// Returns `max - min` for the sanitized domain.
73    ///
74    /// The result is normally positive and finite. It can be zero when two
75    /// large equal bounds cannot be widened by `1.0`, or infinity when the
76    /// difference between finite extreme bounds overflows.
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use ailloli_ui_core::ProgressSpec;
82    ///
83    /// assert_eq!(ProgressSpec::new(10.0, 30.0).span(), 20.0);
84    /// ```
85    pub fn span(self) -> f32 {
86        let spec = self.sanitized();
87        spec.max - spec.min
88    }
89
90    /// Clamps a value into the inclusive sanitized domain.
91    ///
92    /// A non-finite value selects the minimum.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use ailloli_ui_core::ProgressSpec;
98    ///
99    /// assert_eq!(ProgressSpec::new(10.0, 20.0).clamp_value(25.0), 20.0);
100    /// ```
101    pub fn clamp_value(self, value: f32) -> f32 {
102        let spec = self.sanitized();
103        let value = if value.is_finite() { value } else { spec.min };
104        value.clamp(spec.min, spec.max)
105    }
106
107    /// Maps a value into `0.0..=1.0`, clamping values outside the domain.
108    ///
109    /// A zero or overflowing span caused by extreme finite bounds can make the
110    /// IEEE-754 division return `NaN`.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use ailloli_ui_core::ProgressSpec;
116    ///
117    /// assert_eq!(ProgressSpec::new(10.0, 30.0).fraction_for_value(20.0), 0.5);
118    /// ```
119    pub fn fraction_for_value(self, value: f32) -> f32 {
120        let spec = self.sanitized();
121        (spec.clamp_value(value) - spec.min) / spec.span()
122    }
123
124    /// Maps a normalized fraction back into the progress domain.
125    ///
126    /// Fractions outside `0.0..=1.0` are clamped and non-finite fractions
127    /// select the minimum. An overflowing span between extreme finite bounds
128    /// can make the returned value non-finite.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use ailloli_ui_core::ProgressSpec;
134    ///
135    /// assert_eq!(ProgressSpec::new(10.0, 30.0).value_for_fraction(0.75), 25.0);
136    /// ```
137    pub fn value_for_fraction(self, fraction: f32) -> f32 {
138        let spec = self.sanitized();
139        let fraction = if fraction.is_finite() {
140            fraction.clamp(0.0, 1.0)
141        } else {
142            0.0
143        };
144        spec.min + fraction * spec.span()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    //! Covers invalid domains, non-finite values, clamping, and bidirectional mapping.
151
152    use super::*;
153
154    #[test]
155    fn spec_sanitizes_invalid_ranges() {
156        assert_eq!(
157            ProgressSpec::new(10.0, 0.0),
158            ProgressSpec {
159                min: 0.0,
160                max: 10.0
161            }
162        );
163        assert_eq!(
164            ProgressSpec::new(4.0, 4.0),
165            ProgressSpec { min: 4.0, max: 5.0 }
166        );
167        assert_eq!(
168            ProgressSpec {
169                min: f32::NAN,
170                max: 10.0,
171            }
172            .sanitized(),
173            ProgressSpec::default()
174        );
175    }
176
177    #[test]
178    fn clamp_value_bounds_and_nan() {
179        let spec = ProgressSpec::new(10.0, 20.0);
180        assert_eq!(spec.clamp_value(5.0), 10.0);
181        assert_eq!(spec.clamp_value(25.0), 20.0);
182        assert_eq!(spec.clamp_value(f32::NAN), 10.0);
183    }
184
185    #[test]
186    fn fraction_mapping_is_stable() {
187        let spec = ProgressSpec::new(10.0, 30.0);
188        assert_eq!(spec.fraction_for_value(10.0), 0.0);
189        assert_eq!(spec.fraction_for_value(20.0), 0.5);
190        assert_eq!(spec.fraction_for_value(30.0), 1.0);
191        assert_eq!(spec.fraction_for_value(60.0), 1.0);
192    }
193
194    #[test]
195    fn value_for_fraction_clamps_fraction() {
196        let spec = ProgressSpec::new(10.0, 30.0);
197        assert_eq!(spec.value_for_fraction(-1.0), 10.0);
198        assert_eq!(spec.value_for_fraction(0.75), 25.0);
199        assert_eq!(spec.value_for_fraction(2.0), 30.0);
200        assert_eq!(spec.value_for_fraction(f32::NAN), 10.0);
201    }
202}