ui_storybook/components/primitives/
meter.rs1use leptos::prelude::*;
8
9#[component]
10pub fn Meter(
11 level: f32,
13 #[prop(optional, default = 12)]
15 bar_count: u8,
16 #[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#[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 assert_eq!(lit_segments(0.55, 10), 6);
75 assert_eq!(lit_segments(0.54, 10), 5);
77 }
78}