1use crate::{component_prelude::Documented, prelude::*, utils::inner_corner_radius};
2use gpui::{App, ClickEvent, Hsla, IntoElement, Length, RenderOnce, Window};
3use std::{rc::Rc, sync::Arc};
4use theme::{Theme, ThemeRegistry};
5
6/// Shows a preview of a theme as an abstract illustration
7/// of a thumbnail-sized editor.
8#[derive(IntoElement, RegisterComponent, Documented)]
9pub struct ThemePreviewTile {
10 theme: Arc<Theme>,
11 selected: bool,
12 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
13 seed: f32,
14}
15
16impl ThemePreviewTile {
17 pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self {
18 Self {
19 theme,
20 seed,
21 selected,
22 on_click: None,
23 }
24 }
25
26 pub fn selected(mut self, selected: bool) -> Self {
27 self.selected = selected;
28 self
29 }
30
31 pub fn on_click(
32 mut self,
33 listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
34 ) -> Self {
35 self.on_click = Some(Rc::new(listener));
36 self
37 }
38}
39
40impl RenderOnce for ThemePreviewTile {
41 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
42 let color = self.theme.colors();
43
44 let root_radius = px(8.0);
45 let root_border = px(2.0);
46 let root_padding = px(2.0);
47 let child_border = px(1.0);
48 let inner_radius =
49 inner_corner_radius(root_radius, root_border, root_padding, child_border);
50
51 let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
52
53 let skeleton_height = px(4.);
54
55 let sidebar_seeded_width = |seed: f32, index: usize| {
56 let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
57 0.5 + value * 0.45
58 };
59
60 let sidebar_skeleton_items = 8;
61
62 let sidebar_skeleton = (0..sidebar_skeleton_items)
63 .map(|i| {
64 let width = sidebar_seeded_width(self.seed, i);
65 item_skeleton(
66 relative(width).into(),
67 skeleton_height,
68 color.text.alpha(0.45),
69 )
70 })
71 .collect::<Vec<_>>();
72
73 let sidebar = div()
74 .h_full()
75 .w(relative(0.25))
76 .border_r(px(1.))
77 .border_color(color.border_transparent)
78 .bg(color.panel_background)
79 .child(
80 div()
81 .p_2()
82 .flex()
83 .flex_col()
84 .size_full()
85 .gap(px(4.))
86 .children(sidebar_skeleton),
87 );
88
89 let pseudo_code_skeleton = |theme: Arc<Theme>, seed: f32| -> AnyElement {
90 let colors = theme.colors();
91 let syntax = theme.syntax();
92
93 let keyword_color = syntax.get("keyword").color;
94 let function_color = syntax.get("function").color;
95 let string_color = syntax.get("string").color;
96 let comment_color = syntax.get("comment").color;
97 let variable_color = syntax.get("variable").color;
98 let type_color = syntax.get("type").color;
99 let punctuation_color = syntax.get("punctuation").color;
100
101 let syntax_colors = [
102 keyword_color,
103 function_color,
104 string_color,
105 variable_color,
106 type_color,
107 punctuation_color,
108 comment_color,
109 ];
110
111 let line_width = |line_idx: usize, block_idx: usize| -> f32 {
112 let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin()
113 * 0.5
114 + 0.5;
115 0.05 + val * 0.2
116 };
117
118 let indentation = |line_idx: usize| -> f32 {
119 let step = line_idx % 6;
120 if step < 3 {
121 step as f32 * 0.1
122 } else {
123 (5 - step) as f32 * 0.1
124 }
125 };
126
127 let pick_color = |line_idx: usize, block_idx: usize| -> Hsla {
128 let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin()
129 * 3.5)
130 .abs() as usize
131 % syntax_colors.len();
132 syntax_colors[idx].unwrap_or(colors.text)
133 };
134
135 let line_count = 13;
136
137 let lines = (0..line_count)
138 .map(|line_idx| {
139 let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5)
140 * 3.0)
141 .round() as usize
142 + 2;
143
144 let indent = indentation(line_idx);
145
146 let blocks = (0..block_count)
147 .map(|block_idx| {
148 let width = line_width(line_idx, block_idx);
149 let color = pick_color(line_idx, block_idx);
150 item_skeleton(relative(width).into(), skeleton_height, color)
151 })
152 .collect::<Vec<_>>();
153
154 h_flex().gap(px(2.)).ml(relative(indent)).children(blocks)
155 })
156 .collect::<Vec<_>>();
157
158 v_flex()
159 .size_full()
160 .p_1()
161 .gap(px(6.))
162 .children(lines)
163 .into_any_element()
164 };
165
166 let pane = div()
167 .h_full()
168 .flex_grow()
169 .flex()
170 .flex_col()
171 // .child(
172 // div()
173 // .w_full()
174 // .border_color(color.border)
175 // .border_b(px(1.))
176 // .h(relative(0.1))
177 // .bg(color.tab_bar_background),
178 // )
179 .child(
180 div()
181 .size_full()
182 .overflow_hidden()
183 .bg(color.editor_background)
184 .p_2()
185 .child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
186 );
187
188 let content = div().size_full().flex().child(sidebar).child(pane);
189
190 div()
191 // Note: If two theme preview tiles are rendering the same theme they'll share an ID
192 // this will mean on hover and on click events will be shared between them
193 .id(SharedString::from(self.theme.id.clone()))
194 .when_some(self.on_click.clone(), |this, on_click| {
195 this.on_click(move |event, window, cx| on_click(event, window, cx))
196 .hover(|style| style.cursor_pointer().border_color(color.element_hover))
197 })
198 .size_full()
199 .rounded(root_radius)
200 .p(root_padding)
201 .border(root_border)
202 .border_color(color.border_transparent)
203 .when(self.selected, |this| {
204 this.border_color(color.border_selected)
205 })
206 .child(
207 div()
208 .size_full()
209 .rounded(inner_radius)
210 .border(child_border)
211 .border_color(color.border)
212 .bg(color.background)
213 .child(content),
214 )
215 }
216}
217
218impl Component for ThemePreviewTile {
219 fn description() -> Option<&'static str> {
220 Some(Self::DOCS)
221 }
222
223 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
224 let theme_registry = ThemeRegistry::global(cx);
225
226 let one_dark = theme_registry.get("One Dark");
227 let one_light = theme_registry.get("One Light");
228 let gruvbox_dark = theme_registry.get("Gruvbox Dark");
229 let gruvbox_light = theme_registry.get("Gruvbox Light");
230
231 let themes_to_preview = vec![
232 one_dark.clone().ok(),
233 one_light.clone().ok(),
234 gruvbox_dark.clone().ok(),
235 gruvbox_light.clone().ok(),
236 ]
237 .into_iter()
238 .flatten()
239 .collect::<Vec<_>>();
240
241 Some(
242 v_flex()
243 .gap_6()
244 .p_4()
245 .children({
246 if let Some(one_dark) = one_dark.ok() {
247 vec![example_group(vec![
248 single_example(
249 "Default",
250 div()
251 .w(px(240.))
252 .h(px(180.))
253 .child(ThemePreviewTile::new(one_dark.clone(), false, 0.42))
254 .into_any_element(),
255 ),
256 single_example(
257 "Selected",
258 div()
259 .w(px(240.))
260 .h(px(180.))
261 .child(ThemePreviewTile::new(one_dark, true, 0.42))
262 .into_any_element(),
263 ),
264 ])]
265 } else {
266 vec![]
267 }
268 })
269 .child(
270 example_group(vec![single_example(
271 "Default Themes",
272 h_flex()
273 .gap_4()
274 .children(
275 themes_to_preview
276 .iter()
277 .enumerate()
278 .map(|(_, theme)| {
279 div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new(
280 theme.clone(),
281 false,
282 0.42,
283 ))
284 })
285 .collect::<Vec<_>>(),
286 )
287 .into_any_element(),
288 )])
289 .grow(),
290 )
291 .into_any_element(),
292 )
293 }
294}