list.rs

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