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, DecoratedIcon, ElevationIndex, Facepile, IconDecoration,
9 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(IconDecoration::render_component_previews(cx))
514 .child(DecoratedIcon::render_component_previews(cx))
515 .child(Checkbox::render_component_previews(cx))
516 .child(CheckboxWithLabel::render_component_previews(cx))
517 .child(Facepile::render_component_previews(cx))
518 .child(Button::render_component_previews(cx))
519 .child(Indicator::render_component_previews(cx))
520 .child(Icon::render_component_previews(cx))
521 .child(Table::render_component_previews(cx))
522 .child(self.render_avatars(cx))
523 .child(self.render_buttons(layer, cx))
524 }
525
526 fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
527 h_flex()
528 .id("theme-preview-nav")
529 .items_center()
530 .gap_4()
531 .py_2()
532 .bg(Self::preview_bg(cx))
533 .children(ThemePreviewPage::iter().map(|p| {
534 Button::new(ElementId::Name(p.name().into()), p.name())
535 .on_click(cx.listener(move |this, _, cx| {
536 this.current_page = p;
537 cx.notify();
538 }))
539 .selected(p == self.current_page)
540 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
541 }))
542 }
543}
544
545impl Render for ThemePreview {
546 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
547 v_flex()
548 .id("theme-preview")
549 .key_context("ThemePreview")
550 .items_start()
551 .overflow_hidden()
552 .size_full()
553 .max_h_full()
554 .track_focus(&self.focus_handle)
555 .px_2()
556 .bg(Self::preview_bg(cx))
557 .child(self.render_page_nav(cx))
558 .child(self.view(self.current_page, cx))
559 }
560}