ui_storybook/components/recorder/
display_source.rs1use leptos::prelude::*;
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct DisplaySourceView {
15 pub id: String,
17 pub name: String,
19 pub size_label: String,
21 pub dimensions_label: String,
23 pub is_favorite: bool,
25 pub is_selected: bool,
27 pub preview: DisplayPreviewView,
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct DisplayPreviewView {
34 pub aspect_ratio: (u16, u16),
37 pub overlay_label: Option<String>,
40 pub mock_windows: Vec<PreviewWindowChip>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PreviewWindowChip {
49 pub label: String,
51 pub color: String,
53 pub left_pct: u8,
55 pub top_pct: u8,
57 pub width_pct: u8,
59 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 #[prop(optional)]
102 open: bool,
103 #[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#[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}