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<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}