Skip to main content

ui_storybook/fixtures/
recorder.rs

1//! Recorder fixtures — capture mode, source selection, on-screen options,
2//! recording state. Filled in across UI-06..13. Minimal scaffolding for
3//! UI-00 so the smoke tests pass.
4
5/// Capture mode tab — Screen / Window / Area.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum CaptureMode {
8    /// Full-display recording.
9    Screen,
10    /// Single application window.
11    Window,
12    /// User-drawn rectangle on the desktop.
13    Area,
14}
15
16/// Recording state shared by the toolbar, footer, and tray popover.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RecorderState {
19    /// Configuring sources, not yet recording.
20    Idle,
21    /// Countdown before recording starts.
22    Countdown {
23        /// Seconds remaining.
24        seconds: u8,
25    },
26    /// Actively capturing.
27    Recording,
28    /// Recording but paused.
29    Paused,
30}
31
32/// One on-screen overlay option (keypress badges, cursor highlights, etc.).
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct OverlayOptionFixture {
35    /// Stable id.
36    pub id: &'static str,
37    /// Display label.
38    pub label: &'static str,
39    /// Short description shown under the label.
40    pub hint: &'static str,
41    /// `true` when the toggle is on.
42    pub enabled: bool,
43}
44
45/// Sample overlay options for the on-screen-options popover.
46#[must_use]
47pub fn sample_overlay_options() -> Vec<OverlayOptionFixture> {
48    vec![
49        OverlayOptionFixture {
50            id: "overlay-keypress",
51            label: "Show keypresses",
52            hint: "Render ⌘C / ⌃-space badges on top of the recording",
53            enabled: true,
54        },
55        OverlayOptionFixture {
56            id: "overlay-clicks",
57            label: "Highlight clicks",
58            hint: "Soft ripple around each mouse click",
59            enabled: true,
60        },
61        OverlayOptionFixture {
62            id: "overlay-desktop",
63            label: "Hide desktop icons",
64            hint: "Blur the wallpaper area while recording",
65            enabled: false,
66        },
67        OverlayOptionFixture {
68            id: "overlay-sensitive",
69            label: "Redact sensitive areas",
70            hint: "Auto-blur regions marked as sensitive",
71            enabled: false,
72        },
73    ]
74}
75
76/// Sample on-screen-option rows for the `OnScreenOptionsPopover`
77/// (UI-10).
78#[must_use]
79pub fn sample_on_screen_options(
80    sensitive_disabled: bool,
81) -> Vec<crate::components::recorder::OnScreenOptionView> {
82    use crate::components::recorder::{OnScreenOptionKind, OnScreenOptionView};
83    vec![
84        OnScreenOptionView {
85            id: OnScreenOptionKind::CleanDesktop,
86            title: "Clean up the desktop".to_owned(),
87            description: "Hide icons + the dock for a clean recording. Wallpaper stays.".to_owned(),
88            enabled: true,
89            disabled: false,
90        },
91        OnScreenOptionView {
92            id: OnScreenOptionKind::ShowKeys,
93            title: "Show keys you press".to_owned(),
94            description: "Render ⌘C / ⌃-space badges over the recording so viewers can follow along.".to_owned(),
95            enabled: true,
96            disabled: false,
97        },
98        OnScreenOptionView {
99            id: OnScreenOptionKind::BlurSensitiveInfo,
100            title: "Blur sensitive info".to_owned(),
101            description: "Auto-detect password fields and other sensitive regions, and blur them. Coming soon.".to_owned(),
102            enabled: false,
103            disabled: sensitive_disabled,
104        },
105    ]
106}
107
108/// All-on variant for the `all-on` story.
109#[must_use]
110pub fn sample_on_screen_options_all_on() -> Vec<crate::components::recorder::OnScreenOptionView> {
111    sample_on_screen_options(false)
112        .into_iter()
113        .map(|mut o| {
114            o.enabled = true;
115            o.disabled = false;
116            o
117        })
118        .collect()
119}
120
121/// Long-copy variant for the truncation story.
122#[must_use]
123pub fn sample_on_screen_options_long_copy() -> Vec<crate::components::recorder::OnScreenOptionView>
124{
125    use crate::components::recorder::{OnScreenOptionKind, OnScreenOptionView};
126    vec![
127        OnScreenOptionView {
128            id: OnScreenOptionKind::CleanDesktop,
129            title: "Clean up the desktop background and remove distracting icons".to_owned(),
130            description: "Hide every desktop icon plus the entire dock for a noise-free recording. Wallpaper remains visible. This setting reverts when the recording ends; nothing changes permanently on disk.".to_owned(),
131            enabled: true,
132            disabled: false,
133        },
134        OnScreenOptionView {
135            id: OnScreenOptionKind::ShowKeys,
136            title: "Show keys you press during the recording".to_owned(),
137            description: "Render small badges in the bottom-right showing each modifier + key press. Useful for tutorial-style content where viewers want to follow along with what shortcut was used.".to_owned(),
138            enabled: false,
139            disabled: false,
140        },
141    ]
142}
143
144/// Sample recording-controls footer view (UI-11 / AUT-131). The
145/// `start_state` lets stories sweep all `StartRecordingState`
146/// variants.
147#[must_use]
148pub fn sample_recording_controls(
149    state: crate::components::recorder::StartRecordingState,
150) -> crate::components::recorder::RecordingControlsView {
151    use crate::components::recorder::{
152        RecordingControlsView, format_auto_zoom_label, format_countdown_label,
153    };
154    RecordingControlsView {
155        auto_zoom_label: format_auto_zoom_label(Some(2.0)),
156        countdown_label: format_countdown_label(3),
157        shortcuts: vec!["⌘".to_owned(), "⇧".to_owned(), "2".to_owned()],
158        start_state: state,
159    }
160}
161
162/// Compact variant — no auto-zoom, no countdown. Used by the
163/// `recording-footer-compact` story.
164#[must_use]
165pub fn sample_recording_controls_compact() -> crate::components::recorder::RecordingControlsView {
166    use crate::components::recorder::{
167        RecordingControlsView, StartRecordingState, format_auto_zoom_label, format_countdown_label,
168    };
169    RecordingControlsView {
170        auto_zoom_label: format_auto_zoom_label(None),
171        countdown_label: format_countdown_label(0),
172        shortcuts: vec!["⌘".to_owned(), "R".to_owned()],
173        start_state: StartRecordingState::Ready,
174    }
175}
176
177/// Sample Save-panel view in the *choosing* state (M-SAVE.GATE) — a
178/// recording is parked awaiting export, the user is picking folder +
179/// format. `busy` is false (controls live).
180#[must_use]
181pub fn sample_save_panel_choosing() -> crate::components::recorder::SavePanelView {
182    use crate::components::recorder::{SaveFormat, SavePanelView};
183    SavePanelView::Choosing {
184        output_dir: "/Users/you/Movies/Screen".to_owned(),
185        format: SaveFormat::Mp4H264,
186        busy: false,
187    }
188}
189
190/// Variant mid-export — `busy` true so the controls are dimmed and the
191/// Export button reads "Exporting…". Used by the `save-panel-exporting`
192/// story.
193#[must_use]
194pub fn sample_save_panel_exporting() -> crate::components::recorder::SavePanelView {
195    use crate::components::recorder::{SaveFormat, SavePanelView};
196    SavePanelView::Choosing {
197        output_dir: "/Users/you/Movies/Screen".to_owned(),
198        format: SaveFormat::WebmVp9,
199        busy: true,
200    }
201}
202
203/// Save-panel view in the *saved* state — the export landed; the panel
204/// offers Reveal-in-Finder / Done. Used by the `save-panel-saved`
205/// story.
206#[must_use]
207pub fn sample_save_panel_saved() -> crate::components::recorder::SavePanelView {
208    use crate::components::recorder::SavePanelView;
209    SavePanelView::Saved {
210        path: "/Users/you/Movies/Screen/Screen-2026-05-25-143012.mp4".to_owned(),
211    }
212}
213
214/// Sample tray-record-popover view (UI-12 / AUT-132).
215#[must_use]
216pub fn sample_tray_record_popover(
217    open: crate::components::recorder::OpenRecorderPopoverKind,
218) -> crate::components::recorder::TrayRecordPopoverView {
219    use crate::components::recorder::{
220        OnScreenSummaryView, StartRecordingState, TrayRecordPopoverView, WorkspaceSwitcherView,
221        format_on_screen_summary,
222    };
223    use crate::components::shell::AppSection;
224    use crate::fixtures::audio_apps::{sample_audio_apps, sample_system_audio_view};
225    use crate::fixtures::devices::{
226        sample_capture_source_camera, sample_capture_source_microphone, sample_display_source,
227    };
228    use crate::fixtures::workspaces::sample_workspace_views;
229
230    let workspaces = sample_workspace_views();
231    let on_screen = sample_on_screen_options(false);
232    let summary = format_on_screen_summary(&on_screen);
233    let apps = sample_audio_apps();
234    let total = apps.len();
235    TrayRecordPopoverView {
236        active_section: AppSection::Record,
237        capture_mode: CaptureMode::Screen,
238        workspaces: WorkspaceSwitcherView {
239            workspaces,
240            selected_id: "ws-northwind",
241        },
242        display: sample_display_source(true),
243        camera: sample_capture_source_camera(true, false),
244        microphone: sample_capture_source_microphone(true, false, Some(0.35)),
245        system_audio: sample_system_audio_view(true, false, &apps, total),
246        on_screen_summary: OnScreenSummaryView {
247            summary,
248            options: on_screen,
249        },
250        controls: sample_recording_controls(StartRecordingState::Ready),
251        open,
252    }
253}
254
255/// Variant with Start recording disabled (no source selected yet).
256#[must_use]
257pub fn sample_tray_record_popover_start_disabled()
258-> crate::components::recorder::TrayRecordPopoverView {
259    use crate::components::recorder::{OpenRecorderPopoverKind, StartRecordingState};
260    let mut v = sample_tray_record_popover(OpenRecorderPopoverKind::None);
261    v.controls = sample_recording_controls(StartRecordingState::Disabled);
262    v
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn overlay_options_non_empty() {
271        assert!(!sample_overlay_options().is_empty());
272    }
273
274    #[test]
275    fn tray_record_popover_is_record_section_by_default() {
276        use crate::components::recorder::OpenRecorderPopoverKind;
277        use crate::components::shell::AppSection;
278        let v = sample_tray_record_popover(OpenRecorderPopoverKind::None);
279        assert!(matches!(v.active_section, AppSection::Record));
280        assert!(!v.on_screen_summary.summary.is_empty());
281    }
282
283    #[test]
284    fn recording_controls_preserve_shortcut_order() {
285        use crate::components::recorder::StartRecordingState;
286        let v = sample_recording_controls(StartRecordingState::Ready);
287        assert_eq!(v.shortcuts, vec!["⌘", "⇧", "2"]);
288        // The button renders shortcuts in input order; if a future
289        // refactor were to sort them this test catches it.
290    }
291
292    #[test]
293    fn recording_controls_compact_drops_zoom_and_countdown() {
294        let v = sample_recording_controls_compact();
295        assert!(v.auto_zoom_label.contains("off"));
296        assert!(v.countdown_label.starts_with("No"));
297    }
298
299    #[test]
300    fn on_screen_options_have_one_per_kind() {
301        use crate::components::recorder::OnScreenOptionKind;
302        let opts = sample_on_screen_options(false);
303        assert_eq!(opts.len(), 3);
304        for kind in [
305            OnScreenOptionKind::CleanDesktop,
306            OnScreenOptionKind::ShowKeys,
307            OnScreenOptionKind::BlurSensitiveInfo,
308        ] {
309            assert!(opts.iter().any(|o| o.id == kind));
310        }
311    }
312}