Skip to main content

ui_storybook/components/recorder/
display_source.rs

1//! `DisplaySourceCard` + `DisplayPreviewFrame` — selected screen card
2//! shown in the tray record popover after capture-mode selection
3//! (M-UI.7 / AUT-127).
4//!
5//! Pure presentation. SSR-stable: the preview is rendered as
6//! CSS-positioned divs (the "deterministic non-Wisp fallback" called
7//! out in the spec). A Wisp-backed PNG variant can land later via the
8//! existing `wisp-export-stories` harness.
9
10use leptos::prelude::*;
11
12/// View-model for the card.
13#[derive(Debug, Clone, PartialEq)]
14pub struct DisplaySourceView {
15    /// Stable id.
16    pub id: String,
17    /// Display name ("Built-in Retina display").
18    pub name: String,
19    /// Compact size label ("14\"").
20    pub size_label: String,
21    /// Resolution label ("3024 × 1964").
22    pub dimensions_label: String,
23    /// `true` to render the favourited star glyph.
24    pub is_favorite: bool,
25    /// `true` to outline the card in the action-record color.
26    pub is_selected: bool,
27    /// Preview content shown inside the card.
28    pub preview: DisplayPreviewView,
29}
30
31/// Visual preview content for the screen.
32#[derive(Debug, Clone, PartialEq)]
33pub struct DisplayPreviewView {
34    /// Aspect ratio numerator + denominator (e.g. `(16, 10)` for the
35    /// Retina display). Drives the `aspect-ratio:` CSS property.
36    pub aspect_ratio: (u16, u16),
37    /// Optional overlay label drawn in the top-right corner ("Built-in
38    /// 14\"").
39    pub overlay_label: Option<String>,
40    /// Mock window chips drawn inside the preview to suggest "this is
41    /// what would be captured".
42    pub mock_windows: Vec<PreviewWindowChip>,
43}
44
45/// One mock window chip — drawn as a colored rounded rect at a
46/// percentage offset / size inside the preview.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PreviewWindowChip {
49    /// Display label (truncates).
50    pub label: String,
51    /// CSS background color string.
52    pub color: String,
53    /// Left offset in `[0, 100]` percent.
54    pub left_pct: u8,
55    /// Top offset in `[0, 100]` percent.
56    pub top_pct: u8,
57    /// Width in `[0, 100]` percent.
58    pub width_pct: u8,
59    /// Height in `[0, 100]` percent.
60    pub height_pct: u8,
61}
62
63#[component]
64pub fn DisplayPreviewFrame(view: DisplayPreviewView) -> impl IntoView {
65    let (num, den) = view.aspect_ratio;
66    let aspect_style = format!("aspect-ratio: {num} / {den}");
67    view! {
68        <div class="display-preview" style=aspect_style>
69            <div class="display-preview-titlebar">
70                <span class="display-preview-tlbtn display-preview-tlbtn-red"></span>
71                <span class="display-preview-tlbtn display-preview-tlbtn-amber"></span>
72                <span class="display-preview-tlbtn display-preview-tlbtn-green"></span>
73            </div>
74            <div class="display-preview-body">
75                {view.mock_windows.into_iter().map(|chip| {
76                    let style = format!(
77                        "left:{}%;top:{}%;width:{}%;height:{}%;background:{};",
78                        chip.left_pct, chip.top_pct, chip.width_pct, chip.height_pct, chip.color,
79                    );
80                    view! {
81                        <span class="display-preview-chip" style=style>
82                            <span class="display-preview-chip-label">{chip.label}</span>
83                        </span>
84                    }
85                }).collect_view()}
86                {view.overlay_label.as_ref().map(|l| {
87                    let label = l.clone();
88                    view! {
89                        <span class="display-preview-overlay">{label}</span>
90                    }
91                })}
92            </div>
93        </div>
94    }
95}
96
97#[component]
98pub fn DisplaySourceCard(
99    view: DisplaySourceView,
100    /// `true` when the parent's source-picker popover is open.
101    #[prop(optional)]
102    open: bool,
103    /// `true` to render the card disabled (e.g. permissions blocked).
104    #[prop(optional)]
105    unavailable: bool,
106) -> impl IntoView {
107    let mut card_class = String::from("display-source-card");
108    if view.is_selected {
109        card_class.push_str(" display-source-card-selected");
110    }
111    if unavailable {
112        card_class.push_str(" display-source-card-unavailable");
113    }
114    let mut chevron_class = String::from("display-source-chevron");
115    if open {
116        chevron_class.push_str(" display-source-chevron-open");
117    }
118    let name = view.name;
119    let size = view.size_label;
120    let dims = view.dimensions_label;
121    let favorite = view.is_favorite;
122    let preview = view.preview.clone();
123    view! {
124        <div class=card_class>
125            <header class="display-source-header">
126                <div class="display-source-label">
127                    <span class="display-source-name">{name}</span>
128                    <span class="display-source-size">{size}</span>
129                    {favorite.then(|| view! { <span class="display-source-star" aria-label="Favourite" title="Favourite">"★"</span> })}
130                </div>
131                <div class="display-source-meta">
132                    <span class="display-source-dims">{dims}</span>
133                    <span class=chevron_class aria-hidden="true">"▾"</span>
134                </div>
135            </header>
136            <DisplayPreviewFrame view=preview />
137            {unavailable.then(|| view! {
138                <div class="display-source-unavailable-banner">
139                    "Screen recording permission required"
140                </div>
141            })}
142        </div>
143    }
144}
145
146/// CSS `aspect-ratio` property value for a `(num, den)` pair, with
147/// `1 / 1` fallback for zero denominator.
148#[must_use]
149pub fn aspect_ratio_css(num: u16, den: u16) -> String {
150    if den == 0 {
151        "1 / 1".to_string()
152    } else {
153        format!("{num} / {den}")
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn aspect_ratio_css_basic() {
163        assert_eq!(aspect_ratio_css(16, 9), "16 / 9");
164        assert_eq!(aspect_ratio_css(3024, 1964), "3024 / 1964");
165    }
166
167    #[test]
168    fn aspect_ratio_css_zero_denominator_falls_back() {
169        assert_eq!(aspect_ratio_css(16, 0), "1 / 1");
170    }
171}