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