Skip to main content

ui_storybook/fixtures/
devices.rs

1//! Device fixtures — cameras, microphones, system-audio sources.
2//! Filled in across UI-08 (`CaptureSourceRow`) / UI-09
3//! (`SystemAudioPickerList`).
4//! For UI-00 we ship minimal placeholder shapes so the smoke test sees
5//! non-empty collections; UI-08 / UI-09 will widen the fields.
6
7/// A camera or microphone the user could pick as a capture source.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DeviceFixture {
10    /// Stable id (e.g. `"built-in-mic"`); matches the underlying OS handle
11    /// in production but is just a string here.
12    pub id: &'static str,
13    /// Human label shown in pickers.
14    pub label: &'static str,
15    /// Connection / availability hint shown in muted text under the label.
16    pub hint: &'static str,
17    /// Whether the device is currently selected (`true` = filled radio).
18    pub selected: bool,
19}
20
21/// A system-audio source — an application emitting audio, or "all desktop
22/// audio". UI-09 expands this with per-app icons and grouping.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SystemAudioFixture {
25    /// Stable id.
26    pub id: &'static str,
27    /// Display label ("Spotify", "Desktop audio").
28    pub label: &'static str,
29    /// `true` when this row is in the active selection set.
30    pub selected: bool,
31}
32
33/// Sample microphones for stories that need a populated picker.
34#[must_use]
35pub fn sample_microphones() -> Vec<DeviceFixture> {
36    vec![
37        DeviceFixture {
38            id: "mic-built-in",
39            label: "Built-in microphone",
40            hint: "MacBook · Default",
41            selected: true,
42        },
43        DeviceFixture {
44            id: "mic-airpods",
45            label: "AirPods Pro",
46            hint: "Bluetooth · 86% battery",
47            selected: false,
48        },
49        DeviceFixture {
50            id: "mic-shure",
51            label: "Shure MV7",
52            hint: "USB · Connected",
53            selected: false,
54        },
55    ]
56}
57
58/// Sample cameras for stories that need a populated picker.
59#[must_use]
60pub fn sample_cameras() -> Vec<DeviceFixture> {
61    vec![
62        DeviceFixture {
63            id: "cam-facetime",
64            label: "FaceTime HD camera",
65            hint: "MacBook · 1080p",
66            selected: true,
67        },
68        DeviceFixture {
69            id: "cam-iphone",
70            label: "iPhone 15 Pro",
71            hint: "Continuity Camera · 4K",
72            selected: false,
73        },
74    ]
75}
76
77/// Sample display source view used by the `DisplaySourceCard`
78/// stories.
79#[must_use]
80pub fn sample_display_source(is_selected: bool) -> crate::components::recorder::DisplaySourceView {
81    use crate::components::recorder::{DisplayPreviewView, DisplaySourceView, PreviewWindowChip};
82    DisplaySourceView {
83        id: "display-built-in".to_owned(),
84        name: "Built-in Retina display".to_owned(),
85        size_label: "14\"".to_owned(),
86        dimensions_label: "3024 × 1964".to_owned(),
87        is_favorite: true,
88        is_selected,
89        preview: DisplayPreviewView {
90            aspect_ratio: (16, 10),
91            overlay_label: Some("Built-in 14\"".to_owned()),
92            mock_windows: vec![
93                PreviewWindowChip {
94                    label: "Safari".to_owned(),
95                    color: "rgba(99, 102, 241, 0.85)".to_owned(),
96                    left_pct: 6,
97                    top_pct: 12,
98                    width_pct: 56,
99                    height_pct: 70,
100                },
101                PreviewWindowChip {
102                    label: "Terminal".to_owned(),
103                    color: "rgba(15, 15, 15, 0.85)".to_owned(),
104                    left_pct: 56,
105                    top_pct: 28,
106                    width_pct: 36,
107                    height_pct: 50,
108                },
109                PreviewWindowChip {
110                    label: "Notes".to_owned(),
111                    color: "rgba(250, 204, 21, 0.40)".to_owned(),
112                    left_pct: 16,
113                    top_pct: 40,
114                    width_pct: 32,
115                    height_pct: 30,
116                },
117            ],
118        },
119    }
120}
121
122/// Wide variant — ultrawide aspect with two windows.
123#[must_use]
124pub fn sample_display_source_wide() -> crate::components::recorder::DisplaySourceView {
125    use crate::components::recorder::{DisplayPreviewView, DisplaySourceView, PreviewWindowChip};
126    DisplaySourceView {
127        id: "display-ultrawide".to_owned(),
128        name: "LG UltraWide".to_owned(),
129        size_label: "34\"".to_owned(),
130        dimensions_label: "3440 × 1440".to_owned(),
131        is_favorite: false,
132        is_selected: true,
133        preview: DisplayPreviewView {
134            aspect_ratio: (21, 9),
135            overlay_label: Some("LG 34\"".to_owned()),
136            mock_windows: vec![
137                PreviewWindowChip {
138                    label: "Zoom".to_owned(),
139                    color: "rgba(56, 189, 248, 0.80)".to_owned(),
140                    left_pct: 4,
141                    top_pct: 12,
142                    width_pct: 42,
143                    height_pct: 75,
144                },
145                PreviewWindowChip {
146                    label: "Slack".to_owned(),
147                    color: "rgba(244, 114, 182, 0.65)".to_owned(),
148                    left_pct: 52,
149                    top_pct: 12,
150                    width_pct: 44,
151                    height_pct: 75,
152                },
153            ],
154        },
155    }
156}
157
158/// Small / 4:3 variant.
159#[must_use]
160pub fn sample_display_source_small() -> crate::components::recorder::DisplaySourceView {
161    use crate::components::recorder::{DisplayPreviewView, DisplaySourceView, PreviewWindowChip};
162    DisplaySourceView {
163        id: "display-cinema".to_owned(),
164        name: "Apple Cinema".to_owned(),
165        size_label: "20\"".to_owned(),
166        dimensions_label: "1680 × 1050".to_owned(),
167        is_favorite: false,
168        is_selected: false,
169        preview: DisplayPreviewView {
170            aspect_ratio: (16, 10),
171            overlay_label: None,
172            mock_windows: vec![PreviewWindowChip {
173                label: "Finder".to_owned(),
174                color: "rgba(15, 15, 15, 0.7)".to_owned(),
175                left_pct: 22,
176                top_pct: 25,
177                width_pct: 56,
178                height_pct: 55,
179            }],
180        },
181    }
182}
183
184/// Sample `CaptureSourceView` for the camera row in collapsed state.
185#[must_use]
186pub fn sample_capture_source_camera(
187    enabled: bool,
188    expanded: bool,
189) -> crate::components::recorder::CaptureSourceView {
190    use crate::components::recorder::{CaptureSourceKind, CaptureSourceView};
191    CaptureSourceView {
192        id: "camera-facetime".to_owned(),
193        kind: CaptureSourceKind::Camera,
194        title: "FaceTime HD Camera".to_owned(),
195        subtitle: "MacBook · 1080p".to_owned(),
196        enabled,
197        expanded,
198        favorite: false,
199        level: None,
200    }
201}
202
203/// Sample `CaptureSourceView` for the microphone row in collapsed state.
204#[must_use]
205pub fn sample_capture_source_microphone(
206    enabled: bool,
207    expanded: bool,
208    level: Option<f32>,
209) -> crate::components::recorder::CaptureSourceView {
210    use crate::components::recorder::{CaptureSourceKind, CaptureSourceView};
211    CaptureSourceView {
212        id: "mic-built-in".to_owned(),
213        kind: CaptureSourceKind::Microphone,
214        title: "MacBook Pro Microphone".to_owned(),
215        subtitle: "Built-in · 48 kHz".to_owned(),
216        enabled,
217        expanded,
218        favorite: true,
219        level,
220    }
221}
222
223/// Sample camera options for the expanded picker.
224#[must_use]
225pub fn sample_camera_options() -> Vec<crate::components::recorder::DeviceOptionView> {
226    use crate::components::recorder::{DeviceOptionView, DeviceThumb};
227    vec![
228        DeviceOptionView {
229            id: "cam-facetime".to_owned(),
230            name: "FaceTime HD Camera".to_owned(),
231            detail: "MacBook · 1080p".to_owned(),
232            badge: None,
233            selected: true,
234            level: None,
235            thumbnail: Some(DeviceThumb {
236                background: "linear-gradient(135deg, #4338ca, #db2777)".to_owned(),
237                glyph: "PE".to_owned(),
238            }),
239        },
240        DeviceOptionView {
241            id: "cam-iphone".to_owned(),
242            name: "iPhone 15 Pro".to_owned(),
243            detail: "Continuity · 4K".to_owned(),
244            badge: Some("Wireless".to_owned()),
245            selected: false,
246            level: None,
247            thumbnail: Some(DeviceThumb {
248                background: "linear-gradient(135deg, #0ea5e9, #1e293b)".to_owned(),
249                glyph: "iP".to_owned(),
250            }),
251        },
252        DeviceOptionView {
253            id: "cam-obs".to_owned(),
254            name: "OBS Virtual Camera".to_owned(),
255            detail: "Not connected".to_owned(),
256            badge: None,
257            selected: false,
258            level: None,
259            thumbnail: Some(DeviceThumb {
260                background: "linear-gradient(135deg, #525252, #1f1f23)".to_owned(),
261                glyph: "OB".to_owned(),
262            }),
263        },
264    ]
265}
266
267/// Sample microphone options for the expanded picker.
268#[must_use]
269pub fn sample_microphone_options() -> Vec<crate::components::recorder::DeviceOptionView> {
270    use crate::components::recorder::DeviceOptionView;
271    vec![
272        DeviceOptionView {
273            id: "mic-built-in".to_owned(),
274            name: "MacBook Pro Microphone".to_owned(),
275            detail: "Built-in · 48 kHz".to_owned(),
276            badge: None,
277            selected: true,
278            level: Some(0.32),
279            thumbnail: None,
280        },
281        DeviceOptionView {
282            id: "mic-airpods".to_owned(),
283            name: "AirPods Pro".to_owned(),
284            detail: "Bluetooth · 86% battery".to_owned(),
285            badge: Some("Wireless".to_owned()),
286            selected: false,
287            level: Some(0.12),
288            thumbnail: None,
289        },
290        DeviceOptionView {
291            id: "mic-shure".to_owned(),
292            name: "Shure MV7".to_owned(),
293            detail: "USB · Connected".to_owned(),
294            badge: None,
295            selected: false,
296            level: Some(0.55),
297            thumbnail: None,
298        },
299    ]
300}
301
302/// Sample system-audio rows for the system-audio picker.
303#[must_use]
304pub fn sample_system_audio() -> Vec<SystemAudioFixture> {
305    vec![
306        SystemAudioFixture {
307            id: "audio-desktop",
308            label: "All desktop audio",
309            selected: true,
310        },
311        SystemAudioFixture {
312            id: "audio-spotify",
313            label: "Spotify",
314            selected: false,
315        },
316        SystemAudioFixture {
317            id: "audio-zoom",
318            label: "Zoom",
319            selected: false,
320        },
321    ]
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn microphones_non_empty_and_has_a_default() {
330        let mics = sample_microphones();
331        assert!(!mics.is_empty(), "fixture must populate at least one mic");
332        assert!(mics.iter().any(|m| m.selected), "exactly one mic selected");
333    }
334
335    #[test]
336    fn cameras_non_empty() {
337        assert!(!sample_cameras().is_empty());
338    }
339
340    #[test]
341    fn system_audio_non_empty() {
342        assert!(!sample_system_audio().is_empty());
343    }
344}