Skip to main content

gpui_component/list/
list_item.rs

1use crate::{ActiveTheme, Disableable, Icon, Selectable, Sizable as _, StyledExt, h_flex};
2use gpui::{
3    AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, Interactivity, IntoElement,
4    MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, RenderOnce, Stateful,
5    StatefulInteractiveElement, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
6};
7use smallvec::SmallVec;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11enum ListItemMode {
12    #[default]
13    Entry,
14    Separator,
15}
16
17impl ListItemMode {
18    #[inline]
19    fn is_separator(&self) -> bool {
20        matches!(self, ListItemMode::Separator)
21    }
22}
23
24#[derive(IntoElement)]
25pub struct ListItem {
26    base: Stateful<Div>,
27    mode: ListItemMode,
28    style: StyleRefinement,
29    disabled: bool,
30    selected: bool,
31    secondary_selected: bool,
32    confirmed: bool,
33    check_icon: Option<Icon>,
34    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
35    on_mouse_down:
36        HashMap<MouseButton, Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
37    on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
38    suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
39    children: SmallVec<[AnyElement; 2]>,
40}
41
42impl ListItem {
43    pub fn new(id: impl Into<ElementId>) -> Self {
44        let id: ElementId = id.into();
45        Self {
46            mode: ListItemMode::Entry,
47            base: h_flex().id(id),
48            style: StyleRefinement::default(),
49            disabled: false,
50            selected: false,
51            secondary_selected: false,
52            confirmed: false,
53            on_click: None,
54            on_mouse_down: HashMap::new(),
55            on_mouse_enter: None,
56            check_icon: None,
57            suffix: None,
58            children: SmallVec::new(),
59        }
60    }
61
62    /// Set this list item to as a separator, it not able to be selected.
63    pub fn separator(mut self) -> Self {
64        self.mode = ListItemMode::Separator;
65        self
66    }
67
68    /// Set to show check icon, default is None.
69    pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
70        self.check_icon = Some(icon.into());
71        self
72    }
73
74    /// Set ListItem as the selected item style.
75    pub fn selected(mut self, selected: bool) -> Self {
76        self.selected = selected;
77        self
78    }
79
80    /// Set ListItem as the confirmed item style, it will show a check icon.
81    pub fn confirmed(mut self, confirmed: bool) -> Self {
82        self.confirmed = confirmed;
83        self
84    }
85
86    pub fn disabled(mut self, disabled: bool) -> Self {
87        self.disabled = disabled;
88        self
89    }
90
91    /// Set the suffix element of the input field, for example a clear button.
92    pub fn suffix<F, E>(mut self, builder: F) -> Self
93    where
94        F: Fn(&mut Window, &mut App) -> E + 'static,
95        E: IntoElement,
96    {
97        self.suffix = Some(Box::new(move |window, cx| {
98            builder(window, cx).into_any_element()
99        }));
100        self
101    }
102
103    pub fn on_click(
104        mut self,
105        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
106    ) -> Self {
107        self.on_click = Some(Box::new(handler));
108        self
109    }
110
111    pub fn on_mouse_down(
112        mut self,
113        button: MouseButton,
114        handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
115    ) -> Self {
116        self.on_mouse_down.insert(button, Box::new(handler));
117        self
118    }
119
120    pub fn on_mouse_enter(
121        mut self,
122        handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
123    ) -> Self {
124        self.on_mouse_enter = Some(Box::new(handler));
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    fn is_selected(&self) -> bool {
143        self.selected
144    }
145
146    fn secondary_selected(mut self, selected: bool) -> Self {
147        self.secondary_selected = selected;
148        self
149    }
150}
151
152impl Styled for ListItem {
153    fn style(&mut self) -> &mut gpui::StyleRefinement {
154        &mut self.style
155    }
156}
157
158impl ParentElement for ListItem {
159    fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
160        self.children.extend(elements);
161    }
162}
163
164/// Note: Listeners registered via these traits are not gated by
165/// `disabled`/`separator`. The hover style is managed internally, use
166/// `on_hover` instead of `.hover()`.
167impl InteractiveElement for ListItem {
168    fn interactivity(&mut self) -> &mut Interactivity {
169        self.base.interactivity()
170    }
171}
172
173impl StatefulInteractiveElement for ListItem {}
174
175impl RenderOnce for ListItem {
176    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
177        let is_active = self.confirmed || self.selected || self.secondary_selected;
178
179        let corner_radii = self.style.corner_radii.clone();
180
181        let mut selected_style = StyleRefinement::default();
182        selected_style.corner_radii = corner_radii;
183
184        let is_selectable = !(self.disabled || self.mode.is_separator());
185
186        self.base
187            .relative()
188            .gap_x_1()
189            .py_1()
190            .px_3()
191            .text_base()
192            .text_color(cx.theme().foreground)
193            .relative()
194            .items_center()
195            .justify_between()
196            .refine_style(&self.style)
197            .when(is_selectable, |this| {
198                this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
199                    .when_some(self.on_mouse_enter, |this, on_mouse_enter| {
200                        this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
201                    })
202                    .map(|this| {
203                        self.on_mouse_down
204                            .into_iter()
205                            .fold(this, |this, (button, handler)| {
206                                this.on_mouse_down(button, move |ev, window, cx| {
207                                    handler(ev, window, cx)
208                                })
209                            })
210                    })
211                    .when(!is_active, |this| {
212                        this.hover(|this| this.bg(cx.theme().tokens.list_hover))
213                    })
214            })
215            .when(!is_selectable, |this| {
216                this.text_color(cx.theme().muted_foreground)
217            })
218            .child(
219                h_flex()
220                    .w_full()
221                    .items_center()
222                    .justify_between()
223                    .gap_x_1()
224                    .child(div().w_full().children(self.children))
225                    .when_some(self.check_icon, |this, icon| {
226                        this.child(
227                            div().w_5().items_center().justify_center().when(
228                                self.confirmed,
229                                |this| {
230                                    this.child(icon.small().text_color(cx.theme().muted_foreground))
231                                },
232                            ),
233                        )
234                    }),
235            )
236            .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
237            .map(|this| {
238                if is_selectable && (self.selected || self.secondary_selected) {
239                    let bg = if self.selected && cx.theme().list.active_highlight {
240                        cx.theme().list_active
241                    } else {
242                        cx.theme().accent
243                    };
244
245                    this.when(!self.secondary_selected, |this| this.bg(bg))
246                        .when(cx.theme().list.active_highlight, |this| {
247                            this.child(
248                                div()
249                                    .absolute()
250                                    .top_0()
251                                    .left_0()
252                                    .right_0()
253                                    .bottom_0()
254                                    .border_1()
255                                    .border_color(cx.theme().list_active_border)
256                                    .refine_style(&selected_style),
257                            )
258                        })
259                } else {
260                    this
261                }
262            })
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use gpui::{AppContext as _, Context, Render};
270
271    struct DragPreview;
272
273    impl Render for DragPreview {
274        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
275            div()
276        }
277    }
278
279    #[gpui::test]
280    fn test_list_item_interactivity(_cx: &mut gpui::TestAppContext) {
281        let mut item = ListItem::new("item")
282            .on_drag(DragPreview, |_, _, _, cx| cx.new(|_| DragPreview))
283            .drag_over::<DragPreview>(|style, _, _, _| style)
284            .on_drop(|_: &DragPreview, _, _| {})
285            .on_hover(|_, _, _| {});
286
287        assert_eq!(item.interactivity().element_id, Some("item".into()));
288    }
289}