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 pub fn separator(mut self) -> Self {
60 self.mode = ListItemMode::Separator;
61 self
62 }
63
64 pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
66 self.check_icon = Some(icon.into());
67 self
68 }
69
70 pub fn selected(mut self, selected: bool) -> Self {
72 self.selected = selected;
73 self
74 }
75
76 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 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}