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