list.rs

  1use gpui::{div, Div, RenderOnce, Stateful, StatefulInteractiveElement};
  2use std::rc::Rc;
  3
  4use crate::settings::user_settings;
  5use crate::{
  6    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
  7};
  8use crate::{prelude::*, GraphicSlot};
  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
 18pub enum ListHeaderMeta {
 19    // TODO: These should be IconButtons
 20    Tools(Vec<Icon>),
 21    // TODO: This should be a button
 22    Button(Label),
 23    Text(Label),
 24}
 25
 26#[derive(RenderOnce)]
 27pub struct ListHeader {
 28    label: SharedString,
 29    left_icon: Option<Icon>,
 30    meta: Option<ListHeaderMeta>,
 31    variant: ListItemVariant,
 32    toggle: Toggle,
 33}
 34
 35impl<V: 'static> Component<V> for ListHeader {
 36    type Rendered = Div<V>;
 37
 38    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
 39        let disclosure_control = disclosure_control(self.toggle);
 40
 41        let meta = match self.meta {
 42            Some(ListHeaderMeta::Tools(icons)) => div().child(
 43                h_stack()
 44                    .gap_2()
 45                    .items_center()
 46                    .children(icons.into_iter().map(|i| {
 47                        IconElement::new(i)
 48                            .color(TextColor::Muted)
 49                            .size(IconSize::Small)
 50                    })),
 51            ),
 52            Some(ListHeaderMeta::Button(label)) => div().child(label),
 53            Some(ListHeaderMeta::Text(label)) => div().child(label),
 54            None => div(),
 55        };
 56
 57        h_stack()
 58            .w_full()
 59            .bg(cx.theme().colors().surface_background)
 60            .relative()
 61            .child(
 62                div()
 63                    .h_5()
 64                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
 65                    .flex()
 66                    .flex_1()
 67                    .items_center()
 68                    .justify_between()
 69                    .w_full()
 70                    .gap_1()
 71                    .child(
 72                        h_stack()
 73                            .gap_1()
 74                            .child(
 75                                div()
 76                                    .flex()
 77                                    .gap_1()
 78                                    .items_center()
 79                                    .children(self.left_icon.map(|i| {
 80                                        IconElement::new(i)
 81                                            .color(TextColor::Muted)
 82                                            .size(IconSize::Small)
 83                                    }))
 84                                    .child(Label::new(self.label.clone()).color(TextColor::Muted)),
 85                            )
 86                            .child(disclosure_control),
 87                    )
 88                    .child(meta),
 89            )
 90    }
 91}
 92
 93impl ListHeader {
 94    pub fn new(label: impl Into<SharedString>) -> Self {
 95        Self {
 96            label: label.into(),
 97            left_icon: None,
 98            meta: None,
 99            variant: ListItemVariant::default(),
100            toggle: Toggle::NotToggleable,
101        }
102    }
103
104    pub fn toggle(mut self, toggle: Toggle) -> Self {
105        self.toggle = toggle;
106        self
107    }
108
109    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
110        self.left_icon = left_icon;
111        self
112    }
113
114    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
115        self.meta = meta;
116        self
117    }
118
119    // before_ship!("delete")
120    // fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
121    //     let disclosure_control = disclosure_control(self.toggle);
122
123    //     let meta = match self.meta {
124    //         Some(ListHeaderMeta::Tools(icons)) => div().child(
125    //             h_stack()
126    //                 .gap_2()
127    //                 .items_center()
128    //                 .children(icons.into_iter().map(|i| {
129    //                     IconElement::new(i)
130    //                         .color(TextColor::Muted)
131    //                         .size(IconSize::Small)
132    //                 })),
133    //         ),
134    //         Some(ListHeaderMeta::Button(label)) => div().child(label),
135    //         Some(ListHeaderMeta::Text(label)) => div().child(label),
136    //         None => div(),
137    //     };
138
139    //     h_stack()
140    //         .w_full()
141    //         .bg(cx.theme().colors().surface_background)
142    //         // TODO: Add focus state
143    //         // .when(self.state == InteractionState::Focused, |this| {
144    //         //     this.border()
145    //         //         .border_color(cx.theme().colors().border_focused)
146    //         // })
147    //         .relative()
148    //         .child(
149    //             div()
150    //                 .h_5()
151    //                 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
152    //                 .flex()
153    //                 .flex_1()
154    //                 .items_center()
155    //                 .justify_between()
156    //                 .w_full()
157    //                 .gap_1()
158    //                 .child(
159    //                     h_stack()
160    //                         .gap_1()
161    //                         .child(
162    //                             div()
163    //                                 .flex()
164    //                                 .gap_1()
165    //                                 .items_center()
166    //                                 .children(self.left_icon.map(|i| {
167    //                                     IconElement::new(i)
168    //                                         .color(TextColor::Muted)
169    //                                         .size(IconSize::Small)
170    //                                 }))
171    //                                 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
172    //                         )
173    //                         .child(disclosure_control),
174    //                 )
175    //                 .child(meta),
176    //         )
177    // }
178}
179
180#[derive(Clone)]
181pub struct ListSubHeader {
182    label: SharedString,
183    left_icon: Option<Icon>,
184    variant: ListItemVariant,
185}
186
187impl ListSubHeader {
188    pub fn new(label: impl Into<SharedString>) -> Self {
189        Self {
190            label: label.into(),
191            left_icon: None,
192            variant: ListItemVariant::default(),
193        }
194    }
195
196    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
197        self.left_icon = left_icon;
198        self
199    }
200
201    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
202        h_stack().flex_1().w_full().relative().py_1().child(
203            div()
204                .h_6()
205                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
206                .flex()
207                .flex_1()
208                .w_full()
209                .gap_1()
210                .items_center()
211                .justify_between()
212                .child(
213                    div()
214                        .flex()
215                        .gap_1()
216                        .items_center()
217                        .children(self.left_icon.map(|i| {
218                            IconElement::new(i)
219                                .color(TextColor::Muted)
220                                .size(IconSize::Small)
221                        }))
222                        .child(Label::new(self.label.clone()).color(TextColor::Muted)),
223                ),
224        )
225    }
226}
227
228#[derive(Default, PartialEq, Copy, Clone)]
229pub enum ListEntrySize {
230    #[default]
231    Small,
232    Medium,
233}
234
235#[derive(Clone)]
236pub enum ListItem<V: 'static> {
237    Entry(ListEntry<V>),
238    Separator(ListSeparator),
239    Header(ListSubHeader),
240}
241
242impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
243    fn from(entry: ListEntry<V>) -> Self {
244        Self::Entry(entry)
245    }
246}
247
248impl<V: 'static> From<ListSeparator> for ListItem<V> {
249    fn from(entry: ListSeparator) -> Self {
250        Self::Separator(entry)
251    }
252}
253
254impl<V: 'static> From<ListSubHeader> for ListItem<V> {
255    fn from(entry: ListSubHeader) -> Self {
256        Self::Header(entry)
257    }
258}
259
260impl<V: 'static> ListItem<V> {
261    pub fn new(label: Label) -> Self {
262        Self::Entry(ListEntry::new(label))
263    }
264
265    pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
266        if let Self::Entry(entry) = self {
267            Some(entry)
268        } else {
269            None
270        }
271    }
272
273    fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> Div<V> {
274        match self {
275            ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
276            ListItem::Separator(separator) => div().child(separator.render(view, cx)),
277            ListItem::Header(header) => div().child(header.render(view, cx)),
278        }
279    }
280}
281
282// #[derive(RenderOnce)]
283pub struct ListEntry<V> {
284    disabled: bool,
285    // TODO: Reintroduce this
286    // disclosure_control_style: DisclosureControlVisibility,
287    indent_level: u32,
288    label: Label,
289    left_slot: Option<GraphicSlot>,
290    overflow: OverflowStyle,
291    size: ListEntrySize,
292    toggle: Toggle,
293    variant: ListItemVariant,
294    on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
295}
296
297impl<V> Clone for ListEntry<V> {
298    fn clone(&self) -> Self {
299        Self {
300            disabled: self.disabled,
301            indent_level: self.indent_level,
302            label: self.label.clone(),
303            left_slot: self.left_slot.clone(),
304            overflow: self.overflow,
305            size: self.size,
306            toggle: self.toggle,
307            variant: self.variant,
308            on_click: self.on_click.clone(),
309        }
310    }
311}
312
313impl<V: 'static> ListEntry<V> {
314    pub fn new(label: Label) -> Self {
315        Self {
316            disabled: false,
317            indent_level: 0,
318            label,
319            left_slot: None,
320            overflow: OverflowStyle::Hidden,
321            size: ListEntrySize::default(),
322            toggle: Toggle::NotToggleable,
323            variant: ListItemVariant::default(),
324            on_click: Default::default(),
325        }
326    }
327
328    pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
329        self.on_click = Some(Rc::new(handler));
330        self
331    }
332
333    pub fn variant(mut self, variant: ListItemVariant) -> Self {
334        self.variant = variant;
335        self
336    }
337
338    pub fn indent_level(mut self, indent_level: u32) -> Self {
339        self.indent_level = indent_level;
340        self
341    }
342
343    pub fn toggle(mut self, toggle: Toggle) -> Self {
344        self.toggle = toggle;
345        self
346    }
347
348    pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
349        self.left_slot = Some(left_content);
350        self
351    }
352
353    pub fn left_icon(mut self, left_icon: Icon) -> Self {
354        self.left_slot = Some(GraphicSlot::Icon(left_icon));
355        self
356    }
357
358    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
359        self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
360        self
361    }
362
363    pub fn size(mut self, size: ListEntrySize) -> Self {
364        self.size = size;
365        self
366    }
367
368    fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
369        let settings = user_settings(cx);
370
371        let left_content = match self.left_slot.clone() {
372            Some(GraphicSlot::Icon(i)) => Some(
373                h_stack().child(
374                    IconElement::new(i)
375                        .size(IconSize::Small)
376                        .color(TextColor::Muted),
377                ),
378            ),
379            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
380            Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
381            None => None,
382        };
383
384        let sized_item = match self.size {
385            ListEntrySize::Small => div().h_6(),
386            ListEntrySize::Medium => div().h_7(),
387        };
388        div()
389            .id(ix)
390            .relative()
391            .hover(|mut style| {
392                style.background = Some(cx.theme().colors().editor_background.into());
393                style
394            })
395            .on_click({
396                let on_click = self.on_click.clone();
397
398                move |view: &mut V, event, cx| {
399                    if let Some(on_click) = &on_click {
400                        (on_click)(view, cx)
401                    }
402                }
403            })
404            .bg(cx.theme().colors().surface_background)
405            // TODO: Add focus state
406            // .when(self.state == InteractionState::Focused, |this| {
407            //     this.border()
408            //         .border_color(cx.theme().colors().border_focused)
409            // })
410            .child(
411                sized_item
412                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
413                    // .ml(rems(0.75 * self.indent_level as f32))
414                    .children((0..self.indent_level).map(|_| {
415                        div()
416                            .w(*settings.list_indent_depth)
417                            .h_full()
418                            .flex()
419                            .justify_center()
420                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
421                            .child(
422                                h_stack()
423                                    .child(div().w_px().h_full())
424                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
425                            )
426                    }))
427                    .flex()
428                    .gap_1()
429                    .items_center()
430                    .relative()
431                    .child(disclosure_control(self.toggle))
432                    .children(left_content)
433                    .child(self.label),
434            )
435    }
436}
437
438#[derive(RenderOnce, Clone)]
439pub struct ListSeparator;
440
441impl ListSeparator {
442    pub fn new() -> Self {
443        Self
444    }
445}
446
447impl<V: 'static> Component<V> for ListSeparator {
448    type Rendered = Div<V>;
449
450    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
451        div().h_px().w_full().bg(cx.theme().colors().border_variant)
452    }
453}
454
455#[derive(RenderOnce)]
456pub struct List<V: 'static> {
457    items: Vec<ListItem<V>>,
458    /// Message to display when the list is empty
459    /// Defaults to "No items"
460    empty_message: SharedString,
461    header: Option<ListHeader>,
462    toggle: Toggle,
463}
464
465impl<V: 'static> Component<V> for List<V> {
466    type Rendered = Div<V>;
467
468    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
469        let list_content = match (self.items.is_empty(), self.toggle) {
470            (false, _) => div().children(
471                self.items
472                    .into_iter()
473                    .enumerate()
474                    .map(|(ix, item)| item.render(view, ix, cx)),
475            ),
476            (true, Toggle::Toggled(false)) => div(),
477            (true, _) => {
478                div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
479            }
480        };
481
482        v_stack()
483            .w_full()
484            .py_1()
485            .children(self.header.map(|header| header))
486            .child(list_content)
487    }
488}
489
490impl<V: 'static> List<V> {
491    pub fn new(items: Vec<ListItem<V>>) -> Self {
492        Self {
493            items,
494            empty_message: "No items".into(),
495            header: None,
496            toggle: Toggle::NotToggleable,
497        }
498    }
499
500    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
501        self.empty_message = empty_message.into();
502        self
503    }
504
505    pub fn header(mut self, header: ListHeader) -> Self {
506        self.header = Some(header);
507        self
508    }
509
510    pub fn toggle(mut self, toggle: Toggle) -> Self {
511        self.toggle = toggle;
512        self
513    }
514
515    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
516        let list_content = match (self.items.is_empty(), self.toggle) {
517            (false, _) => div().children(
518                self.items
519                    .into_iter()
520                    .enumerate()
521                    .map(|(ix, item)| item.render(view, ix, cx)),
522            ),
523            (true, Toggle::Toggled(false)) => div(),
524            (true, _) => {
525                div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
526            }
527        };
528
529        v_stack()
530            .w_full()
531            .py_1()
532            .children(self.header.map(|header| header))
533            .child(list_content)
534    }
535}