list.rs

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