list.rs

  1use gpui2::elements::div::Div;
  2use gpui2::{Hsla, WindowContext};
  3
  4use crate::prelude::*;
  5use crate::{
  6    h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor,
  7    IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor,
  8    ToggleState,
  9};
 10
 11#[derive(Clone, Copy, Default, Debug, PartialEq)]
 12pub enum ListItemVariant {
 13    /// The list item extends to the far left and right of the list.
 14    #[default]
 15    FullWidth,
 16    Inset,
 17}
 18
 19#[derive(Element, Clone, Copy)]
 20pub struct ListHeader {
 21    label: &'static str,
 22    left_icon: Option<Icon>,
 23    variant: ListItemVariant,
 24    state: InteractionState,
 25    toggleable: Toggleable,
 26}
 27
 28impl ListHeader {
 29    pub fn new(label: &'static str) -> Self {
 30        Self {
 31            label,
 32            left_icon: None,
 33            variant: ListItemVariant::default(),
 34            state: InteractionState::default(),
 35            toggleable: Toggleable::default(),
 36        }
 37    }
 38
 39    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
 40        self.toggleable = toggle.into();
 41        self
 42    }
 43
 44    pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
 45        self.toggleable = toggleable;
 46        self
 47    }
 48
 49    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
 50        self.left_icon = left_icon;
 51        self
 52    }
 53
 54    pub fn state(mut self, state: InteractionState) -> Self {
 55        self.state = state;
 56        self
 57    }
 58
 59    fn disclosure_control<V: 'static>(&self) -> Div<V> {
 60        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
 61        let is_toggled = Toggleable::is_toggled(&self.toggleable);
 62
 63        match (is_toggleable, is_toggled) {
 64            (false, _) => div(),
 65            (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
 66            (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
 67        }
 68    }
 69
 70    fn background_color(&self, cx: &WindowContext) -> Hsla {
 71        let theme = theme(cx);
 72        let system_color = SystemColor::new();
 73
 74        match self.state {
 75            InteractionState::Hovered => theme.lowest.base.hovered.background,
 76            InteractionState::Active => theme.lowest.base.pressed.background,
 77            InteractionState::Enabled => theme.lowest.on.default.background,
 78            _ => system_color.transparent,
 79        }
 80    }
 81
 82    fn label_color(&self) -> LabelColor {
 83        match self.state {
 84            InteractionState::Disabled => LabelColor::Disabled,
 85            _ => Default::default(),
 86        }
 87    }
 88
 89    fn icon_color(&self) -> IconColor {
 90        match self.state {
 91            InteractionState::Disabled => IconColor::Disabled,
 92            _ => Default::default(),
 93        }
 94    }
 95
 96    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
 97        let theme = theme(cx);
 98        let token = token();
 99        let system_color = SystemColor::new();
100        let background_color = self.background_color(cx);
101
102        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
103        let is_toggled = Toggleable::is_toggled(&self.toggleable);
104
105        let disclosure_control = self.disclosure_control();
106
107        h_stack()
108            .flex_1()
109            .w_full()
110            .fill(background_color)
111            .when(self.state == InteractionState::Focused, |this| {
112                this.border()
113                    .border_color(theme.lowest.accent.default.border)
114            })
115            .relative()
116            .py_1()
117            .child(
118                div()
119                    .h_6()
120                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
121                    .flex()
122                    .flex_1()
123                    .w_full()
124                    .gap_1()
125                    .items_center()
126                    .justify_between()
127                    .child(
128                        div()
129                            .flex()
130                            .gap_1()
131                            .items_center()
132                            .children(self.left_icon.map(|i| {
133                                IconElement::new(i)
134                                    .color(IconColor::Muted)
135                                    .size(IconSize::Small)
136                            }))
137                            .child(
138                                Label::new(self.label.clone())
139                                    .color(LabelColor::Muted)
140                                    .size(LabelSize::Small),
141                            ),
142                    )
143                    .child(disclosure_control),
144            )
145    }
146}
147
148#[derive(Element, Clone, Copy)]
149pub struct ListSubHeader {
150    label: &'static str,
151    left_icon: Option<Icon>,
152    variant: ListItemVariant,
153}
154
155impl ListSubHeader {
156    pub fn new(label: &'static str) -> Self {
157        Self {
158            label,
159            left_icon: None,
160            variant: ListItemVariant::default(),
161        }
162    }
163
164    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
165        self.left_icon = left_icon;
166        self
167    }
168
169    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
170        let theme = theme(cx);
171        let token = token();
172
173        h_stack().flex_1().w_full().relative().py_1().child(
174            div()
175                .h_6()
176                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
177                .flex()
178                .flex_1()
179                .w_full()
180                .gap_1()
181                .items_center()
182                .justify_between()
183                .child(
184                    div()
185                        .flex()
186                        .gap_1()
187                        .items_center()
188                        .children(self.left_icon.map(|i| {
189                            IconElement::new(i)
190                                .color(IconColor::Muted)
191                                .size(IconSize::Small)
192                        }))
193                        .child(
194                            Label::new(self.label.clone())
195                                .color(LabelColor::Muted)
196                                .size(LabelSize::Small),
197                        ),
198                ),
199        )
200    }
201}
202
203#[derive(Clone)]
204pub enum LeftContent {
205    Icon(Icon),
206    Avatar(&'static str),
207}
208
209#[derive(Default, PartialEq, Copy, Clone)]
210pub enum ListEntrySize {
211    #[default]
212    Small,
213    Medium,
214}
215
216#[derive(Clone, Element)]
217pub enum ListItem {
218    Entry(ListEntry),
219    Separator(ListSeparator),
220    Header(ListSubHeader),
221}
222
223impl From<ListEntry> for ListItem {
224    fn from(entry: ListEntry) -> Self {
225        Self::Entry(entry)
226    }
227}
228
229impl From<ListSeparator> for ListItem {
230    fn from(entry: ListSeparator) -> Self {
231        Self::Separator(entry)
232    }
233}
234
235impl From<ListSubHeader> for ListItem {
236    fn from(entry: ListSubHeader) -> Self {
237        Self::Header(entry)
238    }
239}
240
241impl ListItem {
242    fn render<V: 'static>(&mut self, v: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
243        match self {
244            ListItem::Entry(entry) => div().child(entry.render(v, cx)),
245            ListItem::Separator(separator) => div().child(separator.render(v, cx)),
246            ListItem::Header(header) => div().child(header.render(v, cx)),
247        }
248    }
249    pub fn new(label: Label) -> Self {
250        Self::Entry(ListEntry::new(label))
251    }
252    pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
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 {
263    disclosure_control_style: DisclosureControlVisibility,
264    indent_level: u32,
265    label: Label,
266    left_content: Option<LeftContent>,
267    variant: ListItemVariant,
268    size: ListEntrySize,
269    state: InteractionState,
270    toggle: Option<ToggleState>,
271}
272
273impl ListEntry {
274    pub fn new(label: Label) -> 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            toggle: None,
284        }
285    }
286    pub fn variant(mut self, variant: ListItemVariant) -> Self {
287        self.variant = variant;
288        self
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 set_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: &'static str) -> Self {
311        self.left_content = Some(LeftContent::Avatar(left_avatar));
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 background_color(&self, cx: &WindowContext) -> Hsla {
334        let theme = theme(cx);
335        let system_color = SystemColor::new();
336
337        match self.state {
338            InteractionState::Hovered => theme.lowest.base.hovered.background,
339            InteractionState::Active => theme.lowest.base.pressed.background,
340            InteractionState::Enabled => theme.lowest.on.default.background,
341            _ => system_color.transparent,
342        }
343    }
344
345    fn label_color(&self) -> LabelColor {
346        match self.state {
347            InteractionState::Disabled => LabelColor::Disabled,
348            _ => Default::default(),
349        }
350    }
351
352    fn icon_color(&self) -> IconColor {
353        match self.state {
354            InteractionState::Disabled => IconColor::Disabled,
355            _ => Default::default(),
356        }
357    }
358
359    fn disclosure_control<V: 'static>(
360        &mut self,
361        cx: &mut ViewContext<V>,
362    ) -> Option<impl IntoElement<V>> {
363        let theme = theme(cx);
364        let token = token();
365
366        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
367            IconElement::new(Icon::ChevronDown)
368        } else {
369            IconElement::new(Icon::ChevronRight)
370        }
371        .color(IconColor::Muted)
372        .size(IconSize::Small);
373
374        match (self.toggle, self.disclosure_control_style) {
375            (Some(_), DisclosureControlVisibility::OnHover) => {
376                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
377            }
378            (Some(_), DisclosureControlVisibility::Always) => {
379                Some(div().child(disclosure_control_icon))
380            }
381            (None, _) => None,
382        }
383    }
384
385    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
386        let theme = theme(cx);
387        let token = token();
388        let system_color = SystemColor::new();
389        let background_color = self.background_color(cx);
390
391        let left_content = match self.left_content {
392            Some(LeftContent::Icon(i)) => {
393                Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
394            }
395            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
396            None => None,
397        };
398
399        let sized_item = match self.size {
400            ListEntrySize::Small => div().h_6(),
401            ListEntrySize::Medium => div().h_7(),
402        };
403
404        div()
405            .fill(background_color)
406            .when(self.state == InteractionState::Focused, |this| {
407                this.border()
408                    .border_color(theme.lowest.accent.default.border)
409            })
410            .relative()
411            .py_1()
412            .child(
413                sized_item
414                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
415                    // .ml(rems(0.75 * self.indent_level as f32))
416                    .children((0..self.indent_level).map(|_| {
417                        div()
418                            .w(token.list_indent_depth)
419                            .h_full()
420                            .flex()
421                            .justify_center()
422                            .child(h_stack().child(div().w_px().h_full()).child(
423                                div().w_px().h_full().fill(theme.middle.base.default.border),
424                            ))
425                    }))
426                    .flex()
427                    .gap_1()
428                    .items_center()
429                    .relative()
430                    .children(self.disclosure_control(cx))
431                    .children(left_content)
432                    .child(self.label.clone()),
433            )
434    }
435}
436
437#[derive(Clone, Default, Element)]
438pub struct ListSeparator;
439
440impl ListSeparator {
441    pub fn new() -> Self {
442        Self::default()
443    }
444
445    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
446        let theme = theme(cx);
447
448        div().h_px().w_full().fill(theme.lowest.base.default.border)
449    }
450}
451
452#[derive(Element)]
453pub struct List {
454    items: Vec<ListItem>,
455    empty_message: &'static str,
456    header: Option<ListHeader>,
457    toggleable: Toggleable,
458}
459
460impl List {
461    pub fn new(items: Vec<ListItem>) -> Self {
462        Self {
463            items,
464            empty_message: "No items",
465            header: None,
466            toggleable: Toggleable::default(),
467        }
468    }
469
470    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
471        self.empty_message = empty_message;
472        self
473    }
474
475    pub fn header(mut self, header: ListHeader) -> Self {
476        self.header = Some(header);
477        self
478    }
479
480    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
481        self.toggleable = toggle.into();
482        self
483    }
484
485    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
486        let theme = theme(cx);
487        let token = token();
488        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
489        let is_toggled = Toggleable::is_toggled(&self.toggleable);
490
491        let disclosure_control = if is_toggleable {
492            IconElement::new(Icon::ChevronRight)
493        } else {
494            IconElement::new(Icon::ChevronDown)
495        };
496
497        let list_content = match (self.items.is_empty(), is_toggled) {
498            (_, false) => div(),
499            (false, _) => div().children(self.items.iter().cloned()),
500            (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
501        };
502
503        v_stack()
504            .py_1()
505            .children(
506                self.header
507                    .clone()
508                    .map(|header| header.set_toggleable(self.toggleable)),
509            )
510            .child(list_content)
511    }
512}