theme_preview.rs

  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}