Skip to main content

ui_storybook/components/primitives/
slider.rs

1//! `Slider` — value slider (M-UI.4 / AUT-124).
2//!
3//! Visual only — renders a track, fill, thumb, and an optional label /
4//! readout. No drag handling. The parent component owns the value and
5//! re-renders with the new value when its input handler fires.
6
7use leptos::prelude::*;
8
9#[component]
10pub fn Slider(
11    /// Current value. Clamped into `[min, max]` for the visual percent.
12    value: f32,
13    /// Lower bound. Default 0.0.
14    #[prop(optional, default = 0.0)]
15    min: f32,
16    /// Upper bound. Default 1.0.
17    #[prop(optional, default = 1.0)]
18    max: f32,
19    /// `true` to render disabled.
20    #[prop(optional)]
21    disabled: bool,
22    /// Optional leading label.
23    #[prop(optional, into)]
24    label: Option<String>,
25    /// Optional trailing readout string (e.g. `"42 px"`).
26    #[prop(optional, into)]
27    readout: Option<String>,
28) -> impl IntoView {
29    let percent = slider_percent(value, min, max);
30    let mut class = String::from("slider");
31    if disabled {
32        class.push_str(" slider-disabled");
33    }
34    let fill_style = format!("width:{percent:.2}%");
35    let thumb_style = format!("left:{percent:.2}%");
36    view! {
37        <div class=class>
38            {label.map(|l| view! { <span class="slider-label">{l}</span> })}
39            <div class="slider-track">
40                <div class="slider-fill" style=fill_style></div>
41                <div class="slider-thumb" style=thumb_style></div>
42            </div>
43            {readout.map(|r| view! { <span class="slider-readout">{r}</span> })}
44        </div>
45    }
46}
47
48/// Project `value` into `[0, 100]` against `[min, max]`. Clamps so
49/// out-of-range values pin to the endpoints; if `min == max` the value
50/// projects to `0%` to avoid divide-by-zero.
51#[must_use]
52pub fn slider_percent(value: f32, min: f32, max: f32) -> f32 {
53    if max <= min {
54        return 0.0;
55    }
56    ((value - min) / (max - min) * 100.0).clamp(0.0, 100.0)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn percent_zero_at_min() {
65        assert!((slider_percent(0.0, 0.0, 1.0) - 0.0).abs() < 1e-4);
66        assert!((slider_percent(-5.0, 0.0, 1.0) - 0.0).abs() < 1e-4);
67    }
68
69    #[test]
70    fn percent_one_hundred_at_max() {
71        assert!((slider_percent(1.0, 0.0, 1.0) - 100.0).abs() < 1e-4);
72        assert!((slider_percent(2.0, 0.0, 1.0) - 100.0).abs() < 1e-4);
73    }
74
75    #[test]
76    fn percent_midway() {
77        assert!((slider_percent(0.5, 0.0, 1.0) - 50.0).abs() < 1e-4);
78        assert!((slider_percent(5.0, 0.0, 10.0) - 50.0).abs() < 1e-4);
79        assert!((slider_percent(15.0, 10.0, 20.0) - 50.0).abs() < 1e-4);
80    }
81
82    #[test]
83    fn zero_range_returns_zero() {
84        assert!((slider_percent(1.0, 5.0, 5.0) - 0.0).abs() < 1e-4);
85    }
86}