adui_dioxus/components/
number_utils.rs

1//! Shared helpers for numeric controls (InputNumber, Slider, Rate, ColorPicker sliders).
2//!
3//! This module centralizes step/precision handling so components can stay focused on UI.
4
5/// Normalization rules applied to numeric values.
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
7pub struct NumberRules {
8    /// Minimum allowed value.
9    pub min: Option<f64>,
10    /// Maximum allowed value.
11    pub max: Option<f64>,
12    /// Step used when incrementing/decrementing. Defaults to `1.0` when not set.
13    pub step: Option<f64>,
14    /// Optional decimal precision enforced on output.
15    pub precision: Option<u32>,
16}
17
18impl NumberRules {
19    /// Returns the effective step (>= 0) used for arithmetic.
20    pub fn effective_step(&self) -> f64 {
21        let step = self.step.unwrap_or(1.0);
22        if step.is_sign_negative() { -step } else { step }
23    }
24}
25
26/// Clamp a value into the optional min/max range.
27pub fn clamp(value: f64, rules: &NumberRules) -> f64 {
28    let after_min = if let Some(min) = rules.min {
29        value.max(min)
30    } else {
31        value
32    };
33    if let Some(max) = rules.max {
34        after_min.min(max)
35    } else {
36        after_min
37    }
38}
39
40/// Round a value with the given precision, leaving untouched when precision is not set.
41pub fn round_with_precision(value: f64, precision: Option<u32>) -> f64 {
42    if let Some(p) = precision {
43        if p == 0 {
44            value.round()
45        } else {
46            let factor = 10_f64.powi(p as i32);
47            (value * factor).round() / factor
48        }
49    } else {
50        value
51    }
52}
53
54/// Apply a delta in step units, clamp, then round with precision.
55pub fn apply_step(value: f64, delta_steps: i32, rules: &NumberRules) -> f64 {
56    let step = rules.effective_step();
57    let next = value + step * (delta_steps as f64);
58    let clamped = clamp(next, rules);
59    round_with_precision(clamped, rules.precision)
60}
61
62/// Parse a string into a number and clamp/round it. Returns `None` on invalid input.
63pub fn parse_and_normalize(input: &str, rules: &NumberRules) -> Option<f64> {
64    let raw = input.trim();
65    if raw.is_empty() {
66        return None;
67    }
68    let parsed: f64 = raw.parse().ok()?;
69    let clamped = clamp(parsed, rules);
70    Some(round_with_precision(clamped, rules.precision))
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn clamp_respects_bounds() {
79        let rules = NumberRules {
80            min: Some(0.0),
81            max: Some(10.0),
82            step: None,
83            precision: None,
84        };
85        assert_eq!(clamp(-2.0, &rules), 0.0);
86        assert_eq!(clamp(5.5, &rules), 5.5);
87        assert_eq!(clamp(20.0, &rules), 10.0);
88    }
89
90    #[test]
91    fn apply_step_clamps_and_rounds() {
92        let rules = NumberRules {
93            min: Some(0.0),
94            max: Some(2.0),
95            step: Some(0.3),
96            precision: Some(2),
97        };
98        // Start from 1.0, add two steps (0.6) -> 1.6
99        assert_eq!(apply_step(1.0, 2, &rules), 1.6);
100        // Go below min
101        assert_eq!(apply_step(0.2, -2, &rules), 0.0);
102        // Go above max
103        assert_eq!(apply_step(1.9, 2, &rules), 2.0);
104    }
105
106    #[test]
107    fn parse_and_normalize_handles_precision() {
108        let rules = NumberRules {
109            min: None,
110            max: None,
111            step: None,
112            precision: Some(1),
113        };
114        assert_eq!(parse_and_normalize("3.1415", &rules), Some(3.1));
115        assert_eq!(parse_and_normalize("   2.05 ", &rules), Some(2.1));
116        assert_eq!(parse_and_normalize("", &rules), None);
117        assert_eq!(parse_and_normalize("abc", &rules), None);
118    }
119}