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