list_item.rs

  1use std::sync::Arc;
  2
  3use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels, px};
  4use smallvec::SmallVec;
  5
  6use crate::{Disclosure, prelude::*};
  7
  8#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
  9pub enum ListItemSpacing {
 10    #[default]
 11    Dense,
 12    ExtraDense,
 13    Sparse,
 14}
 15
 16#[derive(IntoElement)]
 17pub struct ListItem {
 18    id: ElementId,
 19    group_name: Option<SharedString>,
 20    disabled: bool,
 21    selected: bool,
 22    spacing: ListItemSpacing,
 23    indent_level: usize,
 24    indent_step_size: Pixels,
 25    /// A slot for content that appears before the children, like an icon or avatar.
 26    start_slot: Option<AnyElement>,
 27    /// A slot for content that appears after the children, usually on the other side of the header.
 28    /// This might be a button, a disclosure arrow, a face pile, etc.
 29    end_slot: Option<AnyElement>,
 30    /// A slot for content that appears on hover after the children
 31    /// It will obscure the `end_slot` when visible.
 32    end_hover_slot: Option<AnyElement>,
 33    toggle: Option<bool>,
 34    inset: bool,
 35    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 36    on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
 37    on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 38    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
 39    on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
 40    children: SmallVec<[AnyElement; 2]>,
 41    selectable: bool,
 42    always_show_disclosure_icon: bool,
 43    outlined: bool,
 44    rounded: bool,
 45    overflow_x: bool,
 46    focused: Option<bool>,
 47}
 48
 49impl ListItem {
 50    pub fn new(id: impl Into<ElementId>) -> Self {
 51        Self {
 52            id: id.into(),
 53            group_name: None,
 54            disabled: false,
 55            selected: false,
 56            spacing: ListItemSpacing::Dense,
 57            indent_level: 0,
 58            indent_step_size: px(12.),
 59            start_slot: None,
 60            end_slot: None,
 61            end_hover_slot: None,
 62            toggle: None,
 63            inset: false,
 64            on_click: None,
 65            on_secondary_mouse_down: None,
 66            on_toggle: None,
 67            on_hover: None,
 68            tooltip: None,
 69            children: SmallVec::new(),
 70            selectable: true,
 71            always_show_disclosure_icon: false,
 72            outlined: false,
 73            rounded: false,
 74            overflow_x: false,
 75            focused: None,
 76        }
 77    }
 78
 79    pub fn group_name(mut self, group_name: impl Into<SharedString>) -> Self {
 80        self.group_name = Some(group_name.into());
 81        self
 82    }
 83
 84    pub fn spacing(mut self, spacing: ListItemSpacing) -> Self {
 85        self.spacing = spacing;
 86        self
 87    }
 88
 89    pub fn selectable(mut self, has_hover: bool) -> Self {
 90        self.selectable = has_hover;
 91        self
 92    }
 93
 94    pub fn always_show_disclosure_icon(mut self, show: bool) -> Self {
 95        self.always_show_disclosure_icon = show;
 96        self
 97    }
 98
 99    pub fn on_click(
100        mut self,
101        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
102    ) -> Self {
103        self.on_click = Some(Box::new(handler));
104        self
105    }
106
107    pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
108        self.on_hover = Some(Box::new(handler));
109        self
110    }
111
112    pub fn on_secondary_mouse_down(
113        mut self,
114        handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
115    ) -> Self {
116        self.on_secondary_mouse_down = Some(Box::new(handler));
117        self
118    }
119
120    pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
121        self.tooltip = Some(Box::new(tooltip));
122        self
123    }
124
125    pub fn inset(mut self, inset: bool) -> Self {
126        self.inset = inset;
127        self
128    }
129
130    pub fn indent_level(mut self, indent_level: usize) -> Self {
131        self.indent_level = indent_level;
132        self
133    }
134
135    pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
136        self.indent_step_size = indent_step_size;
137        self
138    }
139
140    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
141        self.toggle = toggle.into();
142        self
143    }
144
145    pub fn on_toggle(
146        mut self,
147        on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
148    ) -> Self {
149        self.on_toggle = Some(Arc::new(on_toggle));
150        self
151    }
152
153    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
154        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
155        self
156    }
157
158    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
159        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
160        self
161    }
162
163    pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
164        self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
165        self
166    }
167
168    pub fn outlined(mut self) -> Self {
169        self.outlined = true;
170        self
171    }
172
173    pub fn rounded(mut self) -> Self {
174        self.rounded = true;
175        self
176    }
177
178    pub fn overflow_x(mut self) -> Self {
179        self.overflow_x = true;
180        self
181    }
182
183    pub fn focused(mut self, focused: bool) -> Self {
184        self.focused = Some(focused);
185        self
186    }
187}
188
189impl Disableable for ListItem {
190    fn disabled(mut self, disabled: bool) -> Self {
191        self.disabled = disabled;
192        self
193    }
194}
195
196impl Toggleable for ListItem {
197    fn toggle_state(mut self, selected: bool) -> Self {
198        self.selected = selected;
199        self
200    }
201}
202
203impl ParentElement for ListItem {
204    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
205        self.children.extend(elements)
206    }
207}
208
209impl RenderOnce for ListItem {
210    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
211        h_flex()
212            .id(self.id)
213            .when_some(self.group_name, |this, group| this.group(group))
214            .w_full()
215            .relative()
216            // When an item is inset draw the indent spacing outside of the item
217            .when(self.inset, |this| {
218                this.ml(self.indent_level as f32 * self.indent_step_size)
219                    .px(DynamicSpacing::Base04.rems(cx))
220            })
221            .when(!self.inset && !self.disabled, |this| {
222                this
223                    // TODO: Add focus state
224                    // .when(self.state == InteractionState::Focused, |this| {
225                    .when_some(self.focused, |this, focused| {
226                        if focused {
227                            this.border_1()
228                                .border_color(cx.theme().colors().border_focused)
229                        } else {
230                            this.border_1()
231                        }
232                    })
233                    .when(self.selectable, |this| {
234                        this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
235                            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
236                            .when(self.outlined, |this| this.rounded_sm())
237                            .when(self.selected, |this| {
238                                this.bg(cx.theme().colors().ghost_element_selected)
239                            })
240                    })
241            })
242            .when(self.rounded, |this| this.rounded_sm())
243            .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
244            .child(
245                h_flex()
246                    .id("inner_list_item")
247                    .group("list_item")
248                    .w_full()
249                    .relative()
250                    .gap_1()
251                    .px(DynamicSpacing::Base06.rems(cx))
252                    .map(|this| match self.spacing {
253                        ListItemSpacing::Dense => this,
254                        ListItemSpacing::ExtraDense => this.py_neg_px(),
255                        ListItemSpacing::Sparse => this.py_1(),
256                    })
257                    .when(self.inset && !self.disabled, |this| {
258                        this
259                            // TODO: Add focus state
260                            //.when(self.state == InteractionState::Focused, |this| {
261                            .when_some(self.focused, |this, focused| {
262                                if focused {
263                                    this.border_1()
264                                        .border_color(cx.theme().colors().border_focused)
265                                } else {
266                                    this.border_1()
267                                }
268                            })
269                            .when(self.selectable, |this| {
270                                this.hover(|style| {
271                                    style.bg(cx.theme().colors().ghost_element_hover)
272                                })
273                                .active(|style| style.bg(cx.theme().colors().ghost_element_active))
274                                .when(self.selected, |this| {
275                                    this.bg(cx.theme().colors().ghost_element_selected)
276                                })
277                            })
278                    })
279                    .when_some(
280                        self.on_click.filter(|_| !self.disabled),
281                        |this, on_click| this.cursor_pointer().on_click(on_click),
282                    )
283                    .when(self.outlined, |this| {
284                        this.border_1()
285                            .border_color(cx.theme().colors().border)
286                            .rounded_sm()
287                            .overflow_hidden()
288                    })
289                    .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
290                        this.on_mouse_down(MouseButton::Right, move |event, window, cx| {
291                            (on_mouse_down)(event, window, cx)
292                        })
293                    })
294                    .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
295                    .map(|this| {
296                        if self.inset {
297                            this.rounded_sm()
298                        } else {
299                            // When an item is not inset draw the indent spacing inside of the item
300                            this.ml(self.indent_level as f32 * self.indent_step_size)
301                        }
302                    })
303                    .children(self.toggle.map(|is_open| {
304                        div()
305                            .flex()
306                            .absolute()
307                            .left(rems(-1.))
308                            .when(is_open && !self.always_show_disclosure_icon, |this| {
309                                this.visible_on_hover("")
310                            })
311                            .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle))
312                    }))
313                    .child(
314                        h_flex()
315                            .flex_grow()
316                            .flex_shrink_0()
317                            .flex_basis(relative(0.25))
318                            .gap(DynamicSpacing::Base06.rems(cx))
319                            .map(|list_content| {
320                                if self.overflow_x {
321                                    list_content
322                                } else {
323                                    list_content.overflow_hidden()
324                                }
325                            })
326                            .children(self.start_slot)
327                            .children(self.children),
328                    )
329                    .when_some(self.end_slot, |this, end_slot| {
330                        this.justify_between().child(
331                            h_flex()
332                                .flex_shrink()
333                                .overflow_hidden()
334                                .when(self.end_hover_slot.is_some(), |this| {
335                                    this.visible()
336                                        .group_hover("list_item", |this| this.invisible())
337                                })
338                                .child(end_slot),
339                        )
340                    })
341                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
342                        this.child(
343                            h_flex()
344                                .h_full()
345                                .absolute()
346                                .right(DynamicSpacing::Base06.rems(cx))
347                                .top_0()
348                                .visible_on_hover("list_item")
349                                .child(end_hover_slot),
350                        )
351                    }),
352            )
353    }
354}