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