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 pub fn separator(mut self) -> Self {
64 self.mode = ListItemMode::Separator;
65 self
66 }
67
68 pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
70 self.check_icon = Some(icon.into());
71 self
72 }
73
74 pub fn selected(mut self, selected: bool) -> Self {
76 self.selected = selected;
77 self
78 }
79
80 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 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
164impl 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}