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 clone_on_split(
101 &self,
102 _workspace_id: Option<crate::WorkspaceId>,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) -> Task<Option<Entity<Self>>>
106 where
107 Self: Sized,
108 {
109 Task::ready(Some(cx.new(|cx| Self::new(window, cx))))
110 }
111}
112
113const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4";
114
115impl ThemePreview {
116 fn preview_bg(window: &mut Window, cx: &mut App) -> Hsla {
117 cx.theme().colors().editor_background
118 }
119
120 fn render_text(
121 &self,
122 layer: ElevationIndex,
123 window: &mut Window,
124 cx: &mut Context<Self>,
125 ) -> impl IntoElement {
126 let bg = layer.bg(cx);
127
128 let label_with_contrast = |label: &str, fg: Hsla| {
129 let contrast = calculate_contrast_ratio(fg, bg);
130 format!("{} ({:.2})", label, contrast)
131 };
132
133 v_flex()
134 .gap_1()
135 .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted))
136 .child(
137 h_flex()
138 .items_start()
139 .gap_4()
140 .child(
141 v_flex()
142 .gap_1()
143 .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted))
144 .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge))
145 .child(Headline::new("Large Headline").size(HeadlineSize::Large))
146 .child(Headline::new("Medium Headline").size(HeadlineSize::Medium))
147 .child(Headline::new("Small Headline").size(HeadlineSize::Small))
148 .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)),
149 )
150 .child(
151 v_flex()
152 .gap_1()
153 .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted))
154 .child(
155 Label::new(label_with_contrast(
156 "Default Text",
157 Color::Default.color(cx),
158 ))
159 .color(Color::Default),
160 )
161 .child(
162 Label::new(label_with_contrast(
163 "Accent Text",
164 Color::Accent.color(cx),
165 ))
166 .color(Color::Accent),
167 )
168 .child(
169 Label::new(label_with_contrast(
170 "Conflict Text",
171 Color::Conflict.color(cx),
172 ))
173 .color(Color::Conflict),
174 )
175 .child(
176 Label::new(label_with_contrast(
177 "Created Text",
178 Color::Created.color(cx),
179 ))
180 .color(Color::Created),
181 )
182 .child(
183 Label::new(label_with_contrast(
184 "Deleted Text",
185 Color::Deleted.color(cx),
186 ))
187 .color(Color::Deleted),
188 )
189 .child(
190 Label::new(label_with_contrast(
191 "Disabled Text",
192 Color::Disabled.color(cx),
193 ))
194 .color(Color::Disabled),
195 )
196 .child(
197 Label::new(label_with_contrast(
198 "Error Text",
199 Color::Error.color(cx),
200 ))
201 .color(Color::Error),
202 )
203 .child(
204 Label::new(label_with_contrast(
205 "Hidden Text",
206 Color::Hidden.color(cx),
207 ))
208 .color(Color::Hidden),
209 )
210 .child(
211 Label::new(label_with_contrast(
212 "Hint Text",
213 Color::Hint.color(cx),
214 ))
215 .color(Color::Hint),
216 )
217 .child(
218 Label::new(label_with_contrast(
219 "Ignored Text",
220 Color::Ignored.color(cx),
221 ))
222 .color(Color::Ignored),
223 )
224 .child(
225 Label::new(label_with_contrast(
226 "Info Text",
227 Color::Info.color(cx),
228 ))
229 .color(Color::Info),
230 )
231 .child(
232 Label::new(label_with_contrast(
233 "Modified Text",
234 Color::Modified.color(cx),
235 ))
236 .color(Color::Modified),
237 )
238 .child(
239 Label::new(label_with_contrast(
240 "Muted Text",
241 Color::Muted.color(cx),
242 ))
243 .color(Color::Muted),
244 )
245 .child(
246 Label::new(label_with_contrast(
247 "Placeholder Text",
248 Color::Placeholder.color(cx),
249 ))
250 .color(Color::Placeholder),
251 )
252 .child(
253 Label::new(label_with_contrast(
254 "Selected Text",
255 Color::Selected.color(cx),
256 ))
257 .color(Color::Selected),
258 )
259 .child(
260 Label::new(label_with_contrast(
261 "Success Text",
262 Color::Success.color(cx),
263 ))
264 .color(Color::Success),
265 )
266 .child(
267 Label::new(label_with_contrast(
268 "Warning Text",
269 Color::Warning.color(cx),
270 ))
271 .color(Color::Warning),
272 )
273 )
274 .child(
275 v_flex()
276 .gap_1()
277 .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted))
278 .child(
279 div().max_w(px(200.)).child(
280 "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."
281 ))
282 )
283 )
284 }
285
286 fn render_colors(
287 &self,
288 layer: ElevationIndex,
289 window: &mut Window,
290 cx: &mut Context<Self>,
291 ) -> impl IntoElement {
292 let bg = layer.bg(cx);
293 let all_colors = all_theme_colors(cx);
294
295 v_flex()
296 .gap_1()
297 .child(
298 Headline::new("Colors")
299 .size(HeadlineSize::Small)
300 .color(Color::Muted),
301 )
302 .child(
303 h_flex()
304 .flex_wrap()
305 .gap_1()
306 .children(all_colors.into_iter().map(|(color, name)| {
307 let id = ElementId::Name(format!("{:?}-preview", color).into());
308 div().size_8().flex_none().child(
309 ButtonLike::new(id)
310 .child(
311 div()
312 .size_8()
313 .bg(color)
314 .border_1()
315 .border_color(cx.theme().colors().border)
316 .overflow_hidden(),
317 )
318 .size(ButtonSize::None)
319 .style(ButtonStyle::Transparent)
320 .tooltip(move |window, cx| {
321 let name = name.clone();
322 Tooltip::with_meta(
323 name,
324 None,
325 format!("{:?}", color),
326 window,
327 cx,
328 )
329 }),
330 )
331 })),
332 )
333 }
334
335 fn render_theme_layer(
336 &self,
337 layer: ElevationIndex,
338 window: &mut Window,
339 cx: &mut Context<Self>,
340 ) -> impl IntoElement {
341 v_flex()
342 .p_4()
343 .bg(layer.bg(cx))
344 .text_color(cx.theme().colors().text)
345 .gap_2()
346 .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium))
347 .child(self.render_text(layer, window, cx))
348 .child(self.render_colors(layer, window, cx))
349 }
350
351 fn render_overview_page(
352 &self,
353 window: &mut Window,
354 cx: &mut Context<Self>,
355 ) -> impl IntoElement {
356 v_flex()
357 .id("theme-preview-overview")
358 .overflow_scroll()
359 .size_full()
360 .child(
361 v_flex()
362 .child(Headline::new("Theme Preview").size(HeadlineSize::Large))
363 .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."))
364 )
365 .child(self.render_theme_layer(ElevationIndex::Background, window, cx))
366 .child(self.render_theme_layer(ElevationIndex::Surface, window, cx))
367 .child(self.render_theme_layer(ElevationIndex::EditorSurface, window, cx))
368 .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, window, cx))
369 }
370
371 fn render_typography_page(
372 &self,
373 window: &mut Window,
374 cx: &mut Context<Self>,
375 ) -> impl IntoElement {
376 v_flex()
377 .id("theme-preview-typography")
378 .overflow_scroll()
379 .size_full()
380 .child(v_flex()
381 .gap_4()
382 .child(Headline::new("Headline 1").size(HeadlineSize::XLarge))
383 .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."))
384 .child(Headline::new("Headline 2").size(HeadlineSize::Large))
385 .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
386 .child(Headline::new("Headline 3").size(HeadlineSize::Medium))
387 .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."))
388 .child(Headline::new("Headline 4").size(HeadlineSize::Small))
389 .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
390 .child(Headline::new("Headline 5").size(HeadlineSize::XSmall))
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."))
392 .child(Headline::new("Body Text").size(HeadlineSize::Small))
393 .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."))
394 )
395 }
396
397 fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
398 h_flex()
399 .id("theme-preview-nav")
400 .items_center()
401 .gap_4()
402 .py_2()
403 .bg(Self::preview_bg(window, cx))
404 .children(ThemePreviewPage::iter().map(|p| {
405 Button::new(ElementId::Name(p.name().into()), p.name())
406 .on_click(cx.listener(move |this, _, window, cx| {
407 this.current_page = p;
408 cx.notify();
409 }))
410 .toggle_state(p == self.current_page)
411 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
412 }))
413 }
414}
415
416impl Render for ThemePreview {
417 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
418 v_flex()
419 .id("theme-preview")
420 .key_context("ThemePreview")
421 .items_start()
422 .overflow_hidden()
423 .size_full()
424 .max_h_full()
425 .track_focus(&self.focus_handle)
426 .px_2()
427 .bg(Self::preview_bg(window, cx))
428 .child(self.render_page_nav(window, cx))
429 .child(self.view(self.current_page, window, cx))
430 }
431}