gpui_component/list/
list_item.rs

1use crate::{h_flex, ActiveTheme, Disableable, Icon, Selectable, Sizable as _, StyledExt};
2use gpui::{
3    div, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, Div, ElementId,
4    InteractiveElement, IntoElement, MouseMoveEvent, ParentElement, RenderOnce, Stateful,
5    StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
6};
7use smallvec::SmallVec;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10enum ListItemMode {
11    #[default]
12    Entry,
13    Separator,
14}
15
16impl ListItemMode {
17    #[inline]
18    fn is_separator(&self) -> bool {
19        matches!(self, ListItemMode::Separator)
20    }
21}
22
23#[derive(IntoElement)]
24pub struct ListItem {
25    base: Stateful<Div>,
26    mode: ListItemMode,
27    style: StyleRefinement,
28    disabled: bool,
29    selected: bool,
30    secondary_selected: bool,
31    confirmed: bool,
32    check_icon: Option<Icon>,
33    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
34    on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
35    suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
36    children: SmallVec<[AnyElement; 2]>,
37}
38
39impl ListItem {
40    pub fn new(id: impl Into<ElementId>) -> Self {
41        let id: ElementId = id.into();
42        Self {
43            mode: ListItemMode::Entry,
44            base: h_flex().id(id),
45            style: StyleRefinement::default(),
46            disabled: false,
47            selected: false,
48            secondary_selected: false,
49            confirmed: false,
50            on_click: None,
51            on_mouse_enter: None,
52            check_icon: None,
53            suffix: None,
54            children: SmallVec::new(),
55        }
56    }
57
58    /// Set this list item to as a separator, it not able to be selected.
59    pub fn separator(mut self) -> Self {
60        self.mode = ListItemMode::Separator;
61        self
62    }
63
64    /// Set to show check icon, default is None.
65    pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
66        self.check_icon = Some(icon.into());
67        self
68    }
69
70    /// Set ListItem as the selected item style.
71    pub fn selected(mut self, selected: bool) -> Self {
72        self.selected = selected;
73        self
74    }
75
76    /// Set ListItem as the confirmed item style, it will show a check icon.
77    pub fn confirmed(mut self, confirmed: bool) -> Self {
78        self.confirmed = confirmed;
79        self
80    }
81
82    pub fn disabled(mut self, disabled: bool) -> Self {
83        self.disabled = disabled;
84        self
85    }
86
87    /// Set the suffix element of the input field, for example a clear button.
88    pub fn suffix<F, E>(mut self, builder: F) -> Self
89    where
90        F: Fn(&mut Window, &mut App) -> E + 'static,
91        E: IntoElement,
92    {
93        self.suffix = Some(Box::new(move |window, cx| {
94            builder(window, cx).into_any_element()
95        }));
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_mouse_enter(
108        mut self,
109        handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static,
110    ) -> Self {
111        self.on_mouse_enter = Some(Box::new(handler));
112        self
113    }
114}
115
116impl Disableable for ListItem {
117    fn disabled(mut self, disabled: bool) -> Self {
118        self.disabled = disabled;
119        self
120    }
121}
122
123impl Selectable for ListItem {
124    fn selected(mut self, selected: bool) -> Self {
125        self.selected = selected;
126        self
127    }
128
129    fn is_selected(&self) -> bool {
130        self.selected
131    }
132
133    fn secondary_selected(mut self, selected: bool) -> Self {
134        self.secondary_selected = selected;
135        self
136    }
137}
138
139impl Styled for ListItem {
140    fn style(&mut self) -> &mut gpui::StyleRefinement {
141        &mut self.style
142    }
143}
144
145impl ParentElement for ListItem {
146    fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
147        self.children.extend(elements);
148    }
149}
150
151impl RenderOnce for ListItem {
152    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
153        let is_active = self.confirmed || self.selected;
154
155        let corner_radii = self.style.corner_radii.clone();
156
157        let mut selected_style = StyleRefinement::default();
158        selected_style.corner_radii = corner_radii;
159
160        let is_selectable = !(self.disabled || self.mode.is_separator());
161
162        self.base
163            .relative()
164            .gap_x_1()
165            .py_1()
166            .px_3()
167            .text_base()
168            .text_color(cx.theme().foreground)
169            .relative()
170            .items_center()
171            .justify_between()
172            .refine_style(&self.style)
173            .when(is_selectable, |this| {
174                this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
175                    .when_some(self.on_mouse_enter, |this, on_mouse_enter| {
176                        this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
177                    })
178                    .when(!is_active, |this| {
179                        this.hover(|this| this.bg(cx.theme().list_hover))
180                    })
181            })
182            .when(!is_selectable, |this| {
183                this.text_color(cx.theme().muted_foreground)
184            })
185            .child(
186                h_flex()
187                    .w_full()
188                    .items_center()
189                    .justify_between()
190                    .gap_x_1()
191                    .child(div().w_full().children(self.children))
192                    .when_some(self.check_icon, |this, icon| {
193                        this.child(
194                            div().w_5().items_center().justify_center().when(
195                                self.confirmed,
196                                |this| {
197                                    this.child(icon.small().text_color(cx.theme().muted_foreground))
198                                },
199                            ),
200                        )
201                    }),
202            )
203            .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
204            .map(|this| {
205                if is_selectable && (self.selected || self.secondary_selected) {
206                    this.bg(cx.theme().accent).child(
207                        div()
208                            .absolute()
209                            .top_0()
210                            .left_0()
211                            .right_0()
212                            .bottom_0()
213                            .when(!self.secondary_selected, |this| {
214                                this.bg(cx.theme().list_active)
215                            })
216                            .border_1()
217                            .border_color(cx.theme().list_active_border)
218                            .refine_style(&selected_style),
219                    )
220                } else {
221                    this
222                }
223            })
224    }
225}