list.rs

  1use std::marker::PhantomData;
  2
  3use gpui3::{div, Div};
  4
  5use crate::prelude::*;
  6use crate::theme::theme;
  7use crate::{
  8    h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor,
  9    LabelSize,
 10};
 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, Clone)]
 21pub struct ListHeader<S: 'static + Send + Sync + Clone> {
 22    state_type: PhantomData<S>,
 23    label: &'static str,
 24    left_icon: Option<Icon>,
 25    variant: ListItemVariant,
 26    state: InteractionState,
 27    toggleable: Toggleable,
 28}
 29
 30impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
 31    pub fn new(label: &'static str) -> Self {
 32        Self {
 33            state_type: PhantomData,
 34            label,
 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 theme = theme(cx);
 97        let token = token();
 98        let system_color = SystemColor::new();
 99        let color = ThemeColor::new(cx);
100
101        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
102        let is_toggled = Toggleable::is_toggled(&self.toggleable);
103
104        let disclosure_control = self.disclosure_control();
105
106        h_stack()
107            .flex_1()
108            .w_full()
109            .fill(color.surface)
110            .when(self.state == InteractionState::Focused, |this| {
111                this.border().border_color(color.border_focused)
112            })
113            .relative()
114            .child(
115                div()
116                    .h_5()
117                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
118                    .flex()
119                    .flex_1()
120                    .w_full()
121                    .gap_1()
122                    .items_center()
123                    .child(
124                        div()
125                            .flex()
126                            .gap_1()
127                            .items_center()
128                            .children(self.left_icon.map(|i| {
129                                IconElement::new(i)
130                                    .color(IconColor::Muted)
131                                    .size(IconSize::Small)
132                            }))
133                            .child(
134                                Label::new(self.label)
135                                    .color(LabelColor::Muted)
136                                    .size(LabelSize::Small),
137                            ),
138                    )
139                    .child(disclosure_control),
140            )
141    }
142}
143
144#[derive(Element, Clone)]
145pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
146    state_type: PhantomData<S>,
147    label: &'static str,
148    left_icon: Option<Icon>,
149    variant: ListItemVariant,
150}
151
152impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
153    pub fn new(label: &'static str) -> Self {
154        Self {
155            state_type: PhantomData,
156            label,
157            left_icon: None,
158            variant: ListItemVariant::default(),
159        }
160    }
161
162    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
163        self.left_icon = left_icon;
164        self
165    }
166
167    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
168        let theme = theme(cx);
169        let token = token();
170
171        h_stack().flex_1().w_full().relative().py_1().child(
172            div()
173                .h_6()
174                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
175                .flex()
176                .flex_1()
177                .w_full()
178                .gap_1()
179                .items_center()
180                .justify_between()
181                .child(
182                    div()
183                        .flex()
184                        .gap_1()
185                        .items_center()
186                        .children(self.left_icon.map(|i| {
187                            IconElement::new(i)
188                                .color(IconColor::Muted)
189                                .size(IconSize::Small)
190                        }))
191                        .child(
192                            Label::new(self.label)
193                                .color(LabelColor::Muted)
194                                .size(LabelSize::Small),
195                        ),
196                ),
197        )
198    }
199}
200
201#[derive(Clone)]
202pub enum LeftContent {
203    Icon(Icon),
204    Avatar(&'static str),
205}
206
207#[derive(Default, PartialEq, Copy, Clone)]
208pub enum ListEntrySize {
209    #[default]
210    Small,
211    Medium,
212}
213
214#[derive(Clone, Element)]
215pub enum ListItem<S: 'static + Send + Sync + Clone> {
216    Entry(ListEntry<S>),
217    Separator(ListSeparator<S>),
218    Header(ListSubHeader<S>),
219}
220
221impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
222    fn from(entry: ListEntry<S>) -> Self {
223        Self::Entry(entry)
224    }
225}
226
227impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
228    fn from(entry: ListSeparator<S>) -> Self {
229        Self::Separator(entry)
230    }
231}
232
233impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
234    fn from(entry: ListSubHeader<S>) -> Self {
235        Self::Header(entry)
236    }
237}
238
239impl<S: 'static + Send + Sync + Clone> ListItem<S> {
240    fn render(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
241        match self {
242            ListItem::Entry(entry) => div().child(entry.render(view, cx)),
243            ListItem::Separator(separator) => div().child(separator.render(view, cx)),
244            ListItem::Header(header) => div().child(header.render(view, cx)),
245        }
246    }
247
248    pub fn new(label: Label<S>) -> Self {
249        Self::Entry(ListEntry::new(label))
250    }
251
252    pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
253        if let Self::Entry(entry) = self {
254            Some(entry)
255        } else {
256            None
257        }
258    }
259}
260
261#[derive(Element, Clone)]
262pub struct ListEntry<S: 'static + Send + Sync + Clone> {
263    disclosure_control_style: DisclosureControlVisibility,
264    indent_level: u32,
265    label: Label<S>,
266    left_content: Option<LeftContent>,
267    variant: ListItemVariant,
268    size: ListEntrySize,
269    state: InteractionState,
270    toggle: Option<ToggleState>,
271}
272
273impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
274    pub fn new(label: Label<S>) -> Self {
275        Self {
276            disclosure_control_style: DisclosureControlVisibility::default(),
277            indent_level: 0,
278            label,
279            variant: ListItemVariant::default(),
280            left_content: None,
281            size: ListEntrySize::default(),
282            state: InteractionState::default(),
283            // TODO: Should use Toggleable::NotToggleable
284            // or remove Toggleable::NotToggleable from the system
285            toggle: None,
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: &'static str) -> Self {
313        self.left_content = Some(LeftContent::Avatar(left_avatar));
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 theme = theme(cx);
354        let token = token();
355
356        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
357            IconElement::new(Icon::ChevronDown)
358        } else {
359            IconElement::new(Icon::ChevronRight)
360        }
361        .color(IconColor::Muted)
362        .size(IconSize::Small);
363
364        match (self.toggle, self.disclosure_control_style) {
365            (Some(_), DisclosureControlVisibility::OnHover) => {
366                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
367            }
368            (Some(_), DisclosureControlVisibility::Always) => {
369                Some(div().child(disclosure_control_icon))
370            }
371            (None, _) => None,
372        }
373    }
374
375    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
376        let theme = theme(cx);
377        let token = token();
378        let system_color = SystemColor::new();
379        let color = ThemeColor::new(cx);
380
381        let left_content = match self.left_content {
382            Some(LeftContent::Icon(i)) => Some(
383                h_stack().child(
384                    IconElement::new(i)
385                        .size(IconSize::Small)
386                        .color(IconColor::Muted),
387                ),
388            ),
389            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
390            None => None,
391        };
392
393        let sized_item = match self.size {
394            ListEntrySize::Small => div().h_6(),
395            ListEntrySize::Medium => div().h_7(),
396        };
397
398        div()
399            .relative()
400            .group("")
401            .fill(color.surface)
402            .when(self.state == InteractionState::Focused, |this| {
403                this.border().border_color(color.border_focused)
404            })
405            .child(
406                sized_item
407                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
408                    // .ml(rems(0.75 * self.indent_level as f32))
409                    .children((0..self.indent_level).map(|_| {
410                        div()
411                            .w(token.list_indent_depth)
412                            .h_full()
413                            .flex()
414                            .justify_center()
415                            .group_hover("")
416                            .fill(color.border_focused)
417                            .child(
418                                h_stack()
419                                    .child(div().w_px().h_full())
420                                    .child(div().w_px().h_full().fill(color.border)),
421                            )
422                    }))
423                    .flex()
424                    .gap_1()
425                    .items_center()
426                    .relative()
427                    .children(self.disclosure_control(cx))
428                    .children(left_content)
429                    .child(self.label.clone()),
430            )
431    }
432}
433
434#[derive(Clone, Element)]
435pub struct ListSeparator<S: 'static + Send + Sync> {
436    state_type: PhantomData<S>,
437}
438
439impl<S: 'static + Send + Sync> ListSeparator<S> {
440    pub fn new() -> Self {
441        Self {
442            state_type: PhantomData,
443        }
444    }
445
446    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
447        let color = ThemeColor::new(cx);
448
449        div().h_px().w_full().fill(color.border)
450    }
451}
452
453#[derive(Element)]
454pub struct List<S: 'static + Send + Sync + Clone> {
455    items: Vec<ListItem<S>>,
456    empty_message: &'static str,
457    header: Option<ListHeader<S>>,
458    toggleable: Toggleable,
459}
460
461impl<S: 'static + Send + Sync + Clone> List<S> {
462    pub fn new(items: Vec<ListItem<S>>) -> Self {
463        Self {
464            items,
465            empty_message: "No items",
466            header: None,
467            toggleable: Toggleable::default(),
468        }
469    }
470
471    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
472        self.empty_message = empty_message;
473        self
474    }
475
476    pub fn header(mut self, header: ListHeader<S>) -> Self {
477        self.header = Some(header);
478        self
479    }
480
481    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
482        self.toggleable = toggle.into();
483        self
484    }
485
486    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
487        let theme = theme(cx);
488        let token = token();
489        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
490        let is_toggled = Toggleable::is_toggled(&self.toggleable);
491
492        let list_content = match (self.items.is_empty(), is_toggled) {
493            (_, false) => div(),
494            (false, _) => div().children(self.items.iter().cloned()),
495            (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
496        };
497
498        v_stack()
499            .py_1()
500            .children(
501                self.header
502                    .clone()
503                    .map(|header| header.set_toggleable(self.toggleable)),
504            )
505            .child(list_content)
506    }
507}