1use gpui::{
2 div, px, AnyElement, ClickEvent, Div, RenderOnce, Stateful, StatefulInteractiveElement,
3};
4use smallvec::SmallVec;
5use std::rc::Rc;
6
7use crate::{
8 disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
9};
10use crate::{prelude::*, GraphicSlot};
11
12#[derive(Clone, Copy, Default, Debug, PartialEq)]
13pub enum ListItemVariant {
14 /// The list item extends to the far left and right of the list.
15 FullWidth,
16 #[default]
17 Inset,
18}
19
20pub enum ListHeaderMeta {
21 // TODO: These should be IconButtons
22 Tools(Vec<Icon>),
23 // TODO: This should be a button
24 Button(Label),
25 Text(Label),
26}
27
28#[derive(RenderOnce)]
29pub struct ListHeader {
30 label: SharedString,
31 left_icon: Option<Icon>,
32 meta: Option<ListHeaderMeta>,
33 variant: ListItemVariant,
34 toggle: Toggle,
35}
36
37impl Component for ListHeader {
38 type Rendered = Div;
39
40 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
41 let disclosure_control = disclosure_control(self.toggle);
42
43 let meta = match self.meta {
44 Some(ListHeaderMeta::Tools(icons)) => div().child(
45 h_stack()
46 .gap_2()
47 .items_center()
48 .children(icons.into_iter().map(|i| {
49 IconElement::new(i)
50 .color(Color::Muted)
51 .size(IconSize::Small)
52 })),
53 ),
54 Some(ListHeaderMeta::Button(label)) => div().child(label),
55 Some(ListHeaderMeta::Text(label)) => div().child(label),
56 None => div(),
57 };
58
59 h_stack()
60 .w_full()
61 .bg(cx.theme().colors().surface_background)
62 .relative()
63 .child(
64 div()
65 .h_5()
66 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
67 .flex()
68 .flex_1()
69 .items_center()
70 .justify_between()
71 .w_full()
72 .gap_1()
73 .child(
74 h_stack()
75 .gap_1()
76 .child(
77 div()
78 .flex()
79 .gap_1()
80 .items_center()
81 .children(self.left_icon.map(|i| {
82 IconElement::new(i)
83 .color(Color::Muted)
84 .size(IconSize::Small)
85 }))
86 .child(Label::new(self.label.clone()).color(Color::Muted)),
87 )
88 .child(disclosure_control),
89 )
90 .child(meta),
91 )
92 }
93}
94
95impl ListHeader {
96 pub fn new(label: impl Into<SharedString>) -> Self {
97 Self {
98 label: label.into(),
99 left_icon: None,
100 meta: None,
101 variant: ListItemVariant::default(),
102 toggle: Toggle::NotToggleable,
103 }
104 }
105
106 pub fn toggle(mut self, toggle: Toggle) -> Self {
107 self.toggle = toggle;
108 self
109 }
110
111 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
112 self.left_icon = left_icon;
113 self
114 }
115
116 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
117 self.meta = meta;
118 self
119 }
120
121 // before_ship!("delete")
122 // fn render<V: 'static>(self, cx: &mut WindowContext) -> impl Element<V> {
123 // let disclosure_control = disclosure_control(self.toggle);
124
125 // let meta = match self.meta {
126 // Some(ListHeaderMeta::Tools(icons)) => div().child(
127 // h_stack()
128 // .gap_2()
129 // .items_center()
130 // .children(icons.into_iter().map(|i| {
131 // IconElement::new(i)
132 // .color(TextColor::Muted)
133 // .size(IconSize::Small)
134 // })),
135 // ),
136 // Some(ListHeaderMeta::Button(label)) => div().child(label),
137 // Some(ListHeaderMeta::Text(label)) => div().child(label),
138 // None => div(),
139 // };
140
141 // h_stack()
142 // .w_full()
143 // .bg(cx.theme().colors().surface_background)
144 // // TODO: Add focus state
145 // // .when(self.state == InteractionState::Focused, |this| {
146 // // this.border()
147 // // .border_color(cx.theme().colors().border_focused)
148 // // })
149 // .relative()
150 // .child(
151 // div()
152 // .h_5()
153 // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
154 // .flex()
155 // .flex_1()
156 // .items_center()
157 // .justify_between()
158 // .w_full()
159 // .gap_1()
160 // .child(
161 // h_stack()
162 // .gap_1()
163 // .child(
164 // div()
165 // .flex()
166 // .gap_1()
167 // .items_center()
168 // .children(self.left_icon.map(|i| {
169 // IconElement::new(i)
170 // .color(TextColor::Muted)
171 // .size(IconSize::Small)
172 // }))
173 // .child(Label::new(self.label.clone()).color(TextColor::Muted)),
174 // )
175 // .child(disclosure_control),
176 // )
177 // .child(meta),
178 // )
179 // }
180}
181
182#[derive(RenderOnce, Clone)]
183pub struct ListSubHeader {
184 label: SharedString,
185 left_icon: Option<Icon>,
186 variant: ListItemVariant,
187}
188
189impl ListSubHeader {
190 pub fn new(label: impl Into<SharedString>) -> Self {
191 Self {
192 label: label.into(),
193 left_icon: None,
194 variant: ListItemVariant::default(),
195 }
196 }
197
198 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
199 self.left_icon = left_icon;
200 self
201 }
202}
203
204impl Component for ListSubHeader {
205 type Rendered = Div;
206
207 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
208 h_stack().flex_1().w_full().relative().py_1().child(
209 div()
210 .h_6()
211 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
212 .flex()
213 .flex_1()
214 .w_full()
215 .gap_1()
216 .items_center()
217 .justify_between()
218 .child(
219 div()
220 .flex()
221 .gap_1()
222 .items_center()
223 .children(self.left_icon.map(|i| {
224 IconElement::new(i)
225 .color(Color::Muted)
226 .size(IconSize::Small)
227 }))
228 .child(Label::new(self.label.clone()).color(Color::Muted)),
229 ),
230 )
231 }
232}
233
234#[derive(Default, PartialEq, Copy, Clone)]
235pub enum ListEntrySize {
236 #[default]
237 Small,
238 Medium,
239}
240
241#[derive(RenderOnce)]
242pub struct ListItem {
243 id: ElementId,
244 disabled: bool,
245 // TODO: Reintroduce this
246 // disclosure_control_style: DisclosureControlVisibility,
247 indent_level: u32,
248 label: Label,
249 left_slot: Option<GraphicSlot>,
250 overflow: OverflowStyle,
251 size: ListEntrySize,
252 toggle: Toggle,
253 variant: ListItemVariant,
254 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
255}
256
257impl Clone for ListItem {
258 fn clone(&self) -> Self {
259 Self {
260 id: self.id.clone(),
261 disabled: self.disabled,
262 indent_level: self.indent_level,
263 label: self.label.clone(),
264 left_slot: self.left_slot.clone(),
265 overflow: self.overflow,
266 size: self.size,
267 toggle: self.toggle,
268 variant: self.variant,
269 on_click: self.on_click.clone(),
270 }
271 }
272}
273
274impl ListItem {
275 pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
276 Self {
277 id: id.into(),
278 disabled: false,
279 indent_level: 0,
280 label,
281 left_slot: None,
282 overflow: OverflowStyle::Hidden,
283 size: ListEntrySize::default(),
284 toggle: Toggle::NotToggleable,
285 variant: ListItemVariant::default(),
286 on_click: Default::default(),
287 }
288 }
289
290 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
291 self.on_click = Some(Rc::new(handler));
292 self
293 }
294
295 pub fn variant(mut self, variant: ListItemVariant) -> Self {
296 self.variant = variant;
297 self
298 }
299
300 pub fn indent_level(mut self, indent_level: u32) -> Self {
301 self.indent_level = indent_level;
302 self
303 }
304
305 pub fn toggle(mut self, toggle: Toggle) -> Self {
306 self.toggle = toggle;
307 self
308 }
309
310 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
311 self.left_slot = Some(left_content);
312 self
313 }
314
315 pub fn left_icon(mut self, left_icon: Icon) -> Self {
316 self.left_slot = Some(GraphicSlot::Icon(left_icon));
317 self
318 }
319
320 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
321 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
322 self
323 }
324
325 pub fn size(mut self, size: ListEntrySize) -> Self {
326 self.size = size;
327 self
328 }
329}
330
331impl Component for ListItem {
332 type Rendered = Stateful<Div>;
333
334 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
335 let left_content = match self.left_slot.clone() {
336 Some(GraphicSlot::Icon(i)) => Some(
337 h_stack().child(
338 IconElement::new(i)
339 .size(IconSize::Small)
340 .color(Color::Muted),
341 ),
342 ),
343 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
344 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
345 None => None,
346 };
347
348 let sized_item = match self.size {
349 ListEntrySize::Small => div().h_6(),
350 ListEntrySize::Medium => div().h_7(),
351 };
352 div()
353 .id(self.id)
354 .relative()
355 .hover(|mut style| {
356 style.background = Some(cx.theme().colors().editor_background.into());
357 style
358 })
359 .on_click({
360 let on_click = self.on_click.clone();
361 move |event, cx| {
362 if let Some(on_click) = &on_click {
363 (on_click)(event, cx)
364 }
365 }
366 })
367 .bg(cx.theme().colors().surface_background)
368 // TODO: Add focus state
369 // .when(self.state == InteractionState::Focused, |this| {
370 // this.border()
371 // .border_color(cx.theme().colors().border_focused)
372 // })
373 .child(
374 sized_item
375 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
376 // .ml(rems(0.75 * self.indent_level as f32))
377 .children((0..self.indent_level).map(|_| {
378 div()
379 .w(px(4.))
380 .h_full()
381 .flex()
382 .justify_center()
383 .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
384 .child(
385 h_stack()
386 .child(div().w_px().h_full())
387 .child(div().w_px().h_full().bg(cx.theme().colors().border)),
388 )
389 }))
390 .flex()
391 .gap_1()
392 .items_center()
393 .relative()
394 .child(disclosure_control(self.toggle))
395 .children(left_content)
396 .child(self.label),
397 )
398 }
399}
400
401#[derive(RenderOnce, Clone)]
402pub struct ListSeparator;
403
404impl ListSeparator {
405 pub fn new() -> Self {
406 Self
407 }
408}
409
410impl Component for ListSeparator {
411 type Rendered = Div;
412
413 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
414 div().h_px().w_full().bg(cx.theme().colors().border_variant)
415 }
416}
417
418#[derive(RenderOnce)]
419pub struct List {
420 /// Message to display when the list is empty
421 /// Defaults to "No items"
422 empty_message: SharedString,
423 header: Option<ListHeader>,
424 toggle: Toggle,
425 children: SmallVec<[AnyElement; 2]>,
426}
427
428impl Component for List {
429 type Rendered = Div;
430
431 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
432 let list_content = match (self.children.is_empty(), self.toggle) {
433 (false, _) => div().children(self.children),
434 (true, Toggle::Toggled(false)) => div(),
435 (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
436 };
437
438 v_stack()
439 .w_full()
440 .py_1()
441 .children(self.header.map(|header| header))
442 .child(list_content)
443 }
444}
445
446impl List {
447 pub fn new() -> Self {
448 Self {
449 empty_message: "No items".into(),
450 header: None,
451 toggle: Toggle::NotToggleable,
452 children: SmallVec::new(),
453 }
454 }
455
456 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
457 self.empty_message = empty_message.into();
458 self
459 }
460
461 pub fn header(mut self, header: ListHeader) -> Self {
462 self.header = Some(header);
463 self
464 }
465
466 pub fn toggle(mut self, toggle: Toggle) -> Self {
467 self.toggle = toggle;
468 self
469 }
470}
471
472impl ParentElement for List {
473 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
474 &mut self.children
475 }
476}