list.rs

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