1use std::rc::Rc;
2
3use gpui::{
4 div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
5 Pixels, Stateful, StatefulInteractiveElement,
6};
7use smallvec::SmallVec;
8
9use crate::{
10 disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
11 Toggle,
12};
13use crate::{prelude::*, GraphicSlot};
14
15pub enum ListHeaderMeta {
16 Tools(Vec<IconButton>),
17 // TODO: This should be a button
18 Button(Label),
19 Text(Label),
20}
21
22#[derive(IntoElement)]
23pub struct ListHeader {
24 label: SharedString,
25 left_icon: Option<Icon>,
26 meta: Option<ListHeaderMeta>,
27 toggle: Toggle,
28 inset: bool,
29}
30
31impl ListHeader {
32 pub fn new(label: impl Into<SharedString>) -> Self {
33 Self {
34 label: label.into(),
35 left_icon: None,
36 meta: None,
37 inset: false,
38 toggle: Toggle::NotToggleable,
39 }
40 }
41
42 pub fn toggle(mut self, toggle: Toggle) -> Self {
43 self.toggle = toggle;
44 self
45 }
46
47 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
48 self.left_icon = left_icon;
49 self
50 }
51
52 pub fn right_button(self, button: IconButton) -> Self {
53 self.meta(Some(ListHeaderMeta::Tools(vec![button])))
54 }
55
56 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
57 self.meta = meta;
58 self
59 }
60}
61
62impl RenderOnce for ListHeader {
63 type Rendered = Div;
64
65 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
66 let disclosure_control = disclosure_control(self.toggle, None);
67
68 let meta = match self.meta {
69 Some(ListHeaderMeta::Tools(icons)) => div().child(
70 h_stack()
71 .gap_2()
72 .items_center()
73 .children(icons.into_iter().map(|i| i.color(Color::Muted))),
74 ),
75 Some(ListHeaderMeta::Button(label)) => div().child(label),
76 Some(ListHeaderMeta::Text(label)) => div().child(label),
77 None => div(),
78 };
79
80 h_stack()
81 .w_full()
82 .bg(cx.theme().colors().surface_background)
83 .relative()
84 .child(
85 div()
86 .h_5()
87 .when(self.inset, |this| this.px_2())
88 .flex()
89 .flex_1()
90 .items_center()
91 .justify_between()
92 .w_full()
93 .gap_1()
94 .child(
95 h_stack()
96 .gap_1()
97 .child(
98 div()
99 .flex()
100 .gap_1()
101 .items_center()
102 .children(self.left_icon.map(|i| {
103 IconElement::new(i)
104 .color(Color::Muted)
105 .size(IconSize::Small)
106 }))
107 .child(Label::new(self.label.clone()).color(Color::Muted)),
108 )
109 .child(disclosure_control),
110 )
111 .child(meta),
112 )
113 }
114}
115
116#[derive(IntoElement, Clone)]
117pub struct ListSubHeader {
118 label: SharedString,
119 left_icon: Option<Icon>,
120 inset: bool,
121}
122
123impl ListSubHeader {
124 pub fn new(label: impl Into<SharedString>) -> Self {
125 Self {
126 label: label.into(),
127 left_icon: None,
128 inset: false,
129 }
130 }
131
132 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
133 self.left_icon = left_icon;
134 self
135 }
136}
137
138impl RenderOnce for ListSubHeader {
139 type Rendered = Div;
140
141 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
142 h_stack().flex_1().w_full().relative().py_1().child(
143 div()
144 .h_6()
145 .when(self.inset, |this| this.px_2())
146 .flex()
147 .flex_1()
148 .w_full()
149 .gap_1()
150 .items_center()
151 .justify_between()
152 .child(
153 div()
154 .flex()
155 .gap_1()
156 .items_center()
157 .children(self.left_icon.map(|i| {
158 IconElement::new(i)
159 .color(Color::Muted)
160 .size(IconSize::Small)
161 }))
162 .child(Label::new(self.label.clone()).color(Color::Muted)),
163 ),
164 )
165 }
166}
167
168#[derive(IntoElement)]
169pub struct ListItem {
170 id: ElementId,
171 selected: bool,
172 // TODO: Reintroduce this
173 // disclosure_control_style: DisclosureControlVisibility,
174 indent_level: usize,
175 indent_step_size: Pixels,
176 left_slot: Option<GraphicSlot>,
177 toggle: Toggle,
178 inset: bool,
179 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
180 on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
181 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
182 children: SmallVec<[AnyElement; 2]>,
183}
184
185impl ListItem {
186 pub fn new(id: impl Into<ElementId>) -> Self {
187 Self {
188 id: id.into(),
189 selected: false,
190 indent_level: 0,
191 indent_step_size: px(12.),
192 left_slot: None,
193 toggle: Toggle::NotToggleable,
194 inset: false,
195 on_click: None,
196 on_secondary_mouse_down: None,
197 on_toggle: None,
198 children: SmallVec::new(),
199 }
200 }
201
202 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
203 self.on_click = Some(Rc::new(handler));
204 self
205 }
206
207 pub fn on_secondary_mouse_down(
208 mut self,
209 handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
210 ) -> Self {
211 self.on_secondary_mouse_down = Some(Rc::new(handler));
212 self
213 }
214
215 pub fn inset(mut self, inset: bool) -> Self {
216 self.inset = inset;
217 self
218 }
219
220 pub fn indent_level(mut self, indent_level: usize) -> Self {
221 self.indent_level = indent_level;
222 self
223 }
224
225 pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
226 self.indent_step_size = indent_step_size;
227 self
228 }
229
230 pub fn toggle(mut self, toggle: Toggle) -> Self {
231 self.toggle = toggle;
232 self
233 }
234
235 pub fn on_toggle(
236 mut self,
237 on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
238 ) -> Self {
239 self.on_toggle = Some(Rc::new(on_toggle));
240 self
241 }
242
243 pub fn selected(mut self, selected: bool) -> Self {
244 self.selected = selected;
245 self
246 }
247
248 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
249 self.left_slot = Some(left_content);
250 self
251 }
252
253 pub fn left_icon(mut self, left_icon: Icon) -> Self {
254 self.left_slot = Some(GraphicSlot::Icon(left_icon));
255 self
256 }
257
258 pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
259 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
260 self
261 }
262}
263
264impl RenderOnce for ListItem {
265 type Rendered = Stateful<Div>;
266
267 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
268 let left_content = match self.left_slot.clone() {
269 Some(GraphicSlot::Icon(i)) => Some(
270 h_stack().child(
271 IconElement::new(i)
272 .size(IconSize::Small)
273 .color(Color::Muted),
274 ),
275 ),
276 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
277 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
278 None => None,
279 };
280
281 div()
282 .id(self.id)
283 .relative()
284 // TODO: Add focus state
285 // .when(self.state == InteractionState::Focused, |this| {
286 // this.border()
287 // .border_color(cx.theme().colors().border_focused)
288 // })
289 .when(self.inset, |this| this.rounded_md())
290 .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
291 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
292 .when(self.selected, |this| {
293 this.bg(cx.theme().colors().ghost_element_selected)
294 })
295 .when_some(self.on_click.clone(), |this, on_click| {
296 this.cursor_pointer().on_click(move |event, cx| {
297 // HACK: GPUI currently fires `on_click` with any mouse button,
298 // but we only care about the left button.
299 if event.down.button == MouseButton::Left {
300 (on_click)(event, cx)
301 }
302 })
303 })
304 .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
305 this.on_mouse_down(MouseButton::Right, move |event, cx| {
306 (on_mouse_down)(event, cx)
307 })
308 })
309 .child(
310 div()
311 .when(self.inset, |this| this.px_2())
312 .ml(self.indent_level as f32 * self.indent_step_size)
313 .flex()
314 .gap_1()
315 .items_center()
316 .relative()
317 .child(disclosure_control(self.toggle, self.on_toggle))
318 .children(left_content)
319 .children(self.children),
320 )
321 }
322}
323
324impl ParentElement for ListItem {
325 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
326 &mut self.children
327 }
328}
329
330#[derive(IntoElement, Clone)]
331pub struct ListSeparator;
332
333impl ListSeparator {
334 pub fn new() -> Self {
335 Self
336 }
337}
338
339impl RenderOnce for ListSeparator {
340 type Rendered = Div;
341
342 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
343 div().h_px().w_full().bg(cx.theme().colors().border_variant)
344 }
345}
346
347#[derive(IntoElement)]
348pub struct List {
349 /// Message to display when the list is empty
350 /// Defaults to "No items"
351 empty_message: SharedString,
352 header: Option<ListHeader>,
353 toggle: Toggle,
354 children: SmallVec<[AnyElement; 2]>,
355}
356
357impl RenderOnce for List {
358 type Rendered = Div;
359
360 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
361 let list_content = match (self.children.is_empty(), self.toggle) {
362 (false, _) => div().children(self.children),
363 (true, Toggle::Toggled(false)) => div(),
364 (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
365 };
366
367 v_stack()
368 .w_full()
369 .py_1()
370 .children(self.header.map(|header| header))
371 .child(list_content)
372 }
373}
374
375impl List {
376 pub fn new() -> Self {
377 Self {
378 empty_message: "No items".into(),
379 header: None,
380 toggle: Toggle::NotToggleable,
381 children: SmallVec::new(),
382 }
383 }
384
385 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
386 self.empty_message = empty_message.into();
387 self
388 }
389
390 pub fn header(mut self, header: ListHeader) -> Self {
391 self.header = Some(header);
392 self
393 }
394
395 pub fn toggle(mut self, toggle: Toggle) -> Self {
396 self.toggle = toggle;
397 self
398 }
399}
400
401impl ParentElement for List {
402 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
403 &mut self.children
404 }
405}