list.rs

  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}