1use gpui::{
2 AnyElement, App, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, div,
3 prelude::FluentBuilder as _, relative, rems,
4};
5
6use crate::{ActiveTheme as _, StyledExt as _, v_flex};
7
8#[derive(IntoElement)]
14pub struct Empty {
15 style: StyleRefinement,
16 header: Option<EmptyHeader>,
17 content: Option<EmptyContent>,
18 children: Vec<AnyElement>,
19}
20
21impl Empty {
22 pub fn new() -> Self {
24 Self {
25 style: StyleRefinement::default(),
26 header: None,
27 content: None,
28 children: Vec::new(),
29 }
30 }
31
32 pub fn header(mut self, header: EmptyHeader) -> Self {
34 self.header = Some(header);
35 self
36 }
37
38 pub fn content(mut self, content: EmptyContent) -> Self {
40 self.content = Some(content);
41 self
42 }
43}
44
45impl Default for Empty {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl ParentElement for Empty {
52 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
53 self.children.extend(elements);
54 }
55}
56
57impl Styled for Empty {
58 fn style(&mut self) -> &mut StyleRefinement {
59 &mut self.style
60 }
61}
62
63impl RenderOnce for Empty {
64 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
65 v_flex()
66 .w_full()
67 .min_w_0()
68 .flex_1()
69 .items_center()
70 .justify_center()
71 .gap_4()
72 .p_6()
73 .rounded(cx.theme().radius_tokens().xl)
74 .border_dashed()
75 .border_color(cx.theme().border)
76 .text_center()
77 .text_color(cx.theme().foreground)
78 .refine_style(&self.style)
79 .when_some(self.header, |this, header| this.child(header))
80 .when_some(self.content, |this, content| this.child(content))
81 .children(self.children)
82 }
83}
84
85#[derive(IntoElement)]
89pub struct EmptyHeader {
90 style: StyleRefinement,
91 media: Option<EmptyMedia>,
92 title: Option<EmptyTitle>,
93 description: Option<EmptyDescription>,
94}
95
96impl EmptyHeader {
97 pub fn new() -> Self {
99 Self {
100 style: StyleRefinement::default(),
101 media: None,
102 title: None,
103 description: None,
104 }
105 }
106
107 pub fn media(mut self, media: EmptyMedia) -> Self {
109 self.media = Some(media);
110 self
111 }
112
113 pub fn title(mut self, title: EmptyTitle) -> Self {
115 self.title = Some(title);
116 self
117 }
118
119 pub fn description(mut self, description: EmptyDescription) -> Self {
121 self.description = Some(description);
122 self
123 }
124}
125
126impl Default for EmptyHeader {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl Styled for EmptyHeader {
133 fn style(&mut self) -> &mut StyleRefinement {
134 &mut self.style
135 }
136}
137
138impl RenderOnce for EmptyHeader {
139 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
140 v_flex()
141 .w_full()
142 .max_w(rems(24.))
143 .min_w_0()
144 .items_center()
145 .gap_2()
146 .refine_style(&self.style)
147 .when_some(self.media, |this, media| this.child(media))
148 .when_some(self.title, |this, title| this.child(title))
149 .when_some(self.description, |this, description| {
150 this.child(description)
151 })
152 }
153}
154
155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
157pub enum EmptyMediaVariant {
158 #[default]
160 Default,
161 Icon,
163}
164
165#[derive(IntoElement)]
167pub struct EmptyMedia {
168 style: StyleRefinement,
169 variant: EmptyMediaVariant,
170 children: Vec<AnyElement>,
171}
172
173impl EmptyMedia {
174 pub fn new() -> Self {
176 Self {
177 style: StyleRefinement::default(),
178 variant: EmptyMediaVariant::Default,
179 children: Vec::new(),
180 }
181 }
182
183 pub fn with_variant(mut self, variant: EmptyMediaVariant) -> Self {
185 self.variant = variant;
186 self
187 }
188}
189
190impl Default for EmptyMedia {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl ParentElement for EmptyMedia {
197 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
198 self.children.extend(elements);
199 }
200}
201
202impl Styled for EmptyMedia {
203 fn style(&mut self) -> &mut StyleRefinement {
204 &mut self.style
205 }
206}
207
208impl RenderOnce for EmptyMedia {
209 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
210 v_flex()
212 .flex_shrink_0()
213 .items_center()
214 .justify_center()
215 .mb_2()
216 .when(self.variant == EmptyMediaVariant::Icon, |this| {
217 this.size_8()
218 .rounded(cx.theme().radius_tokens().lg)
219 .bg(cx.theme().muted)
220 .text_color(cx.theme().foreground)
221 .text_base()
223 })
224 .refine_style(&self.style)
225 .children(self.children)
226 }
227}
228
229#[derive(IntoElement)]
231pub struct EmptyTitle {
232 style: StyleRefinement,
233 children: Vec<AnyElement>,
234}
235
236impl EmptyTitle {
237 pub fn new() -> Self {
239 Self {
240 style: StyleRefinement::default(),
241 children: Vec::new(),
242 }
243 }
244}
245
246impl Default for EmptyTitle {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl ParentElement for EmptyTitle {
253 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
254 self.children.extend(elements);
255 }
256}
257
258impl Styled for EmptyTitle {
259 fn style(&mut self) -> &mut StyleRefinement {
260 &mut self.style
261 }
262}
263
264impl RenderOnce for EmptyTitle {
265 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
266 div()
267 .max_w_full()
268 .min_w_0()
269 .text_sm()
270 .font_medium()
271 .whitespace_normal()
272 .refine_style(&self.style)
273 .children(self.children)
274 }
275}
276
277#[derive(IntoElement)]
279pub struct EmptyDescription {
280 style: StyleRefinement,
281 children: Vec<AnyElement>,
282}
283
284impl EmptyDescription {
285 pub fn new() -> Self {
287 Self {
288 style: StyleRefinement::default(),
289 children: Vec::new(),
290 }
291 }
292}
293
294impl Default for EmptyDescription {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300impl ParentElement for EmptyDescription {
301 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
302 self.children.extend(elements);
303 }
304}
305
306impl Styled for EmptyDescription {
307 fn style(&mut self) -> &mut StyleRefinement {
308 &mut self.style
309 }
310}
311
312impl RenderOnce for EmptyDescription {
313 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
314 div()
315 .w_full()
316 .min_w_0()
317 .text_sm()
318 .line_height(relative(1.625))
319 .text_color(cx.theme().muted_foreground)
320 .whitespace_normal()
321 .refine_style(&self.style)
322 .children(self.children)
323 }
324}
325
326#[derive(IntoElement)]
328pub struct EmptyContent {
329 style: StyleRefinement,
330 children: Vec<AnyElement>,
331}
332
333impl EmptyContent {
334 pub fn new() -> Self {
336 Self {
337 style: StyleRefinement::default(),
338 children: Vec::new(),
339 }
340 }
341}
342
343impl Default for EmptyContent {
344 fn default() -> Self {
345 Self::new()
346 }
347}
348
349impl ParentElement for EmptyContent {
350 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
351 self.children.extend(elements);
352 }
353}
354
355impl Styled for EmptyContent {
356 fn style(&mut self) -> &mut StyleRefinement {
357 &mut self.style
358 }
359}
360
361impl RenderOnce for EmptyContent {
362 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
363 v_flex()
364 .w_full()
365 .max_w(rems(24.))
366 .min_w_0()
367 .items_center()
368 .gap_2p5()
369 .text_sm()
370 .refine_style(&self.style)
371 .children(self.children)
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::{
379 Sizable as _,
380 avatar::{Avatar, AvatarGroup},
381 };
382 use gpui::{Context, InteractiveElement as _, Render, TestAppContext, px};
383
384 struct MediaLayout {
385 grouped: bool,
386 leading: bool,
387 }
388
389 impl Render for MediaLayout {
390 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
391 let media = if self.grouped {
392 AvatarGroup::new()
393 .small()
394 .debug_selector(|| "media-content".into())
395 .child(Avatar::new().name("Alex"))
396 .child(Avatar::new().name("Taylor"))
397 .child(Avatar::new().name("Sam"))
398 .into_any_element()
399 } else {
400 Avatar::new()
401 .small()
402 .debug_selector(|| "media-content".into())
403 .name("Alex")
404 .into_any_element()
405 };
406
407 Empty::new()
409 .when(self.leading, |this| this.items_start().text_left())
410 .child(div().size_4().debug_selector(|| "trailing".into()))
411 .content(
412 EmptyContent::new()
413 .when(self.leading, |this| this.items_start())
414 .child(div().size_4().debug_selector(|| "content".into())),
415 )
416 .header(
417 EmptyHeader::new()
418 .when(self.leading, |this| this.items_start())
419 .description(EmptyDescription::new().child("Supporting content"))
420 .title(
421 EmptyTitle::new()
422 .child(div().size_4().debug_selector(|| "title".into())),
423 )
424 .media(EmptyMedia::new().child(media)),
425 )
426 }
427 }
428
429 #[gpui::test]
430 fn media_keeps_intrinsic_width_and_slot_alignment(cx: &mut TestAppContext) {
431 cx.update(crate::init);
432
433 for grouped in [false, true] {
434 for leading in [false, true] {
435 let (_, cx) = cx.add_window_view(|_, _| MediaLayout { grouped, leading });
436 for rem in [14., 20.] {
437 cx.update(|window, cx| {
438 window.set_rem_size(px(rem));
439 window.draw(cx).clear(cx);
440 });
441 let media = cx.debug_bounds("media-content").unwrap();
442 let title = cx.debug_bounds("title").unwrap();
443 let content = cx.debug_bounds("content").unwrap();
444 let trailing = cx.debug_bounds("trailing").unwrap();
445
446 assert!(media.size.width > px(0.), "grouped: {grouped}");
448 let alignment = |bounds: gpui::Bounds<gpui::Pixels>| {
449 if leading {
450 bounds.left()
451 } else {
452 bounds.center().x
453 }
454 };
455 assert_eq!(alignment(media), alignment(title));
456 assert_eq!(alignment(content), alignment(title));
457 assert!(media.bottom() <= title.top());
458 assert!(title.bottom() <= content.top());
459 assert!(content.bottom() <= trailing.top());
460 }
461 }
462 }
463 }
464
465 #[test]
466 fn test_empty_builder() {
467 let root_style = StyleRefinement::default().p_4().border_1();
468 let header_style = StyleRefinement::default().items_start();
469 let media_style = StyleRefinement::default().size_10();
470 let title_style = StyleRefinement::default().text_base();
471 let description_style = StyleRefinement::default().text_left();
472 let content_style = StyleRefinement::default().flex_row().flex_wrap().gap_2();
473
474 let empty = Empty::new()
475 .refine_style(&root_style)
476 .child("Before the named setters, still after the slots")
477 .header(EmptyHeader::new().media(EmptyMedia::new()))
478 .content(EmptyContent::new().child("Replaced content"))
479 .header(
480 EmptyHeader::new()
481 .refine_style(&header_style)
482 .title(EmptyTitle::new().child("Replaced title"))
483 .description(EmptyDescription::new().child("Replaced description"))
484 .media(EmptyMedia::new().child("Replaced media"))
485 .description(
486 EmptyDescription::new()
487 .refine_style(&description_style)
488 .children(["Description", "Custom content"]),
489 )
490 .title(EmptyTitle::new().refine_style(&title_style).child("Title"))
491 .media(
492 EmptyMedia::new()
493 .with_variant(EmptyMediaVariant::Icon)
494 .refine_style(&media_style)
495 .child(crate::Icon::new(crate::IconName::Search)),
496 ),
497 )
498 .content(
499 EmptyContent::new()
500 .refine_style(&content_style)
501 .children(["First action", "Second action"]),
502 )
503 .child("Trailing content");
504
505 assert_eq!(empty.style, root_style);
506 assert_eq!(empty.children.len(), 2);
507 let header = empty.header.unwrap();
508 assert_eq!(header.style, header_style);
509 let media = header.media.unwrap();
510 assert_eq!(media.variant, EmptyMediaVariant::Icon);
511 assert_eq!(media.style, media_style);
512 assert_eq!(media.children.len(), 1);
513 let title = header.title.unwrap();
514 assert_eq!(title.style, title_style);
515 assert_eq!(title.children.len(), 1);
516 let description = header.description.unwrap();
517 assert_eq!(description.style, description_style);
518 assert_eq!(description.children.len(), 2);
519 let content = empty.content.unwrap();
520 assert_eq!(content.style, content_style);
521 assert_eq!(content.children.len(), 2);
522
523 let empty = Empty::default();
524 assert!(empty.header.is_none());
525 assert!(empty.content.is_none());
526 assert!(empty.children.is_empty());
527 assert_eq!(EmptyMedia::default().variant, EmptyMediaVariant::Default);
528 }
529}