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