list.rs

  1use gpui::{
  2    div, px, AnyElement, ClickEvent, Div, RenderOnce, Stateful, StatefulInteractiveElement,
  3};
  4use smallvec::SmallVec;
  5use std::rc::Rc;
  6
  7use crate::{
  8    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
  9};
 10use crate::{prelude::*, GraphicSlot};
 11
 12#[derive(Clone, Copy, Default, Debug, PartialEq)]
 13pub enum ListItemVariant {
 14    /// The list item extends to the far left and right of the list.
 15    FullWidth,
 16    #[default]
 17    Inset,
 18}
 19
 20pub enum ListHeaderMeta {
 21    // TODO: These should be IconButtons
 22    Tools(Vec<Icon>),
 23    // TODO: This should be a button
 24    Button(Label),
 25    Text(Label),
 26}
 27
 28#[derive(RenderOnce)]
 29pub struct ListHeader {
 30    label: SharedString,
 31    left_icon: Option<Icon>,
 32    meta: Option<ListHeaderMeta>,
 33    variant: ListItemVariant,
 34    toggle: Toggle,
 35}
 36
 37impl Component 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| {
 49                        IconElement::new(i)
 50                            .color(Color::Muted)
 51                            .size(IconSize::Small)
 52                    })),
 53            ),
 54            Some(ListHeaderMeta::Button(label)) => div().child(label),
 55            Some(ListHeaderMeta::Text(label)) => div().child(label),
 56            None => div(),
 57        };
 58
 59        h_stack()
 60            .w_full()
 61            .bg(cx.theme().colors().surface_background)
 62            .relative()
 63            .child(
 64                div()
 65                    .h_5()
 66                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
 67                    .flex()
 68                    .flex_1()
 69                    .items_center()
 70                    .justify_between()
 71                    .w_full()
 72                    .gap_1()
 73                    .child(
 74                        h_stack()
 75                            .gap_1()
 76                            .child(
 77                                div()
 78                                    .flex()
 79                                    .gap_1()
 80                                    .items_center()
 81                                    .children(self.left_icon.map(|i| {
 82                                        IconElement::new(i)
 83                                            .color(Color::Muted)
 84                                            .size(IconSize::Small)
 85                                    }))
 86                                    .child(Label::new(self.label.clone()).color(Color::Muted)),
 87                            )
 88                            .child(disclosure_control),
 89                    )
 90                    .child(meta),
 91            )
 92    }
 93}
 94
 95impl ListHeader {
 96    pub fn new(label: impl Into<SharedString>) -> Self {
 97        Self {
 98            label: label.into(),
 99            left_icon: None,
100            meta: None,
101            variant: ListItemVariant::default(),
102            toggle: Toggle::NotToggleable,
103        }
104    }
105
106    pub fn toggle(mut self, toggle: Toggle) -> Self {
107        self.toggle = toggle;
108        self
109    }
110
111    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
112        self.left_icon = left_icon;
113        self
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(RenderOnce, 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 Component 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(RenderOnce)]
242pub struct ListItem {
243    id: ElementId,
244    disabled: bool,
245    // TODO: Reintroduce this
246    // disclosure_control_style: DisclosureControlVisibility,
247    indent_level: u32,
248    label: Label,
249    left_slot: Option<GraphicSlot>,
250    overflow: OverflowStyle,
251    size: ListEntrySize,
252    toggle: Toggle,
253    variant: ListItemVariant,
254    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
255}
256
257impl Clone for ListItem {
258    fn clone(&self) -> Self {
259        Self {
260            id: self.id.clone(),
261            disabled: self.disabled,
262            indent_level: self.indent_level,
263            label: self.label.clone(),
264            left_slot: self.left_slot.clone(),
265            overflow: self.overflow,
266            size: self.size,
267            toggle: self.toggle,
268            variant: self.variant,
269            on_click: self.on_click.clone(),
270        }
271    }
272}
273
274impl ListItem {
275    pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
276        Self {
277            id: id.into(),
278            disabled: false,
279            indent_level: 0,
280            label,
281            left_slot: None,
282            overflow: OverflowStyle::Hidden,
283            size: ListEntrySize::default(),
284            toggle: Toggle::NotToggleable,
285            variant: ListItemVariant::default(),
286            on_click: Default::default(),
287        }
288    }
289
290    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
291        self.on_click = Some(Rc::new(handler));
292        self
293    }
294
295    pub fn variant(mut self, variant: ListItemVariant) -> Self {
296        self.variant = variant;
297        self
298    }
299
300    pub fn indent_level(mut self, indent_level: u32) -> Self {
301        self.indent_level = indent_level;
302        self
303    }
304
305    pub fn toggle(mut self, toggle: Toggle) -> Self {
306        self.toggle = toggle;
307        self
308    }
309
310    pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
311        self.left_slot = Some(left_content);
312        self
313    }
314
315    pub fn left_icon(mut self, left_icon: Icon) -> Self {
316        self.left_slot = Some(GraphicSlot::Icon(left_icon));
317        self
318    }
319
320    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
321        self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
322        self
323    }
324
325    pub fn size(mut self, size: ListEntrySize) -> Self {
326        self.size = size;
327        self
328    }
329}
330
331impl Component for ListItem {
332    type Rendered = Stateful<Div>;
333
334    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
335        let left_content = match self.left_slot.clone() {
336            Some(GraphicSlot::Icon(i)) => Some(
337                h_stack().child(
338                    IconElement::new(i)
339                        .size(IconSize::Small)
340                        .color(Color::Muted),
341                ),
342            ),
343            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
344            Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
345            None => None,
346        };
347
348        let sized_item = match self.size {
349            ListEntrySize::Small => div().h_6(),
350            ListEntrySize::Medium => div().h_7(),
351        };
352        div()
353            .id(self.id)
354            .relative()
355            .hover(|mut style| {
356                style.background = Some(cx.theme().colors().editor_background.into());
357                style
358            })
359            .on_click({
360                let on_click = self.on_click.clone();
361                move |event, cx| {
362                    if let Some(on_click) = &on_click {
363                        (on_click)(event, cx)
364                    }
365                }
366            })
367            .bg(cx.theme().colors().surface_background)
368            // TODO: Add focus state
369            // .when(self.state == InteractionState::Focused, |this| {
370            //     this.border()
371            //         .border_color(cx.theme().colors().border_focused)
372            // })
373            .child(
374                sized_item
375                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
376                    // .ml(rems(0.75 * self.indent_level as f32))
377                    .children((0..self.indent_level).map(|_| {
378                        div()
379                            .w(px(4.))
380                            .h_full()
381                            .flex()
382                            .justify_center()
383                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
384                            .child(
385                                h_stack()
386                                    .child(div().w_px().h_full())
387                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
388                            )
389                    }))
390                    .flex()
391                    .gap_1()
392                    .items_center()
393                    .relative()
394                    .child(disclosure_control(self.toggle))
395                    .children(left_content)
396                    .child(self.label),
397            )
398    }
399}
400
401#[derive(RenderOnce, Clone)]
402pub struct ListSeparator;
403
404impl ListSeparator {
405    pub fn new() -> Self {
406        Self
407    }
408}
409
410impl Component for ListSeparator {
411    type Rendered = Div;
412
413    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
414        div().h_px().w_full().bg(cx.theme().colors().border_variant)
415    }
416}
417
418#[derive(RenderOnce)]
419pub struct List {
420    /// Message to display when the list is empty
421    /// Defaults to "No items"
422    empty_message: SharedString,
423    header: Option<ListHeader>,
424    toggle: Toggle,
425    children: SmallVec<[AnyElement; 2]>,
426}
427
428impl Component for List {
429    type Rendered = Div;
430
431    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
432        let list_content = match (self.children.is_empty(), self.toggle) {
433            (false, _) => div().children(self.children),
434            (true, Toggle::Toggled(false)) => div(),
435            (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
436        };
437
438        v_stack()
439            .w_full()
440            .py_1()
441            .children(self.header.map(|header| header))
442            .child(list_content)
443    }
444}
445
446impl List {
447    pub fn new() -> Self {
448        Self {
449            empty_message: "No items".into(),
450            header: None,
451            toggle: Toggle::NotToggleable,
452            children: SmallVec::new(),
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) -> Self {
462        self.header = Some(header);
463        self
464    }
465
466    pub fn toggle(mut self, toggle: Toggle) -> Self {
467        self.toggle = toggle;
468        self
469    }
470}
471
472impl ParentElement for List {
473    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
474        &mut self.children
475    }
476}