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