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