theme_preview.rs

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