Skip to main content

ui_storybook/components/recorder/
recording_toolbar.rs

1//! `RecordingToolbar` — the top-of-app surface during capture.
2//!
3//! Three states drive everything: `Idle` (big primary "Record" button,
4//! source picker placeholder), `Recording` (red pulsing indicator, timer,
5//! Pause + Stop), `Paused` (paused badge, Resume + Stop). The timer is a
6//! prop the parent ticks; the component is purely presentational.
7
8use leptos::prelude::*;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum RecordingState {
12    #[default]
13    Idle,
14    Recording,
15    Paused,
16}
17
18impl RecordingState {
19    fn css(self) -> &'static str {
20        match self {
21            RecordingState::Idle => "rec-idle",
22            RecordingState::Recording => "rec-recording",
23            RecordingState::Paused => "rec-paused",
24        }
25    }
26
27    fn status_label(self) -> &'static str {
28        match self {
29            RecordingState::Idle => "Ready",
30            RecordingState::Recording => "Recording",
31            RecordingState::Paused => "Paused",
32        }
33    }
34}
35
36#[component]
37pub fn RecordingToolbar(
38    /// Current capture state.
39    #[prop(optional)]
40    state: RecordingState,
41    /// Elapsed recording time in seconds.
42    #[prop(optional)]
43    elapsed_seconds: f32,
44    /// Display name of the current capture source (e.g. "Built-in Display").
45    #[prop(optional, into)]
46    source: String,
47) -> impl IntoView {
48    let class = format!("recording-toolbar {}", state.css());
49    let status = state.status_label();
50    let timer = format_clock(elapsed_seconds.max(0.0));
51    let source_label = if source.is_empty() {
52        "Pick a source".to_string()
53    } else {
54        source
55    };
56
57    let actions = match state {
58        RecordingState::Idle => view! {
59            <button class="rec-action rec-action-primary" type="button">
60                <span class="rec-dot rec-dot-static"></span>
61                <span>"Start recording"</span>
62            </button>
63        }
64        .into_any(),
65        RecordingState::Recording => view! {
66            <button class="rec-action rec-action-secondary" type="button">"Pause"</button>
67            <button class="rec-action rec-action-stop" type="button">"Stop"</button>
68        }
69        .into_any(),
70        RecordingState::Paused => view! {
71            <button class="rec-action rec-action-primary" type="button">
72                <span>"Resume"</span>
73            </button>
74            <button class="rec-action rec-action-stop" type="button">"Stop"</button>
75        }
76        .into_any(),
77    };
78
79    view! {
80        <div class=class role="toolbar" aria-label="Recording controls">
81            <div class="rec-status">
82                <span class="rec-dot"></span>
83                <span class="rec-status-label">{status}</span>
84            </div>
85            <div class="rec-timer">{timer}</div>
86            <div class="rec-source">
87                <span class="rec-source-glyph">"⌬"</span>
88                <span class="rec-source-label">{source_label}</span>
89            </div>
90            <div class="rec-actions">{actions}</div>
91        </div>
92    }
93}
94
95#[allow(
96    clippy::cast_possible_truncation,
97    clippy::cast_sign_loss,
98    reason = "elapsed is positive and small, fits in u32"
99)]
100fn format_clock(seconds: f32) -> String {
101    let total = seconds.round() as u32;
102    let h = total / 3600;
103    let m = (total % 3600) / 60;
104    let s = total % 60;
105    if h > 0 {
106        format!("{h}:{m:02}:{s:02}")
107    } else {
108        format!("{m:02}:{s:02}")
109    }
110}