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