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(name, None, format!("{:?}", color), cx)
323 }),
324 )
325 })),
326 )
327 }
328
329 fn render_theme_layer(
330 &self,
331 layer: ElevationIndex,
332 window: &mut Window,
333 cx: &mut Context<Self>,
334 ) -> impl IntoElement {
335 v_flex()
336 .p_4()
337 .bg(layer.bg(cx))
338 .text_color(cx.theme().colors().text)
339 .gap_2()
340 .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium))
341 .child(self.render_text(layer, window, cx))
342 .child(self.render_colors(layer, window, cx))
343 }
344
345 fn render_overview_page(
346 &self,
347 window: &mut Window,
348 cx: &mut Context<Self>,
349 ) -> impl IntoElement {
350 v_flex()
351 .id("theme-preview-overview")
352 .overflow_scroll()
353 .size_full()
354 .child(
355 v_flex()
356 .child(Headline::new("Theme Preview").size(HeadlineSize::Large))
357 .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."))
358 )
359 .child(self.render_theme_layer(ElevationIndex::Background, window, cx))
360 .child(self.render_theme_layer(ElevationIndex::Surface, window, cx))
361 .child(self.render_theme_layer(ElevationIndex::EditorSurface, window, cx))
362 .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, window, cx))
363 }
364
365 fn render_typography_page(
366 &self,
367 window: &mut Window,
368 cx: &mut Context<Self>,
369 ) -> impl IntoElement {
370 v_flex()
371 .id("theme-preview-typography")
372 .overflow_scroll()
373 .size_full()
374 .child(v_flex()
375 .gap_4()
376 .child(Headline::new("Headline 1").size(HeadlineSize::XLarge))
377 .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."))
378 .child(Headline::new("Headline 2").size(HeadlineSize::Large))
379 .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
380 .child(Headline::new("Headline 3").size(HeadlineSize::Medium))
381 .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."))
382 .child(Headline::new("Headline 4").size(HeadlineSize::Small))
383 .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
384 .child(Headline::new("Headline 5").size(HeadlineSize::XSmall))
385 .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."))
386 .child(Headline::new("Body Text").size(HeadlineSize::Small))
387 .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."))
388 )
389 }
390
391 fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
392 h_flex()
393 .id("theme-preview-nav")
394 .items_center()
395 .gap_4()
396 .py_2()
397 .bg(Self::preview_bg(window, cx))
398 .children(ThemePreviewPage::iter().map(|p| {
399 Button::new(ElementId::Name(p.name().into()), p.name())
400 .on_click(cx.listener(move |this, _, window, cx| {
401 this.current_page = p;
402 cx.notify();
403 }))
404 .toggle_state(p == self.current_page)
405 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
406 }))
407 }
408}
409
410impl Render for ThemePreview {
411 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
412 v_flex()
413 .id("theme-preview")
414 .key_context("ThemePreview")
415 .items_start()
416 .overflow_hidden()
417 .size_full()
418 .max_h_full()
419 .track_focus(&self.focus_handle)
420 .px_2()
421 .bg(Self::preview_bg(window, cx))
422 .child(self.render_page_nav(window, cx))
423 .child(self.view(self.current_page, window, cx))
424 }
425}