list.rs

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