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<S: 'static>(&self) -> Div<S> {
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<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
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<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
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<S: 'static> {
196 Entry(ListEntry),
197 Details(ListDetailsEntry<S>),
198 Separator(ListSeparator),
199 Header(ListSubHeader),
200}
201
202impl<S: 'static> From<ListEntry> for ListItem<S> {
203 fn from(entry: ListEntry) -> Self {
204 Self::Entry(entry)
205 }
206}
207
208impl<S: 'static> From<ListDetailsEntry<S>> for ListItem<S> {
209 fn from(entry: ListDetailsEntry<S>) -> Self {
210 Self::Details(entry)
211 }
212}
213
214impl<S: 'static> From<ListSeparator> for ListItem<S> {
215 fn from(entry: ListSeparator) -> Self {
216 Self::Separator(entry)
217 }
218}
219
220impl<S: 'static> From<ListSubHeader> for ListItem<S> {
221 fn from(entry: ListSubHeader) -> Self {
222 Self::Header(entry)
223 }
224}
225
226impl<S: 'static> ListItem<S> {
227 fn render(self, view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
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: Option<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: Some(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<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
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 .children(self.label.take()),
416 )
417 }
418}
419
420struct ListDetailsEntryHandlers<S: 'static> {
421 click: Option<ClickHandler<S>>,
422}
423
424impl<S: 'static> Default for ListDetailsEntryHandlers<S> {
425 fn default() -> Self {
426 Self { click: None }
427 }
428}
429
430#[derive(Component)]
431pub struct ListDetailsEntry<S: 'static> {
432 label: SharedString,
433 meta: Option<SharedString>,
434 left_content: Option<LeftContent>,
435 handlers: ListDetailsEntryHandlers<S>,
436 actions: Option<Vec<Button<S>>>,
437 // TODO: make this more generic instead of
438 // specifically for notifications
439 seen: bool,
440}
441
442impl<S: 'static> ListDetailsEntry<S> {
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<S>) -> Self {
465 self.handlers.click = Some(handler);
466 self
467 }
468
469 pub fn actions(mut self, actions: Vec<Button<S>>) -> Self {
470 self.actions = Some(actions);
471 self
472 }
473
474 fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
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 .take()
508 .map(|meta| Label::new(meta).color(LabelColor::Muted)),
509 )
510 .child(
511 h_stack()
512 .gap_1()
513 .justify_end()
514 .children(self.actions.take().unwrap_or_default().into_iter()),
515 )
516 }
517}
518
519#[derive(Clone, Component)]
520pub struct ListSeparator;
521
522impl ListSeparator {
523 pub fn new() -> Self {
524 Self
525 }
526
527 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
528 let theme = theme(cx);
529
530 div().h_px().w_full().bg(theme.border)
531 }
532}
533
534#[derive(Component)]
535pub struct List<S: 'static> {
536 items: Vec<ListItem<S>>,
537 empty_message: SharedString,
538 header: Option<ListHeader>,
539 toggleable: Toggleable,
540}
541
542impl<S: 'static> List<S> {
543 pub fn new(items: Vec<ListItem<S>>) -> Self {
544 Self {
545 items,
546 empty_message: "No items".into(),
547 header: None,
548 toggleable: Toggleable::default(),
549 }
550 }
551
552 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
553 self.empty_message = empty_message.into();
554 self
555 }
556
557 pub fn header(mut self, header: ListHeader) -> Self {
558 self.header = Some(header);
559 self
560 }
561
562 pub fn toggle(mut self, toggle: ToggleState) -> Self {
563 self.toggleable = toggle.into();
564 self
565 }
566
567 fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
568 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
569 let is_toggled = Toggleable::is_toggled(&self.toggleable);
570
571 let list_content = match (self.items.is_empty(), is_toggled) {
572 (_, false) => div(),
573 (false, _) => div().children(self.items.drain(..)),
574 (true, _) => {
575 div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
576 }
577 };
578
579 v_stack()
580 .py_1()
581 .children(
582 self.header
583 .take()
584 .map(|header| header.toggleable(self.toggleable)),
585 )
586 .child(list_content)
587 }
588}