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}