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 let mut outline_style = StyleRefinement::default();
184 outline_style.corner_radii = self.style.corner_radii.clone();
185
186 self.base
187 .test_support()
188 .relative()
189 .gap_x_1()
190 .py_1()
191 .px_3()
192 .text_base()
193 .text_color(cx.theme().foreground)
194 .relative()
195 .items_center()
196 .justify_between()
197 .refine_style(&self.style)
198 .when(is_selectable, |this| {
199 this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
200 .when_some(self.on_mouse_enter, |this, on_mouse_enter| {
201 this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
202 })
203 .map(|this| {
204 self.on_mouse_down
205 .into_iter()
206 .fold(this, |this, (button, handler)| {
207 this.on_mouse_down(button, move |ev, window, cx| {
208 handler(ev, window, cx)
209 })
210 })
211 })
212 .hover(|this| {
215 if is_active {
216 this
217 } else {
218 this.bg(cx.theme().tokens.list_hover)
219 }
220 })
221 })
222 .when(!is_selectable, |this| {
223 this.text_color(cx.theme().muted_foreground)
224 })
225 .child(
226 h_flex()
227 .w_full()
228 .items_center()
229 .justify_between()
230 .gap_x_1()
231 .child(div().w_full().children(self.children))
232 .when_some(self.check_icon, |this, icon| {
233 this.child(
234 div().w_5().items_center().justify_center().when(
235 self.confirmed,
236 |this| {
237 this.child(icon.small().text_color(cx.theme().muted_foreground))
238 },
239 ),
240 )
241 }),
242 )
243 .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
244 .when(is_selectable && self.selected, |this| {
245 let bg = if cx.theme().list.active_highlight {
246 cx.theme().list_active
247 } else {
248 cx.theme().accent
249 };
250
251 this.bg(bg)
252 })
253 .when(is_selectable && self.secondary_selected, |this| {
254 this.child(
255 div()
256 .absolute()
257 .inset_0()
258 .border_1()
259 .border_color(cx.theme().selection)
260 .refine_style(&outline_style),
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}