Skip to main content

ui_storybook/components/shell/
status_bar.rs

1//! `StatusBar` — slim bottom-of-app strip with engine-level health.
2//!
3//! Three pieces of information across, all driven by props the parent ticks:
4//! FPS (renderer), encoder state, file size on disk. A single `kind: StatusKind`
5//! drives the right-hand pill ("Ready", "Encoding", "Error") and its color.
6
7use leptos::prelude::*;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum StatusKind {
11    #[default]
12    Ready,
13    Busy,
14    Error,
15}
16
17impl StatusKind {
18    fn css(self) -> &'static str {
19        match self {
20            StatusKind::Ready => "status-pill-ready",
21            StatusKind::Busy => "status-pill-busy",
22            StatusKind::Error => "status-pill-error",
23        }
24    }
25
26    fn label(self) -> &'static str {
27        match self {
28            StatusKind::Ready => "Ready",
29            StatusKind::Busy => "Working",
30            StatusKind::Error => "Error",
31        }
32    }
33}
34
35#[component]
36pub fn StatusBar(
37    /// Renderer FPS, formatted as a whole number.
38    #[prop(optional)]
39    fps: f32,
40    /// Encoder one-line status (e.g. `"H.264 · 9.4 Mbps"`).
41    #[prop(optional, into)]
42    encoder: String,
43    /// File size on disk for the active recording, in bytes.
44    #[prop(optional)]
45    file_bytes: u64,
46    /// Right-hand health pill kind.
47    #[prop(optional)]
48    kind: StatusKind,
49    /// Free-text detail next to the pill (e.g. error message).
50    #[prop(optional, into)]
51    detail: String,
52) -> impl IntoView {
53    let fps_value = fps.max(0.0).round();
54    #[allow(
55        clippy::cast_possible_truncation,
56        clippy::cast_sign_loss,
57        reason = "fps is positive and bounded; rendered FPS comfortably fits in u32"
58    )]
59    let fps_int = fps_value as u32;
60    let encoder = if encoder.is_empty() {
61        "—".to_string()
62    } else {
63        encoder
64    };
65    let size = format_bytes(file_bytes);
66    let pill_class = format!("status-pill {}", kind.css());
67    let pill_label = kind.label();
68    let has_detail = !detail.is_empty();
69
70    view! {
71        <div class="status-bar" role="contentinfo" aria-label="Engine status">
72            <div class="status-cell">
73                <span class="status-key">"FPS"</span>
74                <span class="status-value">{fps_int}</span>
75            </div>
76            <div class="status-cell">
77                <span class="status-key">"Encoder"</span>
78                <span class="status-value">{encoder}</span>
79            </div>
80            <div class="status-cell">
81                <span class="status-key">"Size"</span>
82                <span class="status-value">{size}</span>
83            </div>
84            <div class="status-spacer"></div>
85            <Show when=move || has_detail>
86                <span class="status-detail">{detail.clone()}</span>
87            </Show>
88            <span class=pill_class>
89                <span class="status-pill-dot"></span>
90                {pill_label}
91            </span>
92        </div>
93    }
94}
95
96fn format_bytes(bytes: u64) -> String {
97    const KB: u64 = 1024;
98    const MB: u64 = 1024 * KB;
99    const GB: u64 = 1024 * MB;
100    if bytes == 0 {
101        return "0 B".into();
102    }
103    if bytes >= GB {
104        #[allow(
105            clippy::cast_precision_loss,
106            reason = "display value, fractional precision sufficient"
107        )]
108        let v = bytes as f64 / GB as f64;
109        format!("{v:.2} GB")
110    } else if bytes >= MB {
111        #[allow(
112            clippy::cast_precision_loss,
113            reason = "display value, fractional precision sufficient"
114        )]
115        let v = bytes as f64 / MB as f64;
116        format!("{v:.1} MB")
117    } else if bytes >= KB {
118        #[allow(
119            clippy::cast_precision_loss,
120            reason = "display value, fractional precision sufficient"
121        )]
122        let v = bytes as f64 / KB as f64;
123        format!("{v:.0} KB")
124    } else {
125        format!("{bytes} B")
126    }
127}