theme_preview.rs

  1#![allow(unused, dead_code)]
  2use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla};
  3use strum::IntoEnumIterator;
  4use theme::all_theme_colors;
  5use ui::{
  6    prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar,
  7    AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, ElevationIndex,
  8    Facepile, Indicator, TintColor, Tooltip,
  9};
 10
 11use crate::{Item, Workspace};
 12
 13actions!(debug, [OpenThemePreview]);
 14
 15pub fn init(cx: &mut AppContext) {
 16    cx.observe_new_views(|workspace: &mut Workspace, _| {
 17        workspace.register_action(|workspace, _: &OpenThemePreview, cx| {
 18            let theme_preview = cx.new_view(ThemePreview::new);
 19            workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, cx)
 20        });
 21    })
 22    .detach();
 23}
 24
 25#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum::EnumIter)]
 26enum ThemePreviewPage {
 27    Overview,
 28    Typography,
 29    Components,
 30}
 31
 32impl ThemePreviewPage {
 33    pub fn name(&self) -> &'static str {
 34        match self {
 35            Self::Overview => "Overview",
 36            Self::Typography => "Typography",
 37            Self::Components => "Components",
 38        }
 39    }
 40}
 41
 42struct ThemePreview {
 43    current_page: ThemePreviewPage,
 44    focus_handle: FocusHandle,
 45}
 46
 47impl ThemePreview {
 48    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 49        Self {
 50            current_page: ThemePreviewPage::Overview,
 51            focus_handle: cx.focus_handle(),
 52        }
 53    }
 54
 55    pub fn view(
 56        &self,
 57        page: ThemePreviewPage,
 58        cx: &mut ViewContext<ThemePreview>,
 59    ) -> impl IntoElement {
 60        match page {
 61            ThemePreviewPage::Overview => self.render_overview_page(cx).into_any_element(),
 62            ThemePreviewPage::Typography => self.render_typography_page(cx).into_any_element(),
 63            ThemePreviewPage::Components => self.render_components_page(cx).into_any_element(),
 64        }
 65    }
 66}
 67
 68impl EventEmitter<()> for ThemePreview {}
 69
 70impl FocusableView for ThemePreview {
 71    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
 72        self.focus_handle.clone()
 73    }
 74}
 75impl ThemePreview {}
 76
 77impl Item for ThemePreview {
 78    type Event = ();
 79
 80    fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {}
 81
 82    fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
 83        let name = cx.theme().name.clone();
 84        Some(format!("{} Preview", name).into())
 85    }
 86
 87    fn telemetry_event_text(&self) -> Option<&'static str> {
 88        None
 89    }
 90
 91    fn clone_on_split(
 92        &self,
 93        _workspace_id: Option<crate::WorkspaceId>,
 94        cx: &mut ViewContext<Self>,
 95    ) -> Option<gpui::View<Self>>
 96    where
 97        Self: Sized,
 98    {
 99        Some(cx.new_view(Self::new))
100    }
101}
102
103const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4";
104
105impl ThemePreview {
106    fn preview_bg(cx: &WindowContext) -> Hsla {
107        cx.theme().colors().editor_background
108    }
109
110    fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
111        v_flex()
112            .gap_1()
113            .child(
114                Headline::new("Avatars")
115                    .size(HeadlineSize::Small)
116                    .color(Color::Muted),
117            )
118            .child(
119                h_flex()
120                    .items_start()
121                    .gap_4()
122                    .child(Avatar::new(AVATAR_URL).size(px(24.)))
123                    .child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true))
124                    .child(
125                        Avatar::new(AVATAR_URL)
126                            .size(px(24.))
127                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
128                    )
129                    .child(
130                        Avatar::new(AVATAR_URL)
131                            .size(px(24.))
132                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
133                    )
134                    .child(
135                        Avatar::new(AVATAR_URL)
136                            .size(px(24.))
137                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
138                    )
139                    .child(
140                        Avatar::new(AVATAR_URL)
141                            .size(px(24.))
142                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
143                    )
144                    .child(
145                        Facepile::empty()
146                            .child(
147                                Avatar::new(AVATAR_URL)
148                                    .border_color(Self::preview_bg(cx))
149                                    .size(px(22.))
150                                    .into_any_element(),
151                            )
152                            .child(
153                                Avatar::new(AVATAR_URL)
154                                    .border_color(Self::preview_bg(cx))
155                                    .size(px(22.))
156                                    .into_any_element(),
157                            )
158                            .child(
159                                Avatar::new(AVATAR_URL)
160                                    .border_color(Self::preview_bg(cx))
161                                    .size(px(22.))
162                                    .into_any_element(),
163                            )
164                            .child(
165                                Avatar::new(AVATAR_URL)
166                                    .border_color(Self::preview_bg(cx))
167                                    .size(px(22.))
168                                    .into_any_element(),
169                            ),
170                    ),
171            )
172    }
173
174    fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
175        v_flex()
176            .gap_1()
177            .child(
178                Headline::new("Buttons")
179                    .size(HeadlineSize::Small)
180                    .color(Color::Muted),
181            )
182            .child(
183                h_flex()
184                    .items_start()
185                    .gap_px()
186                    .child(
187                        IconButton::new("icon_button_transparent", IconName::Check)
188                            .style(ButtonStyle::Transparent),
189                    )
190                    .child(
191                        IconButton::new("icon_button_subtle", IconName::Check)
192                            .style(ButtonStyle::Subtle),
193                    )
194                    .child(
195                        IconButton::new("icon_button_filled", IconName::Check)
196                            .style(ButtonStyle::Filled),
197                    )
198                    .child(
199                        IconButton::new("icon_button_selected_accent", IconName::Check)
200                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
201                            .selected(true),
202                    )
203                    .child(IconButton::new("icon_button_selected", IconName::Check).selected(true))
204                    .child(
205                        IconButton::new("icon_button_positive", IconName::Check)
206                            .style(ButtonStyle::Tinted(TintColor::Positive)),
207                    )
208                    .child(
209                        IconButton::new("icon_button_warning", IconName::Check)
210                            .style(ButtonStyle::Tinted(TintColor::Warning)),
211                    )
212                    .child(
213                        IconButton::new("icon_button_negative", IconName::Check)
214                            .style(ButtonStyle::Tinted(TintColor::Negative)),
215                    ),
216            )
217            .child(
218                h_flex()
219                    .gap_px()
220                    .child(
221                        Button::new("button_transparent", "Transparent")
222                            .style(ButtonStyle::Transparent),
223                    )
224                    .child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle))
225                    .child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled))
226                    .child(
227                        Button::new("button_selected", "Selected")
228                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
229                            .selected(true),
230                    )
231                    .child(
232                        Button::new("button_selected_tinted", "Selected (Tinted)")
233                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
234                            .selected(true),
235                    )
236                    .child(
237                        Button::new("button_positive", "Tint::Positive")
238                            .style(ButtonStyle::Tinted(TintColor::Positive)),
239                    )
240                    .child(
241                        Button::new("button_warning", "Tint::Warning")
242                            .style(ButtonStyle::Tinted(TintColor::Warning)),
243                    )
244                    .child(
245                        Button::new("button_negative", "Tint::Negative")
246                            .style(ButtonStyle::Tinted(TintColor::Negative)),
247                    ),
248            )
249    }
250
251    fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
252        let bg = layer.bg(cx);
253
254        let label_with_contrast = |label: &str, fg: Hsla| {
255            let contrast = calculate_contrast_ratio(fg, bg);
256            format!("{} ({:.2})", label, contrast)
257        };
258
259        v_flex()
260            .gap_1()
261            .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted))
262            .child(
263                h_flex()
264                    .items_start()
265                    .gap_4()
266                    .child(
267                        v_flex()
268                            .gap_1()
269                            .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted))
270                            .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge))
271                            .child(Headline::new("Large Headline").size(HeadlineSize::Large))
272                            .child(Headline::new("Medium Headline").size(HeadlineSize::Medium))
273                            .child(Headline::new("Small Headline").size(HeadlineSize::Small))
274                            .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)),
275                    )
276                    .child(
277                        v_flex()
278                            .gap_1()
279                            .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted))
280                            .child(
281                                Label::new(label_with_contrast(
282                                    "Default Text",
283                                    Color::Default.color(cx),
284                                ))
285                                .color(Color::Default),
286                            )
287                            .child(
288                                Label::new(label_with_contrast(
289                                    "Accent Text",
290                                    Color::Accent.color(cx),
291                                ))
292                                .color(Color::Accent),
293                            )
294                            .child(
295                                Label::new(label_with_contrast(
296                                    "Conflict Text",
297                                    Color::Conflict.color(cx),
298                                ))
299                                .color(Color::Conflict),
300                            )
301                            .child(
302                                Label::new(label_with_contrast(
303                                    "Created Text",
304                                    Color::Created.color(cx),
305                                ))
306                                .color(Color::Created),
307                            )
308                            .child(
309                                Label::new(label_with_contrast(
310                                    "Deleted Text",
311                                    Color::Deleted.color(cx),
312                                ))
313                                .color(Color::Deleted),
314                            )
315                            .child(
316                                Label::new(label_with_contrast(
317                                    "Disabled Text",
318                                    Color::Disabled.color(cx),
319                                ))
320                                .color(Color::Disabled),
321                            )
322                            .child(
323                                Label::new(label_with_contrast(
324                                    "Error Text",
325                                    Color::Error.color(cx),
326                                ))
327                                .color(Color::Error),
328                            )
329                            .child(
330                                Label::new(label_with_contrast(
331                                    "Hidden Text",
332                                    Color::Hidden.color(cx),
333                                ))
334                                .color(Color::Hidden),
335                            )
336                            .child(
337                                Label::new(label_with_contrast(
338                                    "Hint Text",
339                                    Color::Hint.color(cx),
340                                ))
341                                .color(Color::Hint),
342                            )
343                            .child(
344                                Label::new(label_with_contrast(
345                                    "Ignored Text",
346                                    Color::Ignored.color(cx),
347                                ))
348                                .color(Color::Ignored),
349                            )
350                            .child(
351                                Label::new(label_with_contrast(
352                                    "Info Text",
353                                    Color::Info.color(cx),
354                                ))
355                                .color(Color::Info),
356                            )
357                            .child(
358                                Label::new(label_with_contrast(
359                                    "Modified Text",
360                                    Color::Modified.color(cx),
361                                ))
362                                .color(Color::Modified),
363                            )
364                            .child(
365                                Label::new(label_with_contrast(
366                                    "Muted Text",
367                                    Color::Muted.color(cx),
368                                ))
369                                .color(Color::Muted),
370                            )
371                            .child(
372                                Label::new(label_with_contrast(
373                                    "Placeholder Text",
374                                    Color::Placeholder.color(cx),
375                                ))
376                                .color(Color::Placeholder),
377                            )
378                            .child(
379                                Label::new(label_with_contrast(
380                                    "Selected Text",
381                                    Color::Selected.color(cx),
382                                ))
383                                .color(Color::Selected),
384                            )
385                            .child(
386                                Label::new(label_with_contrast(
387                                    "Success Text",
388                                    Color::Success.color(cx),
389                                ))
390                                .color(Color::Success),
391                            )
392                            .child(
393                                Label::new(label_with_contrast(
394                                    "Warning Text",
395                                    Color::Warning.color(cx),
396                                ))
397                                .color(Color::Warning),
398                            )
399                    )
400                    .child(
401                        v_flex()
402                            .gap_1()
403                            .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted))
404                            .child(
405                                div().max_w(px(200.)).child(
406                                "This is a longer piece of text that should wrap to multiple lines. It demonstrates how text behaves when it exceeds the width of its container."
407                            ))
408                    )
409            )
410    }
411
412    fn render_colors(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
413        let bg = layer.bg(cx);
414        let all_colors = all_theme_colors(cx);
415
416        v_flex()
417            .gap_1()
418            .child(
419                Headline::new("Colors")
420                    .size(HeadlineSize::Small)
421                    .color(Color::Muted),
422            )
423            .child(
424                h_flex()
425                    .flex_wrap()
426                    .gap_1()
427                    .children(all_colors.into_iter().map(|(color, name)| {
428                        let id = ElementId::Name(format!("{:?}-preview", color).into());
429                        let name = name.clone();
430                        div().size_8().flex_none().child(
431                            ButtonLike::new(id)
432                                .child(
433                                    div()
434                                        .size_8()
435                                        .bg(color)
436                                        .border_1()
437                                        .border_color(cx.theme().colors().border)
438                                        .overflow_hidden(),
439                                )
440                                .size(ButtonSize::None)
441                                .style(ButtonStyle::Transparent)
442                                .tooltip(move |cx| {
443                                    let name = name.clone();
444                                    Tooltip::with_meta(name, None, format!("{:?}", color), cx)
445                                }),
446                        )
447                    })),
448            )
449    }
450
451    fn render_theme_layer(
452        &self,
453        layer: ElevationIndex,
454        cx: &ViewContext<Self>,
455    ) -> impl IntoElement {
456        v_flex()
457            .p_4()
458            .bg(layer.bg(cx))
459            .text_color(cx.theme().colors().text)
460            .gap_2()
461            .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium))
462            .child(self.render_text(layer, cx))
463            .child(self.render_colors(layer, cx))
464    }
465
466    fn render_overview_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
467        v_flex()
468            .id("theme-preview-overview")
469            .overflow_scroll()
470            .size_full()
471            .child(
472                v_flex()
473                    .child(Headline::new("Theme Preview").size(HeadlineSize::Large))
474                    .child(div().w_full().text_color(cx.theme().colors().text_muted).child("This view lets you preview a range of UI elements across a theme. Use it for testing out changes to the theme."))
475                    )
476            .child(self.render_theme_layer(ElevationIndex::Background, cx))
477            .child(self.render_theme_layer(ElevationIndex::Surface, cx))
478            .child(self.render_theme_layer(ElevationIndex::EditorSurface, cx))
479            .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, cx))
480    }
481
482    fn render_typography_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
483        v_flex()
484            .id("theme-preview-typography")
485            .overflow_scroll()
486            .size_full()
487            .child(v_flex()
488                .gap_4()
489                .child(Headline::new("Headline 1").size(HeadlineSize::XLarge))
490                .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."))
491                .child(Headline::new("Headline 2").size(HeadlineSize::Large))
492                .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
493                .child(Headline::new("Headline 3").size(HeadlineSize::Medium))
494                .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."))
495                .child(Headline::new("Headline 4").size(HeadlineSize::Small))
496                .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
497                .child(Headline::new("Headline 5").size(HeadlineSize::XSmall))
498                .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
499                .child(Headline::new("Body Text").size(HeadlineSize::Small))
500                .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
501            )
502    }
503
504    fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
505        let layer = ElevationIndex::Surface;
506
507        v_flex()
508            .id("theme-preview-components")
509            .overflow_scroll()
510            .size_full()
511            .gap_2()
512            .child(Checkbox::render_component_previews(cx))
513            .child(Facepile::render_component_previews(cx))
514            .child(Button::render_component_previews(cx))
515            .child(Indicator::render_component_previews(cx))
516            .child(Icon::render_component_previews(cx))
517            .child(self.render_avatars(cx))
518            .child(self.render_buttons(layer, cx))
519    }
520
521    fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
522        h_flex()
523            .id("theme-preview-nav")
524            .items_center()
525            .gap_4()
526            .py_2()
527            .bg(Self::preview_bg(cx))
528            .children(ThemePreviewPage::iter().map(|p| {
529                Button::new(ElementId::Name(p.name().into()), p.name())
530                    .on_click(cx.listener(move |this, _, cx| {
531                        this.current_page = p;
532                        cx.notify();
533                    }))
534                    .selected(p == self.current_page)
535                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
536            }))
537    }
538}
539
540impl Render for ThemePreview {
541    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
542        v_flex()
543            .id("theme-preview")
544            .key_context("ThemePreview")
545            .items_start()
546            .overflow_hidden()
547            .size_full()
548            .max_h_full()
549            .track_focus(&self.focus_handle)
550            .px_2()
551            .bg(Self::preview_bg(cx))
552            .child(self.render_page_nav(cx))
553            .child(self.view(self.current_page, cx))
554    }
555}