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