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 is_selectable = !(self.disabled || self.mode.is_separator());
181
182 self.base
183 .test_support()
184 .relative()
185 .gap_x_1()
186 .py_1()
187 .px_3()
188 .text_base()
189 .text_color(cx.theme().foreground)
190 .relative()
191 .items_center()
192 .justify_between()
193 .refine_style(&self.style)
194 .when(is_selectable, |this| {
195 this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
196 .when_some(self.on_mouse_enter, |this, on_mouse_enter| {
197 this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
198 })
199 .map(|this| {
200 self.on_mouse_down
201 .into_iter()
202 .fold(this, |this, (button, handler)| {
203 this.on_mouse_down(button, move |ev, window, cx| {
204 handler(ev, window, cx)
205 })
206 })
207 })
208 .when(!is_active, |this| {
209 this.hover(|this| this.bg(cx.theme().tokens.list_hover))
210 })
211 })
212 .when(!is_selectable, |this| {
213 this.text_color(cx.theme().muted_foreground)
214 })
215 .child(
216 h_flex()
217 .w_full()
218 .items_center()
219 .justify_between()
220 .gap_x_1()
221 .child(div().w_full().children(self.children))
222 .when_some(self.check_icon, |this, icon| {
223 this.child(
224 div().w_5().items_center().justify_center().when(
225 self.confirmed,
226 |this| {
227 this.child(icon.small().text_color(cx.theme().muted_foreground))
228 },
229 ),
230 )
231 }),
232 )
233 .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
234 .map(|this| {
235 if is_selectable && (self.selected || self.secondary_selected) {
236 let bg = if self.selected && cx.theme().list.active_highlight {
237 cx.theme().list_active
238 } else {
239 cx.theme().accent
240 };
241
242 this.when(!self.secondary_selected, |this| this.bg(bg))
243 } else {
244 this
245 }
246 })
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use gpui::{AppContext as _, Context, Render};
254
255 struct DragPreview;
256
257 impl Render for DragPreview {
258 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
259 div()
260 }
261 }
262
263 #[gpui::test]
264 fn test_list_item_interactivity(_cx: &mut gpui::TestAppContext) {
265 let mut item = ListItem::new("item")
266 .on_drag(DragPreview, |_, _, _, cx| cx.new(|_| DragPreview))
267 .drag_over::<DragPreview>(|style, _, _, _| style)
268 .on_drop(|_: &DragPreview, _, _| {})
269 .on_hover(|_, _, _| {});
270
271 assert_eq!(item.interactivity().element_id, Some("item".into()));
272 }
273}