list.rs

  1use gpui::{
  2    div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
  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(&ClickEvent, &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(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
274        self.on_click = Some(Rc::new(handler));
275        self
276    }
277
278    pub fn variant(mut self, variant: ListItemVariant) -> Self {
279        self.variant = variant;
280        self
281    }
282
283    pub fn indent_level(mut self, indent_level: u32) -> Self {
284        self.indent_level = indent_level;
285        self
286    }
287
288    pub fn toggle(mut self, toggle: Toggle) -> Self {
289        self.toggle = toggle;
290        self
291    }
292
293    pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
294        self.left_slot = Some(left_content);
295        self
296    }
297
298    pub fn left_icon(mut self, left_icon: Icon) -> Self {
299        self.left_slot = Some(GraphicSlot::Icon(left_icon));
300        self
301    }
302
303    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
304        self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
305        self
306    }
307
308    pub fn size(mut self, size: ListEntrySize) -> Self {
309        self.size = size;
310        self
311    }
312}
313
314impl RenderOnce for ListItem {
315    type Rendered = Stateful<Div>;
316
317    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
318        let left_content = match self.left_slot.clone() {
319            Some(GraphicSlot::Icon(i)) => Some(
320                h_stack().child(
321                    IconElement::new(i)
322                        .size(IconSize::Small)
323                        .color(Color::Muted),
324                ),
325            ),
326            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
327            Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
328            None => None,
329        };
330
331        let sized_item = match self.size {
332            ListEntrySize::Small => div().h_6(),
333            ListEntrySize::Medium => div().h_7(),
334        };
335        div()
336            .id(self.id)
337            .relative()
338            .hover(|mut style| {
339                style.background = Some(cx.theme().colors().editor_background.into());
340                style
341            })
342            .on_click({
343                let on_click = self.on_click.clone();
344                move |event, cx| {
345                    if let Some(on_click) = &on_click {
346                        (on_click)(event, cx)
347                    }
348                }
349            })
350            // TODO: Add focus state
351            // .when(self.state == InteractionState::Focused, |this| {
352            //     this.border()
353            //         .border_color(cx.theme().colors().border_focused)
354            // })
355            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
356            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
357            .child(
358                sized_item
359                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
360                    // .ml(rems(0.75 * self.indent_level as f32))
361                    .children((0..self.indent_level).map(|_| {
362                        div()
363                            .w(px(4.))
364                            .h_full()
365                            .flex()
366                            .justify_center()
367                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
368                            .child(
369                                h_stack()
370                                    .child(div().w_px().h_full())
371                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
372                            )
373                    }))
374                    .flex()
375                    .gap_1()
376                    .items_center()
377                    .relative()
378                    .child(disclosure_control(self.toggle))
379                    .children(left_content)
380                    .children(self.children),
381            )
382    }
383}
384
385impl ParentElement for ListItem {
386    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
387        &mut self.children
388    }
389}
390
391#[derive(IntoElement, Clone)]
392pub struct ListSeparator;
393
394impl ListSeparator {
395    pub fn new() -> Self {
396        Self
397    }
398}
399
400impl RenderOnce for ListSeparator {
401    type Rendered = Div;
402
403    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
404        div().h_px().w_full().bg(cx.theme().colors().border_variant)
405    }
406}
407
408#[derive(IntoElement)]
409pub struct List {
410    /// Message to display when the list is empty
411    /// Defaults to "No items"
412    empty_message: SharedString,
413    header: Option<ListHeader>,
414    toggle: Toggle,
415    children: SmallVec<[AnyElement; 2]>,
416}
417
418impl RenderOnce for List {
419    type Rendered = Div;
420
421    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
422        let list_content = match (self.children.is_empty(), self.toggle) {
423            (false, _) => div().children(self.children),
424            (true, Toggle::Toggled(false)) => div(),
425            (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
426        };
427
428        v_stack()
429            .w_full()
430            .py_1()
431            .children(self.header.map(|header| header))
432            .child(list_content)
433    }
434}
435
436impl List {
437    pub fn new() -> Self {
438        Self {
439            empty_message: "No items".into(),
440            header: None,
441            toggle: Toggle::NotToggleable,
442            children: SmallVec::new(),
443        }
444    }
445
446    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
447        self.empty_message = empty_message.into();
448        self
449    }
450
451    pub fn header(mut self, header: ListHeader) -> Self {
452        self.header = Some(header);
453        self
454    }
455
456    pub fn toggle(mut self, toggle: Toggle) -> Self {
457        self.toggle = toggle;
458        self
459    }
460}
461
462impl ParentElement for List {
463    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
464        &mut self.children
465    }
466}