1#![allow(unused, dead_code)]
2use gpui::{Hsla, Length};
3use std::{
4 cell::LazyCell,
5 sync::{Arc, LazyLock, OnceLock},
6};
7use theme::{Theme, ThemeColors, ThemeRegistry};
8use ui::{
9 IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius,
10};
11
12#[derive(Clone, PartialEq)]
13pub enum ThemePreviewStyle {
14 Bordered,
15 Borderless,
16 SideBySide(Arc<Theme>),
17}
18
19/// Shows a preview of a theme as an abstract illustration
20/// of a thumbnail-sized editor.
21#[derive(IntoElement, RegisterComponent, Documented)]
22pub struct ThemePreviewTile {
23 theme: Arc<Theme>,
24 seed: f32,
25 style: ThemePreviewStyle,
26}
27
28static CHILD_RADIUS: LazyLock<Pixels> = LazyLock::new(|| {
29 inner_corner_radius(
30 ThemePreviewTile::ROOT_RADIUS,
31 ThemePreviewTile::ROOT_BORDER,
32 ThemePreviewTile::ROOT_PADDING,
33 ThemePreviewTile::CHILD_BORDER,
34 )
35});
36
37impl ThemePreviewTile {
38 pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.);
39 pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8;
40 pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25);
41 pub const ROOT_RADIUS: Pixels = px(8.0);
42 pub const ROOT_BORDER: Pixels = px(2.0);
43 pub const ROOT_PADDING: Pixels = px(2.0);
44 pub const CHILD_BORDER: Pixels = px(1.0);
45
46 pub fn new(theme: Arc<Theme>, seed: f32) -> Self {
47 Self {
48 theme,
49 seed,
50 style: ThemePreviewStyle::Bordered,
51 }
52 }
53
54 pub fn style(mut self, style: ThemePreviewStyle) -> Self {
55 self.style = style;
56 self
57 }
58
59 pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement {
60 div().w(w).h(h).rounded_full().bg(bg)
61 }
62
63 pub fn render_sidebar_skeleton_items(
64 seed: f32,
65 colors: &ThemeColors,
66 skeleton_height: impl Into<Length> + Clone,
67 ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] {
68 let skeleton_height = skeleton_height.into();
69 std::array::from_fn(|index| {
70 let width = {
71 let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
72 0.5 + value * 0.45
73 };
74 Self::item_skeleton(
75 relative(width).into(),
76 skeleton_height,
77 colors.text.alpha(0.45),
78 )
79 })
80 }
81
82 pub fn render_pseudo_code_skeleton(
83 seed: f32,
84 theme: Arc<Theme>,
85 skeleton_height: impl Into<Length>,
86 ) -> impl IntoElement {
87 let colors = theme.colors();
88 let syntax = theme.syntax();
89
90 let keyword_color = syntax.get("keyword").color;
91 let function_color = syntax.get("function").color;
92 let string_color = syntax.get("string").color;
93 let comment_color = syntax.get("comment").color;
94 let variable_color = syntax.get("variable").color;
95 let type_color = syntax.get("type").color;
96 let punctuation_color = syntax.get("punctuation").color;
97
98 let syntax_colors = [
99 keyword_color,
100 function_color,
101 string_color,
102 variable_color,
103 type_color,
104 punctuation_color,
105 comment_color,
106 ];
107
108 let skeleton_height = skeleton_height.into();
109
110 let line_width = |line_idx: usize, block_idx: usize| -> f32 {
111 let val =
112 (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() * 0.5 + 0.5;
113 0.05 + val * 0.2
114 };
115
116 let indentation = |line_idx: usize| -> f32 {
117 let step = line_idx % 6;
118 if step < 3 {
119 step as f32 * 0.1
120 } else {
121 (5 - step) as f32 * 0.1
122 }
123 };
124
125 let pick_color = |line_idx: usize, block_idx: usize| -> Hsla {
126 let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() * 3.5)
127 .abs() as usize
128 % syntax_colors.len();
129 syntax_colors[idx].unwrap_or(colors.text)
130 };
131
132 let line_count = 13;
133
134 let lines = (0..line_count)
135 .map(|line_idx| {
136 let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) * 3.0)
137 .round() as usize
138 + 2;
139
140 let indent = indentation(line_idx);
141
142 let blocks = (0..block_count)
143 .map(|block_idx| {
144 let width = line_width(line_idx, block_idx);
145 let color = pick_color(line_idx, block_idx);
146 Self::item_skeleton(relative(width).into(), skeleton_height, color)
147 })
148 .collect::<Vec<_>>();
149
150 h_flex().gap(px(2.)).ml(relative(indent)).children(blocks)
151 })
152 .collect::<Vec<_>>();
153
154 v_flex().size_full().p_1().gap_1p5().children(lines)
155 }
156
157 pub fn render_sidebar(
158 seed: f32,
159 colors: &ThemeColors,
160 width: impl Into<Length> + Clone,
161 skeleton_height: impl Into<Length>,
162 ) -> impl IntoElement {
163 div()
164 .h_full()
165 .w(width)
166 .border_r(px(1.))
167 .border_color(colors.border_transparent)
168 .bg(colors.panel_background)
169 .child(v_flex().p_2().size_full().gap_1().children(
170 Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()),
171 ))
172 }
173
174 pub fn render_pane(
175 seed: f32,
176 theme: Arc<Theme>,
177 skeleton_height: impl Into<Length>,
178 ) -> impl IntoElement {
179 v_flex().h_full().flex_grow().child(
180 div()
181 .size_full()
182 .overflow_hidden()
183 .bg(theme.colors().editor_background)
184 .p_2()
185 .child(Self::render_pseudo_code_skeleton(
186 seed,
187 theme,
188 skeleton_height.into(),
189 )),
190 )
191 }
192
193 pub fn render_editor(
194 seed: f32,
195 theme: Arc<Theme>,
196 sidebar_width: impl Into<Length> + Clone,
197 skeleton_height: impl Into<Length> + Clone,
198 ) -> impl IntoElement {
199 div()
200 .size_full()
201 .flex()
202 .bg(theme.colors().background.alpha(1.00))
203 .child(Self::render_sidebar(
204 seed,
205 theme.colors(),
206 sidebar_width,
207 skeleton_height.clone(),
208 ))
209 .child(Self::render_pane(seed, theme, skeleton_height))
210 }
211
212 fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement {
213 Self::render_editor(
214 seed,
215 theme,
216 Self::SIDEBAR_WIDTH_DEFAULT,
217 Self::SKELETON_HEIGHT_DEFAULT,
218 )
219 }
220
221 fn render_border(seed: f32, theme: Arc<Theme>) -> impl IntoElement {
222 div()
223 .size_full()
224 .p(Self::ROOT_PADDING)
225 .rounded(Self::ROOT_RADIUS)
226 .child(
227 div()
228 .size_full()
229 .rounded(*CHILD_RADIUS)
230 .border(Self::CHILD_BORDER)
231 .border_color(theme.colors().border)
232 .child(Self::render_editor(
233 seed,
234 theme.clone(),
235 Self::SIDEBAR_WIDTH_DEFAULT,
236 Self::SKELETON_HEIGHT_DEFAULT,
237 )),
238 )
239 }
240
241 fn render_side_by_side(
242 seed: f32,
243 theme: Arc<Theme>,
244 other_theme: Arc<Theme>,
245 border_color: Hsla,
246 ) -> impl IntoElement {
247 let sidebar_width = relative(0.20);
248
249 div()
250 .size_full()
251 .p(Self::ROOT_PADDING)
252 .rounded(Self::ROOT_RADIUS)
253 .child(
254 h_flex()
255 .size_full()
256 .relative()
257 .rounded(*CHILD_RADIUS)
258 .border(Self::CHILD_BORDER)
259 .border_color(border_color)
260 .overflow_hidden()
261 .child(div().size_full().child(Self::render_editor(
262 seed,
263 theme,
264 sidebar_width,
265 Self::SKELETON_HEIGHT_DEFAULT,
266 )))
267 .child(
268 div()
269 .size_full()
270 .absolute()
271 .left_1_2()
272 .bg(other_theme.colors().editor_background)
273 .child(Self::render_editor(
274 seed,
275 other_theme,
276 sidebar_width,
277 Self::SKELETON_HEIGHT_DEFAULT,
278 )),
279 ),
280 )
281 .into_any_element()
282 }
283}
284
285impl RenderOnce for ThemePreviewTile {
286 fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement {
287 match self.style {
288 ThemePreviewStyle::Bordered => {
289 Self::render_border(self.seed, self.theme).into_any_element()
290 }
291 ThemePreviewStyle::Borderless => {
292 Self::render_borderless(self.seed, self.theme).into_any_element()
293 }
294 ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side(
295 self.seed,
296 self.theme,
297 other_theme,
298 _cx.theme().colors().border,
299 )
300 .into_any_element(),
301 }
302 }
303}
304
305impl Component for ThemePreviewTile {
306 fn scope() -> ComponentScope {
307 ComponentScope::Onboarding
308 }
309
310 fn name() -> &'static str {
311 "Theme Preview Tile"
312 }
313
314 fn sort_name() -> &'static str {
315 "Theme Preview Tile"
316 }
317
318 fn description() -> Option<&'static str> {
319 Some(Self::DOCS)
320 }
321
322 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
323 let theme_registry = ThemeRegistry::global(cx);
324
325 let one_dark = theme_registry.get("One Dark");
326 let one_light = theme_registry.get("One Light");
327 let gruvbox_dark = theme_registry.get("Gruvbox Dark");
328 let gruvbox_light = theme_registry.get("Gruvbox Light");
329
330 let themes_to_preview = vec![
331 one_dark.clone().ok(),
332 one_light.ok(),
333 gruvbox_dark.ok(),
334 gruvbox_light.ok(),
335 ]
336 .into_iter()
337 .flatten()
338 .collect::<Vec<_>>();
339
340 Some(
341 v_flex()
342 .gap_6()
343 .p_4()
344 .children({
345 if let Some(one_dark) = one_dark.ok() {
346 vec![example_group(vec![single_example(
347 "Default",
348 div()
349 .w(px(240.))
350 .h(px(180.))
351 .child(ThemePreviewTile::new(one_dark, 0.42))
352 .into_any_element(),
353 )])]
354 } else {
355 vec![]
356 }
357 })
358 .child(
359 example_group(vec![single_example(
360 "Default Themes",
361 h_flex()
362 .gap_4()
363 .children(
364 themes_to_preview
365 .into_iter()
366 .map(|theme| {
367 div()
368 .w(px(200.))
369 .h(px(140.))
370 .child(ThemePreviewTile::new(theme, 0.42))
371 })
372 .collect::<Vec<_>>(),
373 )
374 .into_any_element(),
375 )])
376 .grow(),
377 )
378 .into_any_element(),
379 )
380 }
381}