list_item.rs

  1use gpui::{
  2    px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
  3};
  4use smallvec::SmallVec;
  5
  6use crate::{prelude::*, Disclosure};
  7
  8#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
  9pub enum ListItemSpacing {
 10    #[default]
 11    Dense,
 12    Sparse,
 13}
 14
 15#[derive(IntoElement)]
 16pub struct ListItem {
 17    id: ElementId,
 18    disabled: bool,
 19    selected: bool,
 20    spacing: ListItemSpacing,
 21    indent_level: usize,
 22    indent_step_size: Pixels,
 23    /// A slot for content that appears before the children, like an icon or avatar.
 24    start_slot: Option<AnyElement>,
 25    /// A slot for content that appears after the children, usually on the other side of the header.
 26    /// This might be a button, a disclosure arrow, a face pile, etc.
 27    end_slot: Option<AnyElement>,
 28    /// A slot for content that appears on hover after the children
 29    /// It will obscure the `end_slot` when visible.
 30    end_hover_slot: Option<AnyElement>,
 31    toggle: Option<bool>,
 32    inset: bool,
 33    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 34    on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 35    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
 36    on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
 37    children: SmallVec<[AnyElement; 2]>,
 38}
 39
 40impl ListItem {
 41    pub fn new(id: impl Into<ElementId>) -> Self {
 42        Self {
 43            id: id.into(),
 44            disabled: false,
 45            selected: false,
 46            spacing: ListItemSpacing::Dense,
 47            indent_level: 0,
 48            indent_step_size: px(12.),
 49            start_slot: None,
 50            end_slot: None,
 51            end_hover_slot: None,
 52            toggle: None,
 53            inset: false,
 54            on_click: None,
 55            on_secondary_mouse_down: None,
 56            on_toggle: None,
 57            tooltip: None,
 58            children: SmallVec::new(),
 59        }
 60    }
 61
 62    pub fn spacing(mut self, spacing: ListItemSpacing) -> Self {
 63        self.spacing = spacing;
 64        self
 65    }
 66
 67    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
 68        self.on_click = Some(Box::new(handler));
 69        self
 70    }
 71
 72    pub fn on_secondary_mouse_down(
 73        mut self,
 74        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
 75    ) -> Self {
 76        self.on_secondary_mouse_down = Some(Box::new(handler));
 77        self
 78    }
 79
 80    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
 81        self.tooltip = Some(Box::new(tooltip));
 82        self
 83    }
 84
 85    pub fn inset(mut self, inset: bool) -> Self {
 86        self.inset = inset;
 87        self
 88    }
 89
 90    pub fn indent_level(mut self, indent_level: usize) -> Self {
 91        self.indent_level = indent_level;
 92        self
 93    }
 94
 95    pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
 96        self.indent_step_size = indent_step_size;
 97        self
 98    }
 99
100    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
101        self.toggle = toggle.into();
102        self
103    }
104
105    pub fn on_toggle(
106        mut self,
107        on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
108    ) -> Self {
109        self.on_toggle = Some(Box::new(on_toggle));
110        self
111    }
112
113    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
114        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
115        self
116    }
117
118    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
119        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
120        self
121    }
122
123    pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
124        self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
125        self
126    }
127}
128
129impl Disableable for ListItem {
130    fn disabled(mut self, disabled: bool) -> Self {
131        self.disabled = disabled;
132        self
133    }
134}
135
136impl Selectable for ListItem {
137    fn selected(mut self, selected: bool) -> Self {
138        self.selected = selected;
139        self
140    }
141}
142
143impl ParentElement for ListItem {
144    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
145        &mut self.children
146    }
147}
148
149impl RenderOnce for ListItem {
150    type Rendered = Stateful<Div>;
151
152    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
153        h_stack()
154            .id(self.id)
155            .w_full()
156            .relative()
157            // When an item is inset draw the indent spacing outside of the item
158            .when(self.inset, |this| {
159                this.ml(self.indent_level as f32 * self.indent_step_size)
160                    .px_2()
161            })
162            .when(!self.inset, |this| {
163                this
164                    // TODO: Add focus state
165                    // .when(self.state == InteractionState::Focused, |this| {
166                    //     this.border()
167                    //         .border_color(cx.theme().colors().border_focused)
168                    // })
169                    .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
170                    .active(|style| style.bg(cx.theme().colors().ghost_element_active))
171                    .when(self.selected, |this| {
172                        this.bg(cx.theme().colors().ghost_element_selected)
173                    })
174            })
175            .child(
176                h_stack()
177                    .id("inner_list_item")
178                    .w_full()
179                    .relative()
180                    .gap_1()
181                    .px_2()
182                    .map(|this| match self.spacing {
183                        ListItemSpacing::Dense => this,
184                        ListItemSpacing::Sparse => this.py_1(),
185                    })
186                    .group("list_item")
187                    .when(self.inset && !self.disabled, |this| {
188                        this
189                            // TODO: Add focus state
190                            // .when(self.state == InteractionState::Focused, |this| {
191                            //     this.border()
192                            //         .border_color(cx.theme().colors().border_focused)
193                            // })
194                            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
195                            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
196                            .when(self.selected, |this| {
197                                this.bg(cx.theme().colors().ghost_element_selected)
198                            })
199                    })
200                    .when_some(self.on_click, |this, on_click| {
201                        this.cursor_pointer().on_click(on_click)
202                    })
203                    .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
204                        this.on_mouse_down(MouseButton::Right, move |event, cx| {
205                            (on_mouse_down)(event, cx)
206                        })
207                    })
208                    .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
209                    .map(|this| {
210                        if self.inset {
211                            this.rounded_md()
212                        } else {
213                            // When an item is not inset draw the indent spacing inside of the item
214                            this.ml(self.indent_level as f32 * self.indent_step_size)
215                        }
216                    })
217                    .children(self.toggle.map(|is_open| {
218                        div()
219                            .flex()
220                            .absolute()
221                            .left(rems(-1.))
222                            .when(is_open, |this| this.visible_on_hover(""))
223                            .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle))
224                    }))
225                    .child(
226                        h_stack()
227                            .flex_1()
228                            .gap_1()
229                            .children(self.start_slot)
230                            .children(self.children),
231                    )
232                    .when_some(self.end_slot, |this, end_slot| {
233                        this.justify_between().child(
234                            h_stack()
235                                .when(self.end_hover_slot.is_some(), |this| {
236                                    this.visible()
237                                        .group_hover("list_item", |this| this.invisible())
238                                })
239                                .child(end_slot),
240                        )
241                    })
242                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
243                        this.child(
244                            h_stack()
245                                .h_full()
246                                .absolute()
247                                .right_2()
248                                .top_0()
249                                .visible_on_hover("list_item")
250                                .child(end_hover_slot),
251                        )
252                    }),
253            )
254    }
255}