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