1#![allow(unused, dead_code)]
2use gpui::{
3 AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla,
4};
5use strum::IntoEnumIterator;
6use theme::all_theme_colors;
7use ui::{
8 AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
9 Checkbox, CheckboxWithLabel, CollaboratorAvailability, ContentGroup, DecoratedIcon,
10 ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, TintColor,
11 Tooltip, prelude::*, utils::calculate_contrast_ratio,
12};
13
14use crate::{Item, Workspace};
15
16actions!(
17 dev,
18 [
19 /// Opens the theme preview window.
20 OpenThemePreview
21 ]
22);
23
24pub fn init(cx: &mut App) {
25 cx.observe_new(|workspace: &mut Workspace, _, _| {
26 workspace.register_action(|workspace, _: &OpenThemePreview, window, cx| {
27 let theme_preview = cx.new(|cx| ThemePreview::new(window, cx));
28 workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, window, cx)
29 });
30 })
31 .detach();
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum::EnumIter)]
35enum ThemePreviewPage {
36 Overview,
37 Typography,
38}
39
40impl ThemePreviewPage {
41 pub fn name(&self) -> &'static str {
42 match self {
43 Self::Overview => "Overview",
44 Self::Typography => "Typography",
45 }
46 }
47}
48
49struct ThemePreview {
50 current_page: ThemePreviewPage,
51 focus_handle: FocusHandle,
52}
53
54impl ThemePreview {
55 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
56 Self {
57 current_page: ThemePreviewPage::Overview,
58 focus_handle: cx.focus_handle(),
59 }
60 }
61
62 pub fn view(
63 &self,
64 page: ThemePreviewPage,
65 window: &mut Window,
66 cx: &mut Context<ThemePreview>,
67 ) -> impl IntoElement {
68 match page {
69 ThemePreviewPage::Overview => self.render_overview_page(window, cx).into_any_element(),
70 ThemePreviewPage::Typography => {
71 self.render_typography_page(window, cx).into_any_element()
72 }
73 }
74 }
75}
76
77impl EventEmitter<()> for ThemePreview {}
78
79impl Focusable for ThemePreview {
80 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
81 self.focus_handle.clone()
82 }
83}
84impl ThemePreview {}
85
86impl Item for ThemePreview {
87 type Event = ();
88
89 fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {}
90
91 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
92 let name = cx.theme().name.clone();
93 format!("{} Preview", name).into()
94 }
95
96 fn telemetry_event_text(&self) -> Option<&'static str> {
97 None
98 }
99
100 fn can_split(&self) -> bool {
101 true
102 }
103
104 fn clone_on_split(
105 &self,
106 _workspace_id: Option<crate::WorkspaceId>,
107 window: &mut Window,
108 cx: &mut Context<Self>,
109 ) -> Task<Option<Entity<Self>>>
110 where
111 Self: Sized,
112 {
113 Task::ready(Some(cx.new(|cx| Self::new(window, cx))))
114 }
115}
116
117const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4";
118
119impl ThemePreview {
120 fn preview_bg(window: &mut Window, cx: &mut App) -> Hsla {
121 cx.theme().colors().editor_background
122 }
123
124 fn render_text(
125 &self,
126 layer: ElevationIndex,
127 window: &mut Window,
128 cx: &mut Context<Self>,
129 ) -> impl IntoElement {
130 let bg = layer.bg(cx);
131
132 let label_with_contrast = |label: &str, fg: Hsla| {
133 let contrast = calculate_contrast_ratio(fg, bg);
134 format!("{} ({:.2})", label, contrast)
135 };
136
137 v_flex()
138 .gap_1()
139 .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted))
140 .child(
141 h_flex()
142 .items_start()
143 .gap_4()
144 .child(
145 v_flex()
146 .gap_1()
147 .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted))
148 .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge))
149 .child(Headline::new("Large Headline").size(HeadlineSize::Large))
150 .child(Headline::new("Medium Headline").size(HeadlineSize::Medium))
151 .child(Headline::new("Small Headline").size(HeadlineSize::Small))
152 .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)),
153 )
154 .child(
155 v_flex()
156 .gap_1()
157 .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted))
158 .child(
159 Label::new(label_with_contrast(
160 "Default Text",
161 Color::Default.color(cx),
162 ))
163 .color(Color::Default),
164 )
165 .child(
166 Label::new(label_with_contrast(
167 "Accent Text",
168 Color::Accent.color(cx),
169 ))
170 .color(Color::Accent),
171 )
172 .child(
173 Label::new(label_with_contrast(
174 "Conflict Text",
175 Color::Conflict.color(cx),
176 ))
177 .color(Color::Conflict),
178 )
179 .child(
180 Label::new(label_with_contrast(
181 "Created Text",
182 Color::Created.color(cx),
183 ))
184 .color(Color::Created),
185 )
186 .child(
187 Label::new(label_with_contrast(
188 "Deleted Text",
189 Color::Deleted.color(cx),
190 ))
191 .color(Color::Deleted),
192 )
193 .child(
194 Label::new(label_with_contrast(
195 "Disabled Text",
196 Color::Disabled.color(cx),
197 ))
198 .color(Color::Disabled),
199 )
200 .child(
201 Label::new(label_with_contrast(
202 "Error Text",
203 Color::Error.color(cx),
204 ))
205 .color(Color::Error),
206 )
207 .child(
208 Label::new(label_with_contrast(
209 "Hidden Text",
210 Color::Hidden.color(cx),
211 ))
212 .color(Color::Hidden),
213 )
214 .child(
215 Label::new(label_with_contrast(
216 "Hint Text",
217 Color::Hint.color(cx),
218 ))
219 .color(Color::Hint),
220 )
221 .child(
222 Label::new(label_with_contrast(
223 "Ignored Text",
224 Color::Ignored.color(cx),
225 ))
226 .color(Color::Ignored),
227 )
228 .child(
229 Label::new(label_with_contrast(
230 "Info Text",
231 Color::Info.color(cx),
232 ))
233 .color(Color::Info),
234 )
235 .child(
236 Label::new(label_with_contrast(
237 "Modified Text",
238 Color::Modified.color(cx),
239 ))
240 .color(Color::Modified),
241 )
242 .child(
243 Label::new(label_with_contrast(
244 "Muted Text",
245 Color::Muted.color(cx),
246 ))
247 .color(Color::Muted),
248 )
249 .child(
250 Label::new(label_with_contrast(
251 "Placeholder Text",
252 Color::Placeholder.color(cx),
253 ))
254 .color(Color::Placeholder),
255 )
256 .child(
257 Label::new(label_with_contrast(
258 "Selected Text",
259 Color::Selected.color(cx),
260 ))
261 .color(Color::Selected),
262 )
263 .child(
264 Label::new(label_with_contrast(
265 "Success Text",
266 Color::Success.color(cx),
267 ))
268 .color(Color::Success),
269 )
270 .child(
271 Label::new(label_with_contrast(
272 "Warning Text",
273 Color::Warning.color(cx),
274 ))
275 .color(Color::Warning),
276 )
277 )
278 .child(
279 v_flex()
280 .gap_1()
281 .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted))
282 .child(
283 div().max_w(px(200.)).child(
284 "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."
285 ))
286 )
287 )
288 }
289
290 fn render_colors(
291 &self,
292 layer: ElevationIndex,
293 window: &mut Window,
294 cx: &mut Context<Self>,
295 ) -> impl IntoElement {
296 let bg = layer.bg(cx);
297 let all_colors = all_theme_colors(cx);
298
299 v_flex()
300 .gap_1()
301 .child(
302 Headline::new("Colors")
303 .size(HeadlineSize::Small)
304 .color(Color::Muted),
305 )
306 .child(
307 h_flex()
308 .flex_wrap()
309 .gap_1()
310 .children(all_colors.into_iter().map(|(color, name)| {
311 let id = ElementId::Name(format!("{:?}-preview", color).into());
312 div().size_8().flex_none().child(
313 ButtonLike::new(id)
314 .child(
315 div()
316 .size_8()
317 .bg(color)
318 .border_1()
319 .border_color(cx.theme().colors().border)
320 .overflow_hidden(),
321 )
322 .size(ButtonSize::None)
323 .style(ButtonStyle::Transparent)
324 .tooltip(move |window, cx| {
325 let name = name.clone();
326 Tooltip::with_meta(name, None, format!("{:?}", color), cx)
327 }),
328 )
329 })),
330 )
331 }
332
333 fn render_theme_layer(
334 &self,
335 layer: ElevationIndex,
336 window: &mut Window,
337 cx: &mut Context<Self>,
338 ) -> impl IntoElement {
339 v_flex()
340 .p_4()
341 .bg(layer.bg(cx))
342 .text_color(cx.theme().colors().text)
343 .gap_2()
344 .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium))
345 .child(self.render_text(layer, window, cx))
346 .child(self.render_colors(layer, window, cx))
347 }
348
349 fn render_overview_page(
350 &self,
351 window: &mut Window,
352 cx: &mut Context<Self>,
353 ) -> impl IntoElement {
354 v_flex()
355 .id("theme-preview-overview")
356 .overflow_scroll()
357 .size_full()
358 .child(
359 v_flex()
360 .child(Headline::new("Theme Preview").size(HeadlineSize::Large))
361 .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."))
362 )
363 .child(self.render_theme_layer(ElevationIndex::Background, window, cx))
364 .child(self.render_theme_layer(ElevationIndex::Surface, window, cx))
365 .child(self.render_theme_layer(ElevationIndex::EditorSurface, window, cx))
366 .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, window, cx))
367 }
368
369 fn render_typography_page(
370 &self,
371 window: &mut Window,
372 cx: &mut Context<Self>,
373 ) -> impl IntoElement {
374 v_flex()
375 .id("theme-preview-typography")
376 .overflow_scroll()
377 .size_full()
378 .child(v_flex()
379 .gap_4()
380 .child(Headline::new("Headline 1").size(HeadlineSize::XLarge))
381 .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."))
382 .child(Headline::new("Headline 2").size(HeadlineSize::Large))
383 .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
384 .child(Headline::new("Headline 3").size(HeadlineSize::Medium))
385 .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."))
386 .child(Headline::new("Headline 4").size(HeadlineSize::Small))
387 .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
388 .child(Headline::new("Headline 5").size(HeadlineSize::XSmall))
389 .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."))
390 .child(Headline::new("Body Text").size(HeadlineSize::Small))
391 .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."))
392 )
393 }
394
395 fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
396 h_flex()
397 .id("theme-preview-nav")
398 .items_center()
399 .gap_4()
400 .py_2()
401 .bg(Self::preview_bg(window, cx))
402 .children(ThemePreviewPage::iter().map(|p| {
403 Button::new(ElementId::Name(p.name().into()), p.name())
404 .on_click(cx.listener(move |this, _, window, cx| {
405 this.current_page = p;
406 cx.notify();
407 }))
408 .toggle_state(p == self.current_page)
409 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
410 }))
411 }
412}
413
414impl Render for ThemePreview {
415 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
416 v_flex()
417 .id("theme-preview")
418 .key_context("ThemePreview")
419 .items_start()
420 .overflow_hidden()
421 .size_full()
422 .max_h_full()
423 .track_focus(&self.focus_handle)
424 .px_2()
425 .bg(Self::preview_bg(window, cx))
426 .child(self.render_page_nav(window, cx))
427 .child(self.view(self.current_page, window, cx))
428 }
429}