list.rs

  1use std::marker::PhantomData;
  2
  3use gpui3::{div, Div, Hsla, WindowContext};
  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    #[default]
 16    FullWidth,
 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::default(),
 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 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(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
 69            (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
 70        }
 71    }
 72
 73    fn background_color(&self, cx: &WindowContext) -> Hsla {
 74        let theme = theme(cx);
 75        let system_color = SystemColor::new();
 76
 77        match self.state {
 78            InteractionState::Hovered => theme.lowest.base.hovered.background,
 79            InteractionState::Active => theme.lowest.base.pressed.background,
 80            InteractionState::Enabled => theme.lowest.on.default.background,
 81            _ => system_color.transparent,
 82        }
 83    }
 84
 85    fn label_color(&self) -> LabelColor {
 86        match self.state {
 87            InteractionState::Disabled => LabelColor::Disabled,
 88            _ => Default::default(),
 89        }
 90    }
 91
 92    fn icon_color(&self) -> IconColor {
 93        match self.state {
 94            InteractionState::Disabled => IconColor::Disabled,
 95            _ => Default::default(),
 96        }
 97    }
 98
 99    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
100        let theme = theme(cx);
101        let token = token();
102        let system_color = SystemColor::new();
103        let background_color = self.background_color(cx);
104
105        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
106        let is_toggled = Toggleable::is_toggled(&self.toggleable);
107
108        let disclosure_control = self.disclosure_control();
109
110        h_stack()
111            .flex_1()
112            .w_full()
113            .fill(background_color)
114            .when(self.state == InteractionState::Focused, |this| {
115                this.border()
116                    .border_color(theme.lowest.accent.default.border)
117            })
118            .relative()
119            .py_1()
120            .child(
121                div()
122                    .h_6()
123                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
124                    .flex()
125                    .flex_1()
126                    .w_full()
127                    .gap_1()
128                    .items_center()
129                    .justify_between()
130                    .child(
131                        div()
132                            .flex()
133                            .gap_1()
134                            .items_center()
135                            .children(self.left_icon.map(|i| {
136                                IconElement::new(i)
137                                    .color(IconColor::Muted)
138                                    .size(IconSize::Small)
139                            }))
140                            .child(
141                                Label::new(self.label.clone())
142                                    .color(LabelColor::Muted)
143                                    .size(LabelSize::Small),
144                            ),
145                    )
146                    .child(disclosure_control),
147            )
148    }
149}
150
151#[derive(Element, Clone)]
152pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
153    state_type: PhantomData<S>,
154    label: &'static str,
155    left_icon: Option<Icon>,
156    variant: ListItemVariant,
157}
158
159impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
160    pub fn new(label: &'static str) -> Self {
161        Self {
162            state_type: PhantomData,
163            label,
164            left_icon: None,
165            variant: ListItemVariant::default(),
166        }
167    }
168
169    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
170        self.left_icon = left_icon;
171        self
172    }
173
174    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
175        let theme = theme(cx);
176        let token = token();
177
178        h_stack().flex_1().w_full().relative().py_1().child(
179            div()
180                .h_6()
181                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
182                .flex()
183                .flex_1()
184                .w_full()
185                .gap_1()
186                .items_center()
187                .justify_between()
188                .child(
189                    div()
190                        .flex()
191                        .gap_1()
192                        .items_center()
193                        .children(self.left_icon.map(|i| {
194                            IconElement::new(i)
195                                .color(IconColor::Muted)
196                                .size(IconSize::Small)
197                        }))
198                        .child(
199                            Label::new(self.label.clone())
200                                .color(LabelColor::Muted)
201                                .size(LabelSize::Small),
202                        ),
203                ),
204        )
205    }
206}
207
208#[derive(Clone)]
209pub enum LeftContent {
210    Icon(Icon),
211    Avatar(&'static str),
212}
213
214#[derive(Default, PartialEq, Copy, Clone)]
215pub enum ListEntrySize {
216    #[default]
217    Small,
218    Medium,
219}
220
221#[derive(Clone, Element)]
222pub enum ListItem<S: 'static + Send + Sync + Clone> {
223    Entry(ListEntry<S>),
224    Separator(ListSeparator<S>),
225    Header(ListSubHeader<S>),
226}
227
228impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
229    fn from(entry: ListEntry<S>) -> Self {
230        Self::Entry(entry)
231    }
232}
233
234impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
235    fn from(entry: ListSeparator<S>) -> Self {
236        Self::Separator(entry)
237    }
238}
239
240impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
241    fn from(entry: ListSubHeader<S>) -> Self {
242        Self::Header(entry)
243    }
244}
245
246impl<S: 'static + Send + Sync + Clone> ListItem<S> {
247    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
248        match self {
249            ListItem::Entry(entry) => div().child(entry.render(cx)),
250            ListItem::Separator(separator) => div().child(separator.render(cx)),
251            ListItem::Header(header) => div().child(header.render(cx)),
252        }
253    }
254
255    pub fn new(label: Label<S>) -> Self {
256        Self::Entry(ListEntry::new(label))
257    }
258
259    pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
260        if let Self::Entry(entry) = self {
261            Some(entry)
262        } else {
263            None
264        }
265    }
266}
267
268#[derive(Element, Clone)]
269pub struct ListEntry<S: 'static + Send + Sync + Clone> {
270    disclosure_control_style: DisclosureControlVisibility,
271    indent_level: u32,
272    label: Label<S>,
273    left_content: Option<LeftContent>,
274    variant: ListItemVariant,
275    size: ListEntrySize,
276    state: InteractionState,
277    toggle: Option<ToggleState>,
278}
279
280impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
281    pub fn new(label: Label<S>) -> Self {
282        Self {
283            disclosure_control_style: DisclosureControlVisibility::default(),
284            indent_level: 0,
285            label,
286            variant: ListItemVariant::default(),
287            left_content: None,
288            size: ListEntrySize::default(),
289            state: InteractionState::default(),
290            toggle: None,
291        }
292    }
293    pub fn variant(mut self, variant: ListItemVariant) -> Self {
294        self.variant = variant;
295        self
296    }
297    pub fn indent_level(mut self, indent_level: u32) -> Self {
298        self.indent_level = indent_level;
299        self
300    }
301
302    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
303        self.toggle = Some(toggle);
304        self
305    }
306
307    pub fn left_content(mut self, left_content: LeftContent) -> Self {
308        self.left_content = Some(left_content);
309        self
310    }
311
312    pub fn left_icon(mut self, left_icon: Icon) -> Self {
313        self.left_content = Some(LeftContent::Icon(left_icon));
314        self
315    }
316
317    pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
318        self.left_content = Some(LeftContent::Avatar(left_avatar));
319        self
320    }
321
322    pub fn state(mut self, state: InteractionState) -> Self {
323        self.state = state;
324        self
325    }
326
327    pub fn size(mut self, size: ListEntrySize) -> Self {
328        self.size = size;
329        self
330    }
331
332    pub fn disclosure_control_style(
333        mut self,
334        disclosure_control_style: DisclosureControlVisibility,
335    ) -> Self {
336        self.disclosure_control_style = disclosure_control_style;
337        self
338    }
339
340    fn background_color(&self, cx: &WindowContext) -> Hsla {
341        let theme = theme(cx);
342        let system_color = SystemColor::new();
343
344        match self.state {
345            InteractionState::Hovered => theme.lowest.base.hovered.background,
346            InteractionState::Active => theme.lowest.base.pressed.background,
347            InteractionState::Enabled => theme.lowest.on.default.background,
348            _ => system_color.transparent,
349        }
350    }
351
352    fn label_color(&self) -> LabelColor {
353        match self.state {
354            InteractionState::Disabled => LabelColor::Disabled,
355            _ => Default::default(),
356        }
357    }
358
359    fn icon_color(&self) -> IconColor {
360        match self.state {
361            InteractionState::Disabled => IconColor::Disabled,
362            _ => Default::default(),
363        }
364    }
365
366    fn disclosure_control(&mut self, cx: &mut ViewContext<S>) -> Option<impl Element<State = S>> {
367        let theme = theme(cx);
368        let token = token();
369
370        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
371            IconElement::new(Icon::ChevronDown)
372        } else {
373            IconElement::new(Icon::ChevronRight)
374        }
375        .color(IconColor::Muted)
376        .size(IconSize::Small);
377
378        match (self.toggle, self.disclosure_control_style) {
379            (Some(_), DisclosureControlVisibility::OnHover) => {
380                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
381            }
382            (Some(_), DisclosureControlVisibility::Always) => {
383                Some(div().child(disclosure_control_icon))
384            }
385            (None, _) => None,
386        }
387    }
388
389    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
390        let theme = theme(cx);
391        let token = token();
392        let system_color = SystemColor::new();
393        let background_color = self.background_color(cx);
394
395        let left_content = match self.left_content {
396            Some(LeftContent::Icon(i)) => {
397                Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
398            }
399            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
400            None => None,
401        };
402
403        let sized_item = match self.size {
404            ListEntrySize::Small => div().h_6(),
405            ListEntrySize::Medium => div().h_7(),
406        };
407
408        div()
409            .fill(background_color)
410            .when(self.state == InteractionState::Focused, |this| {
411                this.border()
412                    .border_color(theme.lowest.accent.default.border)
413            })
414            .relative()
415            .py_1()
416            .child(
417                sized_item
418                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
419                    // .ml(rems(0.75 * self.indent_level as f32))
420                    .children((0..self.indent_level).map(|_| {
421                        div()
422                            .w(token.list_indent_depth)
423                            .h_full()
424                            .flex()
425                            .justify_center()
426                            .child(h_stack().child(div().w_px().h_full()).child(
427                                div().w_px().h_full().fill(theme.middle.base.default.border),
428                            ))
429                    }))
430                    .flex()
431                    .gap_1()
432                    .items_center()
433                    .relative()
434                    .children(self.disclosure_control(cx))
435                    .children(left_content)
436                    .child(self.label.clone()),
437            )
438    }
439}
440
441#[derive(Clone, Element)]
442pub struct ListSeparator<S: 'static + Send + Sync> {
443    state_type: PhantomData<S>,
444}
445
446impl<S: 'static + Send + Sync> ListSeparator<S> {
447    pub fn new() -> Self {
448        Self {
449            state_type: PhantomData,
450        }
451    }
452
453    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
454        let theme = theme(cx);
455
456        div().h_px().w_full().fill(theme.lowest.base.default.border)
457    }
458}
459
460#[derive(Element)]
461pub struct List<S: 'static + Send + Sync + Clone> {
462    items: Vec<ListItem<S>>,
463    empty_message: &'static str,
464    header: Option<ListHeader<S>>,
465    toggleable: Toggleable,
466}
467
468impl<S: 'static + Send + Sync + Clone> List<S> {
469    pub fn new(items: Vec<ListItem<S>>) -> Self {
470        Self {
471            items,
472            empty_message: "No items",
473            header: None,
474            toggleable: Toggleable::default(),
475        }
476    }
477
478    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
479        self.empty_message = empty_message;
480        self
481    }
482
483    pub fn header(mut self, header: ListHeader<S>) -> Self {
484        self.header = Some(header);
485        self
486    }
487
488    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
489        self.toggleable = toggle.into();
490        self
491    }
492
493    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
494        let theme = theme(cx);
495        let token = token();
496        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
497        let is_toggled = Toggleable::is_toggled(&self.toggleable);
498
499        let list_content = match (self.items.is_empty(), is_toggled) {
500            (_, false) => div(),
501            (false, _) => div().children(self.items.iter().cloned()),
502            (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
503        };
504
505        v_stack()
506            .py_1()
507            .children(
508                self.header
509                    .clone()
510                    .map(|header| header.set_toggleable(self.toggleable)),
511            )
512            .child(list_content)
513    }
514}