Skip to main content

ui_storybook/components/primitives/
meter.rs

1//! `Meter` — audio-level bars (M-UI.4 / AUT-124).
2//!
3//! Renders `bar_count` segments, lit from the left up to the integer
4//! count corresponding to `level` (clamped to `[0, 1]`). Pure
5//! presentation; the parent feeds in the current normalized level.
6
7use leptos::prelude::*;
8
9#[component]
10pub fn Meter(
11    /// Normalized level in `[0, 1]`. Out-of-range values are clamped.
12    level: f32,
13    /// Number of segments to render.
14    #[prop(optional, default = 12)]
15    bar_count: u8,
16    /// `true` to render in the danger color (clipping / over).
17    #[prop(optional)]
18    danger: bool,
19) -> impl IntoView {
20    let lit = lit_segments(level, bar_count);
21    let mut class = String::from("meter");
22    if danger {
23        class.push_str(" meter-danger");
24    }
25    view! {
26        <div class=class role="meter" aria-valuemin="0" aria-valuemax="1" aria-valuenow=level.clamp(0.0, 1.0)>
27            {(0..bar_count).map(|i| {
28                let on = i < lit;
29                let bar_class = if on { "meter-bar meter-bar-on" } else { "meter-bar" };
30                view! { <span class=bar_class></span> }
31            }).collect_view()}
32        </div>
33    }
34}
35
36/// How many of `bar_count` segments to light at `level`.
37#[must_use]
38pub fn lit_segments(level: f32, bar_count: u8) -> u8 {
39    let l = level.clamp(0.0, 1.0);
40    #[allow(
41        clippy::cast_precision_loss,
42        clippy::cast_possible_truncation,
43        clippy::cast_sign_loss,
44        reason = "bar_count <= 255 fits f32 + result is bound [0, bar_count]"
45    )]
46    let lit = (l * f32::from(bar_count)).round() as u8;
47    lit.min(bar_count)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn zero_level_lights_no_bars() {
56        assert_eq!(lit_segments(0.0, 12), 0);
57        assert_eq!(lit_segments(-0.5, 12), 0);
58    }
59
60    #[test]
61    fn full_level_lights_all_bars() {
62        assert_eq!(lit_segments(1.0, 12), 12);
63        assert_eq!(lit_segments(1.5, 12), 12);
64    }
65
66    #[test]
67    fn half_level_lights_half() {
68        assert_eq!(lit_segments(0.5, 12), 6);
69    }
70
71    #[test]
72    fn rounds_to_nearest() {
73        // 0.55 * 10 = 5.5 → 6
74        assert_eq!(lit_segments(0.55, 10), 6);
75        // 0.54 * 10 = 5.4 → 5
76        assert_eq!(lit_segments(0.54, 10), 5);
77    }
78}