1use gpui2::{div, relative, Div};
2
3use crate::settings::user_settings;
4use crate::{
5 h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label,
6 LabelColor,
7};
8use crate::{prelude::*, Button};
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
18#[derive(Component)]
19pub struct ListHeader {
20 label: SharedString,
21 left_icon: Option<Icon>,
22 variant: ListItemVariant,
23 state: InteractionState,
24 toggleable: Toggleable,
25}
26
27impl ListHeader {
28 pub fn new(label: impl Into<SharedString>) -> Self {
29 Self {
30 label: label.into(),
31 left_icon: None,
32 variant: ListItemVariant::default(),
33 state: InteractionState::default(),
34 toggleable: Toggleable::Toggleable(ToggleState::Toggled),
35 }
36 }
37
38 pub fn toggle(mut self, toggle: ToggleState) -> Self {
39 self.toggleable = toggle.into();
40 self
41 }
42
43 pub fn toggleable(mut self, toggleable: Toggleable) -> Self {
44 self.toggleable = toggleable;
45 self
46 }
47
48 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
49 self.left_icon = left_icon;
50 self
51 }
52
53 pub fn state(mut self, state: InteractionState) -> Self {
54 self.state = state;
55 self
56 }
57
58 fn disclosure_control<V: 'static>(&self) -> Div<V> {
59 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
60 let is_toggled = Toggleable::is_toggled(&self.toggleable);
61
62 match (is_toggleable, is_toggled) {
63 (false, _) => div(),
64 (_, true) => div().child(
65 IconElement::new(Icon::ChevronDown)
66 .color(IconColor::Muted)
67 .size(IconSize::Small),
68 ),
69 (_, false) => div().child(
70 IconElement::new(Icon::ChevronRight)
71 .color(IconColor::Muted)
72 .size(IconSize::Small),
73 ),
74 }
75 }
76
77 fn label_color(&self) -> LabelColor {
78 match self.state {
79 InteractionState::Disabled => LabelColor::Disabled,
80 _ => Default::default(),
81 }
82 }
83
84 fn icon_color(&self) -> IconColor {
85 match self.state {
86 InteractionState::Disabled => IconColor::Disabled,
87 _ => Default::default(),
88 }
89 }
90
91 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
92 let theme = theme(cx);
93
94 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
95 let is_toggled = self.toggleable.is_toggled();
96
97 let disclosure_control = self.disclosure_control();
98
99 h_stack()
100 .flex_1()
101 .w_full()
102 .bg(theme.surface)
103 .when(self.state == InteractionState::Focused, |this| {
104 this.border().border_color(theme.border_focused)
105 })
106 .relative()
107 .child(
108 div()
109 .h_5()
110 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
111 .flex()
112 .flex_1()
113 .w_full()
114 .gap_1()
115 .items_center()
116 .child(
117 div()
118 .flex()
119 .gap_1()
120 .items_center()
121 .children(self.left_icon.map(|i| {
122 IconElement::new(i)
123 .color(IconColor::Muted)
124 .size(IconSize::Small)
125 }))
126 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
127 )
128 .child(disclosure_control),
129 )
130 }
131}
132
133#[derive(Component)]
134pub struct ListSubHeader {
135 label: SharedString,
136 left_icon: Option<Icon>,
137 variant: ListItemVariant,
138}
139
140impl ListSubHeader {
141 pub fn new(label: impl Into<SharedString>) -> Self {
142 Self {
143 label: label.into(),
144 left_icon: None,
145 variant: ListItemVariant::default(),
146 }
147 }
148
149 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
150 self.left_icon = left_icon;
151 self
152 }
153
154 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
155 h_stack().flex_1().w_full().relative().py_1().child(
156 div()
157 .h_6()
158 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
159 .flex()
160 .flex_1()
161 .w_full()
162 .gap_1()
163 .items_center()
164 .justify_between()
165 .child(
166 div()
167 .flex()
168 .gap_1()
169 .items_center()
170 .children(self.left_icon.map(|i| {
171 IconElement::new(i)
172 .color(IconColor::Muted)
173 .size(IconSize::Small)
174 }))
175 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
176 ),
177 )
178 }
179}
180
181#[derive(Clone)]
182pub enum LeftContent {
183 Icon(Icon),
184 Avatar(SharedString),
185}
186
187#[derive(Default, PartialEq, Copy, Clone)]
188pub enum ListEntrySize {
189 #[default]
190 Small,
191 Medium,
192}
193
194#[derive(Component)]
195pub enum ListItem<V: 'static> {
196 Entry(ListEntry),
197 Details(ListDetailsEntry<V>),
198 Separator(ListSeparator),
199 Header(ListSubHeader),
200}
201
202impl<V: 'static> From<ListEntry> for ListItem<V> {
203 fn from(entry: ListEntry) -> Self {
204 Self::Entry(entry)
205 }
206}
207
208impl<V: 'static> From<ListDetailsEntry<V>> for ListItem<V> {
209 fn from(entry: ListDetailsEntry<V>) -> Self {
210 Self::Details(entry)
211 }
212}
213
214impl<V: 'static> From<ListSeparator> for ListItem<V> {
215 fn from(entry: ListSeparator) -> Self {
216 Self::Separator(entry)
217 }
218}
219
220impl<V: 'static> From<ListSubHeader> for ListItem<V> {
221 fn from(entry: ListSubHeader) -> Self {
222 Self::Header(entry)
223 }
224}
225
226impl<V: 'static> ListItem<V> {
227 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
228 match self {
229 ListItem::Entry(entry) => div().child(entry.render(view, cx)),
230 ListItem::Separator(separator) => div().child(separator.render(view, cx)),
231 ListItem::Header(header) => div().child(header.render(view, cx)),
232 ListItem::Details(details) => div().child(details.render(view, cx)),
233 }
234 }
235
236 pub fn new(label: Label) -> Self {
237 Self::Entry(ListEntry::new(label))
238 }
239
240 pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
241 if let Self::Entry(entry) = self {
242 Some(entry)
243 } else {
244 None
245 }
246 }
247}
248
249#[derive(Component)]
250pub struct ListEntry {
251 disclosure_control_style: DisclosureControlVisibility,
252 indent_level: u32,
253 label: Label,
254 left_content: Option<LeftContent>,
255 variant: ListItemVariant,
256 size: ListEntrySize,
257 state: InteractionState,
258 toggle: Option<ToggleState>,
259 overflow: OverflowStyle,
260}
261
262impl ListEntry {
263 pub fn new(label: Label) -> Self {
264 Self {
265 disclosure_control_style: DisclosureControlVisibility::default(),
266 indent_level: 0,
267 label,
268 variant: ListItemVariant::default(),
269 left_content: None,
270 size: ListEntrySize::default(),
271 state: InteractionState::default(),
272 // TODO: Should use Toggleable::NotToggleable
273 // or remove Toggleable::NotToggleable from the system
274 toggle: None,
275 overflow: OverflowStyle::Hidden,
276 }
277 }
278
279 pub fn variant(mut self, variant: ListItemVariant) -> Self {
280 self.variant = variant;
281 self
282 }
283
284 pub fn indent_level(mut self, indent_level: u32) -> Self {
285 self.indent_level = indent_level;
286 self
287 }
288
289 pub fn toggle(mut self, toggle: ToggleState) -> Self {
290 self.toggle = Some(toggle);
291 self
292 }
293
294 pub fn left_content(mut self, left_content: LeftContent) -> Self {
295 self.left_content = Some(left_content);
296 self
297 }
298
299 pub fn left_icon(mut self, left_icon: Icon) -> Self {
300 self.left_content = Some(LeftContent::Icon(left_icon));
301 self
302 }
303
304 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
305 self.left_content = Some(LeftContent::Avatar(left_avatar.into()));
306 self
307 }
308
309 pub fn state(mut self, state: InteractionState) -> Self {
310 self.state = state;
311 self
312 }
313
314 pub fn size(mut self, size: ListEntrySize) -> Self {
315 self.size = size;
316 self
317 }
318
319 pub fn disclosure_control_style(
320 mut self,
321 disclosure_control_style: DisclosureControlVisibility,
322 ) -> Self {
323 self.disclosure_control_style = disclosure_control_style;
324 self
325 }
326
327 fn label_color(&self) -> LabelColor {
328 match self.state {
329 InteractionState::Disabled => LabelColor::Disabled,
330 _ => Default::default(),
331 }
332 }
333
334 fn icon_color(&self) -> IconColor {
335 match self.state {
336 InteractionState::Disabled => IconColor::Disabled,
337 _ => Default::default(),
338 }
339 }
340
341 fn disclosure_control<V: 'static>(
342 &mut self,
343 cx: &mut ViewContext<V>,
344 ) -> Option<impl Component<V>> {
345 let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
346 IconElement::new(Icon::ChevronDown)
347 } else {
348 IconElement::new(Icon::ChevronRight)
349 }
350 .color(IconColor::Muted)
351 .size(IconSize::Small);
352
353 match (self.toggle, self.disclosure_control_style) {
354 (Some(_), DisclosureControlVisibility::OnHover) => {
355 Some(div().absolute().neg_left_5().child(disclosure_control_icon))
356 }
357 (Some(_), DisclosureControlVisibility::Always) => {
358 Some(div().child(disclosure_control_icon))
359 }
360 (None, _) => None,
361 }
362 }
363
364 fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
365 let settings = user_settings(cx);
366 let theme = theme(cx);
367
368 let left_content = match self.left_content.clone() {
369 Some(LeftContent::Icon(i)) => Some(
370 h_stack().child(
371 IconElement::new(i)
372 .size(IconSize::Small)
373 .color(IconColor::Muted),
374 ),
375 ),
376 Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
377 None => None,
378 };
379
380 let sized_item = match self.size {
381 ListEntrySize::Small => div().h_6(),
382 ListEntrySize::Medium => div().h_7(),
383 };
384
385 div()
386 .relative()
387 .group("")
388 .bg(theme.surface)
389 .when(self.state == InteractionState::Focused, |this| {
390 this.border().border_color(theme.border_focused)
391 })
392 .child(
393 sized_item
394 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
395 // .ml(rems(0.75 * self.indent_level as f32))
396 .children((0..self.indent_level).map(|_| {
397 div()
398 .w(*settings.list_indent_depth)
399 .h_full()
400 .flex()
401 .justify_center()
402 .group_hover("", |style| style.bg(theme.border_focused))
403 .child(
404 h_stack()
405 .child(div().w_px().h_full())
406 .child(div().w_px().h_full().bg(theme.border)),
407 )
408 }))
409 .flex()
410 .gap_1()
411 .items_center()
412 .relative()
413 .children(self.disclosure_control(cx))
414 .children(left_content)
415 .child(self.label),
416 )
417 }
418}
419
420struct ListDetailsEntryHandlers<V: 'static> {
421 click: Option<ClickHandler<V>>,
422}
423
424impl<V: 'static> Default for ListDetailsEntryHandlers<V> {
425 fn default() -> Self {
426 Self { click: None }
427 }
428}
429
430#[derive(Component)]
431pub struct ListDetailsEntry<V: 'static> {
432 label: SharedString,
433 meta: Option<SharedString>,
434 left_content: Option<LeftContent>,
435 handlers: ListDetailsEntryHandlers<V>,
436 actions: Option<Vec<Button<V>>>,
437 // TODO: make this more generic instead of
438 // specifically for notifications
439 seen: bool,
440}
441
442impl<V: 'static> ListDetailsEntry<V> {
443 pub fn new(label: impl Into<SharedString>) -> Self {
444 Self {
445 label: label.into(),
446 meta: None,
447 left_content: None,
448 handlers: ListDetailsEntryHandlers::default(),
449 actions: None,
450 seen: false,
451 }
452 }
453
454 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
455 self.meta = Some(meta.into());
456 self
457 }
458
459 pub fn seen(mut self, seen: bool) -> Self {
460 self.seen = seen;
461 self
462 }
463
464 pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
465 self.handlers.click = Some(handler);
466 self
467 }
468
469 pub fn actions(mut self, actions: Vec<Button<V>>) -> Self {
470 self.actions = Some(actions);
471 self
472 }
473
474 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
475 let theme = theme(cx);
476 let settings = user_settings(cx);
477
478 let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
479 true => (
480 theme.ghost_element,
481 theme.ghost_element_hover,
482 theme.ghost_element_active,
483 ),
484 false => (
485 theme.filled_element,
486 theme.filled_element_hover,
487 theme.filled_element_active,
488 ),
489 };
490
491 let label_color = match self.seen {
492 true => LabelColor::Muted,
493 false => LabelColor::Default,
494 };
495
496 v_stack()
497 .relative()
498 .group("")
499 .bg(item_bg)
500 .px_1()
501 .py_1_5()
502 .w_full()
503 .line_height(relative(1.2))
504 .child(Label::new(self.label.clone()).color(label_color))
505 .children(
506 self.meta
507 .map(|meta| Label::new(meta).color(LabelColor::Muted)),
508 )
509 .child(
510 h_stack()
511 .gap_1()
512 .justify_end()
513 .children(self.actions.unwrap_or_default()),
514 )
515 }
516}
517
518#[derive(Clone, Component)]
519pub struct ListSeparator;
520
521impl ListSeparator {
522 pub fn new() -> Self {
523 Self
524 }
525
526 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
527 let theme = theme(cx);
528
529 div().h_px().w_full().bg(theme.border)
530 }
531}
532
533#[derive(Component)]
534pub struct List<V: 'static> {
535 items: Vec<ListItem<V>>,
536 empty_message: SharedString,
537 header: Option<ListHeader>,
538 toggleable: Toggleable,
539}
540
541impl<V: 'static> List<V> {
542 pub fn new(items: Vec<ListItem<V>>) -> Self {
543 Self {
544 items,
545 empty_message: "No items".into(),
546 header: None,
547 toggleable: Toggleable::default(),
548 }
549 }
550
551 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
552 self.empty_message = empty_message.into();
553 self
554 }
555
556 pub fn header(mut self, header: ListHeader) -> Self {
557 self.header = Some(header);
558 self
559 }
560
561 pub fn toggle(mut self, toggle: ToggleState) -> Self {
562 self.toggleable = toggle.into();
563 self
564 }
565
566 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
567 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
568 let is_toggled = Toggleable::is_toggled(&self.toggleable);
569
570 let list_content = match (self.items.is_empty(), is_toggled) {
571 (_, false) => div(),
572 (false, _) => div().children(self.items),
573 (true, _) => {
574 div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
575 }
576 };
577
578 v_stack()
579 .py_1()
580 .children(self.header.map(|header| header.toggleable(self.toggleable)))
581 .child(list_content)
582 }
583}