1use component::{Component, ComponentScope, example_group_with_title, single_example};
2use gpui::{AnyElement, ElementId, Role};
3use smallvec::SmallVec;
4
5use crate::{Label, ListHeader, ListItem, prelude::*};
6
7pub enum EmptyMessage {
8 Text(SharedString),
9 Element(AnyElement),
10}
11
12#[derive(IntoElement, RegisterComponent)]
13pub struct List {
14 id: Option<ElementId>,
15 /// Message to display when the list is empty
16 /// Defaults to "No items"
17 empty_message: EmptyMessage,
18 header: Option<ListHeader>,
19 toggle: Option<bool>,
20 children: SmallVec<[AnyElement; 2]>,
21}
22
23impl Default for List {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl List {
30 pub fn new() -> Self {
31 Self {
32 id: None,
33 empty_message: EmptyMessage::Text("No items".into()),
34 header: None,
35 toggle: None,
36 children: SmallVec::new(),
37 }
38 }
39
40 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
41 self.id = Some(id.into());
42 self
43 }
44
45 pub fn empty_message(mut self, message: impl Into<EmptyMessage>) -> Self {
46 self.empty_message = message.into();
47 self
48 }
49
50 pub fn header(mut self, header: impl Into<Option<ListHeader>>) -> Self {
51 self.header = header.into();
52 self
53 }
54
55 pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
56 self.toggle = toggle.into();
57 self
58 }
59}
60
61impl ParentElement for List {
62 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
63 self.children.extend(elements)
64 }
65}
66
67impl From<String> for EmptyMessage {
68 fn from(s: String) -> Self {
69 EmptyMessage::Text(SharedString::from(s))
70 }
71}
72
73impl From<&str> for EmptyMessage {
74 fn from(s: &str) -> Self {
75 EmptyMessage::Text(SharedString::from(s.to_owned()))
76 }
77}
78
79impl From<AnyElement> for EmptyMessage {
80 fn from(e: AnyElement) -> Self {
81 EmptyMessage::Element(e)
82 }
83}
84
85impl RenderOnce for List {
86 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
87 let styled = v_flex()
88 .w_full()
89 .py(DynamicSpacing::Base04.rems(cx))
90 .children(self.header)
91 .map(|this| match (self.children.is_empty(), self.toggle) {
92 (false, _) => this.children(self.children),
93 (true, Some(false)) => this,
94 (true, _) => match self.empty_message {
95 EmptyMessage::Text(text) => {
96 this.px_2().child(Label::new(text).color(Color::Muted))
97 }
98 EmptyMessage::Element(element) => this.child(element),
99 },
100 });
101
102 if let Some(id) = self.id {
103 styled.id(id).role(Role::List).into_any_element()
104 } else {
105 styled.into_any_element()
106 }
107 }
108}
109
110impl Component for List {
111 fn scope() -> ComponentScope {
112 ComponentScope::Layout
113 }
114
115 fn description() -> Option<&'static str> {
116 Some(
117 "A container component for displaying a collection of list items with optional header and empty state.",
118 )
119 }
120
121 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
122 Some(
123 v_flex()
124 .gap_6()
125 .children(vec![example_group_with_title(
126 "Basic Lists",
127 vec![
128 single_example(
129 "Simple List",
130 List::new().id("simple-list")
131 .child(ListItem::new("item1").child(Label::new("Item 1")))
132 .child(ListItem::new("item2").child(Label::new("Item 2")))
133 .child(ListItem::new("item3").child(Label::new("Item 3")))
134 .into_any_element(),
135 ),
136 single_example(
137 "With Header",
138 List::new().id("header-list")
139 .header(ListHeader::new("Section Header"))
140 .child(ListItem::new("item1").child(Label::new("Item 1")))
141 .child(ListItem::new("item2").child(Label::new("Item 2")))
142 .into_any_element(),
143 ),
144 single_example(
145 "Empty List",
146 List::new().id("empty-list")
147 .empty_message("No items to display")
148 .into_any_element(),
149 ),
150 ],
151 )])
152 .into_any_element(),
153 )
154 }
155}