list_item.rs

  1use std::sync::Arc;
  2
  3use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels};
  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<Arc<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(Arc::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 extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
145        self.children.extend(elements)
146    }
147}
148
149impl RenderOnce for ListItem {
150    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
151        h_flex()
152            .id(self.id)
153            .w_full()
154            .relative()
155            // When an item is inset draw the indent spacing outside of the item
156            .when(self.inset, |this| {
157                this.ml(self.indent_level as f32 * self.indent_step_size)
158                    .px_2()
159            })
160            .when(!self.inset && !self.disabled, |this| {
161                this
162                    // TODO: Add focus state
163                    // .when(self.state == InteractionState::Focused, |this| {
164                    //     this.border_1()
165                    //         .border_color(cx.theme().colors().border_focused)
166                    // })
167                    .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
168                    .active(|style| style.bg(cx.theme().colors().ghost_element_active))
169                    .when(self.selected, |this| {
170                        this.bg(cx.theme().colors().ghost_element_selected)
171                    })
172            })
173            .child(
174                h_flex()
175                    .id("inner_list_item")
176                    .w_full()
177                    .relative()
178                    .gap_1()
179                    .px_2()
180                    .map(|this| match self.spacing {
181                        ListItemSpacing::Dense => this,
182                        ListItemSpacing::Sparse => this.py_1(),
183                    })
184                    .group("list_item")
185                    .when(self.inset && !self.disabled, |this| {
186                        this
187                            // TODO: Add focus state
188                            // .when(self.state == InteractionState::Focused, |this| {
189                            //     this.border_1()
190                            //         .border_color(cx.theme().colors().border_focused)
191                            // })
192                            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
193                            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
194                            .when(self.selected, |this| {
195                                this.bg(cx.theme().colors().ghost_element_selected)
196                            })
197                    })
198                    .when_some(self.on_click, |this, on_click| {
199                        this.cursor_pointer().on_click(on_click)
200                    })
201                    .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
202                        this.on_mouse_down(MouseButton::Right, move |event, cx| {
203                            (on_mouse_down)(event, cx)
204                        })
205                    })
206                    .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
207                    .map(|this| {
208                        if self.inset {
209                            this.rounded_md()
210                        } else {
211                            // When an item is not inset draw the indent spacing inside of the item
212                            this.ml(self.indent_level as f32 * self.indent_step_size)
213                        }
214                    })
215                    .children(self.toggle.map(|is_open| {
216                        div()
217                            .flex()
218                            .absolute()
219                            .left(rems(-1.))
220                            .when(is_open, |this| this.visible_on_hover(""))
221                            .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle))
222                    }))
223                    .child(
224                        h_flex()
225                            .flex_grow()
226                            .flex_shrink_0()
227                            .flex_basis(relative(0.25))
228                            .gap_1()
229                            .overflow_hidden()
230                            .children(self.start_slot)
231                            .children(self.children),
232                    )
233                    .when_some(self.end_slot, |this, end_slot| {
234                        this.justify_between().child(
235                            h_flex()
236                                .flex_shrink()
237                                .overflow_hidden()
238                                .when(self.end_hover_slot.is_some(), |this| {
239                                    this.visible()
240                                        .group_hover("list_item", |this| this.invisible())
241                                })
242                                .child(end_slot),
243                        )
244                    })
245                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
246                        this.child(
247                            h_flex()
248                                .h_full()
249                                .absolute()
250                                .right_2()
251                                .top_0()
252                                .visible_on_hover("list_item")
253                                .child(end_hover_slot),
254                        )
255                    }),
256            )
257    }
258}