list.rs

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