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}