list.rs

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