1use gpui::div;
2
3use crate::prelude::*;
4use crate::settings::user_settings;
5use crate::{
6 disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
7 TextColor, Toggle,
8};
9
10#[derive(Clone, Copy, Default, Debug, PartialEq)]
11pub enum ListItemVariant {
12 /// The list item extends to the far left and right of the list.
13 FullWidth,
14 #[default]
15 Inset,
16}
17
18pub enum ListHeaderMeta {
19 // TODO: These should be IconButtons
20 Tools(Vec<Icon>),
21 // TODO: This should be a button
22 Button(Label),
23 Text(Label),
24}
25
26#[derive(Component)]
27pub struct ListHeader {
28 label: SharedString,
29 left_icon: Option<Icon>,
30 meta: Option<ListHeaderMeta>,
31 variant: ListItemVariant,
32 toggle: Toggle,
33}
34
35impl ListHeader {
36 pub fn new(label: impl Into<SharedString>) -> Self {
37 Self {
38 label: label.into(),
39 left_icon: None,
40 meta: None,
41 variant: ListItemVariant::default(),
42 toggle: Toggle::NotToggleable,
43 }
44 }
45
46 pub fn toggle(mut self, toggle: Toggle) -> Self {
47 self.toggle = toggle;
48 self
49 }
50
51 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
52 self.left_icon = left_icon;
53 self
54 }
55
56 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
57 self.meta = meta;
58 self
59 }
60
61 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
62 let disclosure_control = disclosure_control(self.toggle);
63
64 let meta = match self.meta {
65 Some(ListHeaderMeta::Tools(icons)) => div().child(
66 h_stack()
67 .gap_2()
68 .items_center()
69 .children(icons.into_iter().map(|i| {
70 IconElement::new(i)
71 .color(TextColor::Muted)
72 .size(IconSize::Small)
73 })),
74 ),
75 Some(ListHeaderMeta::Button(label)) => div().child(label),
76 Some(ListHeaderMeta::Text(label)) => div().child(label),
77 None => div(),
78 };
79
80 h_stack()
81 .w_full()
82 .bg(cx.theme().colors().surface_background)
83 // TODO: Add focus state
84 // .when(self.state == InteractionState::Focused, |this| {
85 // this.border()
86 // .border_color(cx.theme().colors().border_focused)
87 // })
88 .relative()
89 .child(
90 div()
91 .h_5()
92 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
93 .flex()
94 .flex_1()
95 .items_center()
96 .justify_between()
97 .w_full()
98 .gap_1()
99 .child(
100 h_stack()
101 .gap_1()
102 .child(
103 div()
104 .flex()
105 .gap_1()
106 .items_center()
107 .children(self.left_icon.map(|i| {
108 IconElement::new(i)
109 .color(TextColor::Muted)
110 .size(IconSize::Small)
111 }))
112 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
113 )
114 .child(disclosure_control),
115 )
116 .child(meta),
117 )
118 }
119}
120
121#[derive(Component)]
122pub struct ListSubHeader {
123 label: SharedString,
124 left_icon: Option<Icon>,
125 variant: ListItemVariant,
126}
127
128impl ListSubHeader {
129 pub fn new(label: impl Into<SharedString>) -> Self {
130 Self {
131 label: label.into(),
132 left_icon: None,
133 variant: ListItemVariant::default(),
134 }
135 }
136
137 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
138 self.left_icon = left_icon;
139 self
140 }
141
142 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
143 h_stack().flex_1().w_full().relative().py_1().child(
144 div()
145 .h_6()
146 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
147 .flex()
148 .flex_1()
149 .w_full()
150 .gap_1()
151 .items_center()
152 .justify_between()
153 .child(
154 div()
155 .flex()
156 .gap_1()
157 .items_center()
158 .children(self.left_icon.map(|i| {
159 IconElement::new(i)
160 .color(TextColor::Muted)
161 .size(IconSize::Small)
162 }))
163 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
164 ),
165 )
166 }
167}
168
169#[derive(Default, PartialEq, Copy, Clone)]
170pub enum ListEntrySize {
171 #[default]
172 Small,
173 Medium,
174}
175
176#[derive(Component)]
177pub enum ListItem {
178 Entry(ListEntry),
179 Separator(ListSeparator),
180 Header(ListSubHeader),
181}
182
183impl From<ListEntry> for ListItem {
184 fn from(entry: ListEntry) -> Self {
185 Self::Entry(entry)
186 }
187}
188
189impl From<ListSeparator> for ListItem {
190 fn from(entry: ListSeparator) -> Self {
191 Self::Separator(entry)
192 }
193}
194
195impl From<ListSubHeader> for ListItem {
196 fn from(entry: ListSubHeader) -> Self {
197 Self::Header(entry)
198 }
199}
200
201impl ListItem {
202 fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
203 match self {
204 ListItem::Entry(entry) => div().child(entry.render(view, cx)),
205 ListItem::Separator(separator) => div().child(separator.render(view, cx)),
206 ListItem::Header(header) => div().child(header.render(view, cx)),
207 }
208 }
209
210 pub fn new(label: Label) -> Self {
211 Self::Entry(ListEntry::new(label))
212 }
213
214 pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
215 if let Self::Entry(entry) = self {
216 Some(entry)
217 } else {
218 None
219 }
220 }
221}
222
223#[derive(Component)]
224pub struct ListEntry {
225 disabled: bool,
226 // TODO: Reintroduce this
227 // disclosure_control_style: DisclosureControlVisibility,
228 indent_level: u32,
229 label: Label,
230 left_slot: Option<GraphicSlot>,
231 overflow: OverflowStyle,
232 size: ListEntrySize,
233 toggle: Toggle,
234 variant: ListItemVariant,
235}
236
237impl ListEntry {
238 pub fn new(label: Label) -> Self {
239 Self {
240 disabled: false,
241 indent_level: 0,
242 label,
243 left_slot: None,
244 overflow: OverflowStyle::Hidden,
245 size: ListEntrySize::default(),
246 toggle: Toggle::NotToggleable,
247 variant: ListItemVariant::default(),
248 }
249 }
250
251 pub fn variant(mut self, variant: ListItemVariant) -> Self {
252 self.variant = variant;
253 self
254 }
255
256 pub fn indent_level(mut self, indent_level: u32) -> Self {
257 self.indent_level = indent_level;
258 self
259 }
260
261 pub fn toggle(mut self, toggle: Toggle) -> Self {
262 self.toggle = toggle;
263 self
264 }
265
266 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
267 self.left_slot = Some(left_content);
268 self
269 }
270
271 pub fn left_icon(mut self, left_icon: Icon) -> Self {
272 self.left_slot = Some(GraphicSlot::Icon(left_icon));
273 self
274 }
275
276 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
277 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
278 self
279 }
280
281 pub fn size(mut self, size: ListEntrySize) -> Self {
282 self.size = size;
283 self
284 }
285
286 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
287 let settings = user_settings(cx);
288
289 let left_content = match self.left_slot.clone() {
290 Some(GraphicSlot::Icon(i)) => Some(
291 h_stack().child(
292 IconElement::new(i)
293 .size(IconSize::Small)
294 .color(TextColor::Muted),
295 ),
296 ),
297 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
298 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
299 None => None,
300 };
301
302 let sized_item = match self.size {
303 ListEntrySize::Small => div().h_6(),
304 ListEntrySize::Medium => div().h_7(),
305 };
306
307 div()
308 .relative()
309 .group("")
310 .bg(cx.theme().colors().surface_background)
311 // TODO: Add focus state
312 // .when(self.state == InteractionState::Focused, |this| {
313 // this.border()
314 // .border_color(cx.theme().colors().border_focused)
315 // })
316 .child(
317 sized_item
318 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
319 // .ml(rems(0.75 * self.indent_level as f32))
320 .children((0..self.indent_level).map(|_| {
321 div()
322 .w(*settings.list_indent_depth)
323 .h_full()
324 .flex()
325 .justify_center()
326 .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
327 .child(
328 h_stack()
329 .child(div().w_px().h_full())
330 .child(div().w_px().h_full().bg(cx.theme().colors().border)),
331 )
332 }))
333 .flex()
334 .gap_1()
335 .items_center()
336 .relative()
337 .child(disclosure_control(self.toggle))
338 .children(left_content)
339 .child(self.label),
340 )
341 }
342}
343
344#[derive(Clone, Component)]
345pub struct ListSeparator;
346
347impl ListSeparator {
348 pub fn new() -> Self {
349 Self
350 }
351
352 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
353 div().h_px().w_full().bg(cx.theme().colors().border_variant)
354 }
355}
356
357#[derive(Component)]
358pub struct List {
359 items: Vec<ListItem>,
360 /// Message to display when the list is empty
361 /// Defaults to "No items"
362 empty_message: SharedString,
363 header: Option<ListHeader>,
364 toggle: Toggle,
365}
366
367impl List {
368 pub fn new(items: Vec<ListItem>) -> Self {
369 Self {
370 items,
371 empty_message: "No items".into(),
372 header: None,
373 toggle: Toggle::NotToggleable,
374 }
375 }
376
377 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
378 self.empty_message = empty_message.into();
379 self
380 }
381
382 pub fn header(mut self, header: ListHeader) -> Self {
383 self.header = Some(header);
384 self
385 }
386
387 pub fn toggle(mut self, toggle: Toggle) -> Self {
388 self.toggle = toggle;
389 self
390 }
391
392 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
393 let list_content = match (self.items.is_empty(), self.toggle) {
394 (false, _) => div().children(self.items),
395 (true, Toggle::Toggled(false)) => div(),
396 (true, _) => {
397 div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
398 }
399 };
400
401 v_stack()
402 .w_full()
403 .py_1()
404 .children(self.header)
405 .child(list_content)
406 }
407}