1use gpui2::{div, px, relative, Div};
2
3use crate::settings::user_settings;
4use crate::{
5 h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label,
6 LabelColor,
7};
8use crate::{prelude::*, Button};
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(Component)]
27pub struct ListHeader {
28 label: SharedString,
29 left_icon: Option<Icon>,
30 meta: Option<ListHeaderMeta>,
31 variant: ListItemVariant,
32 toggleable: Toggleable,
33}
34
35impl ListHeader {
36 pub fn new(label: impl Into<SharedString>) -> Self {
37 Self {
38 label: label.into(),
39 left_icon: None,
40 meta: None,
41 variant: ListItemVariant::default(),
42 toggleable: Toggleable::NotToggleable,
43 }
44 }
45
46 pub fn toggle(mut self, toggle: ToggleState) -> Self {
47 self.toggleable = toggle.into();
48 self
49 }
50
51 pub fn toggleable(mut self, toggleable: Toggleable) -> Self {
52 self.toggleable = toggleable;
53 self
54 }
55
56 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
57 self.left_icon = left_icon;
58 self
59 }
60
61 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
62 self.meta = meta;
63 self
64 }
65
66 fn disclosure_control<V: 'static>(&self) -> Div<V> {
67 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
68 let is_toggled = Toggleable::is_toggled(&self.toggleable);
69
70 match (is_toggleable, is_toggled) {
71 (false, _) => div(),
72 (_, true) => div().child(
73 IconElement::new(Icon::ChevronDown)
74 .color(IconColor::Muted)
75 .size(IconSize::Small),
76 ),
77 (_, false) => div().child(
78 IconElement::new(Icon::ChevronRight)
79 .color(IconColor::Muted)
80 .size(IconSize::Small),
81 ),
82 }
83 }
84
85 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
86 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
87 let is_toggled = self.toggleable.is_toggled();
88
89 let disclosure_control = self.disclosure_control();
90
91 let meta = match self.meta {
92 Some(ListHeaderMeta::Tools(icons)) => div().child(
93 h_stack()
94 .gap_2()
95 .items_center()
96 .children(icons.into_iter().map(|i| {
97 IconElement::new(i)
98 .color(IconColor::Muted)
99 .size(IconSize::Small)
100 })),
101 ),
102 Some(ListHeaderMeta::Button(label)) => div().child(label),
103 Some(ListHeaderMeta::Text(label)) => div().child(label),
104 None => div(),
105 };
106
107 h_stack()
108 .w_full()
109 .bg(cx.theme().colors().surface)
110 // TODO: Add focus state
111 // .when(self.state == InteractionState::Focused, |this| {
112 // this.border()
113 // .border_color(cx.theme().colors().border_focused)
114 // })
115 .relative()
116 .child(
117 div()
118 .h_5()
119 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
120 .flex()
121 .flex_1()
122 .items_center()
123 .justify_between()
124 .w_full()
125 .gap_1()
126 .child(
127 h_stack()
128 .gap_1()
129 .child(
130 div()
131 .flex()
132 .gap_1()
133 .items_center()
134 .children(self.left_icon.map(|i| {
135 IconElement::new(i)
136 .color(IconColor::Muted)
137 .size(IconSize::Small)
138 }))
139 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
140 )
141 .child(disclosure_control),
142 )
143 .child(meta),
144 )
145 }
146}
147
148#[derive(Component)]
149pub struct ListSubHeader {
150 label: SharedString,
151 left_icon: Option<Icon>,
152 variant: ListItemVariant,
153}
154
155impl ListSubHeader {
156 pub fn new(label: impl Into<SharedString>) -> Self {
157 Self {
158 label: label.into(),
159 left_icon: None,
160 variant: ListItemVariant::default(),
161 }
162 }
163
164 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
165 self.left_icon = left_icon;
166 self
167 }
168
169 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
170 h_stack().flex_1().w_full().relative().py_1().child(
171 div()
172 .h_6()
173 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
174 .flex()
175 .flex_1()
176 .w_full()
177 .gap_1()
178 .items_center()
179 .justify_between()
180 .child(
181 div()
182 .flex()
183 .gap_1()
184 .items_center()
185 .children(self.left_icon.map(|i| {
186 IconElement::new(i)
187 .color(IconColor::Muted)
188 .size(IconSize::Small)
189 }))
190 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
191 ),
192 )
193 }
194}
195
196#[derive(Clone)]
197pub enum LeftContent {
198 Icon(Icon),
199 Avatar(SharedString),
200}
201
202#[derive(Default, PartialEq, Copy, Clone)]
203pub enum ListEntrySize {
204 #[default]
205 Small,
206 Medium,
207}
208
209#[derive(Component)]
210pub enum ListItem<V: 'static> {
211 Entry(ListEntry),
212 Details(ListDetailsEntry<V>),
213 Separator(ListSeparator),
214 Header(ListSubHeader),
215}
216
217impl<V: 'static> From<ListEntry> for ListItem<V> {
218 fn from(entry: ListEntry) -> Self {
219 Self::Entry(entry)
220 }
221}
222
223impl<V: 'static> From<ListDetailsEntry<V>> for ListItem<V> {
224 fn from(entry: ListDetailsEntry<V>) -> Self {
225 Self::Details(entry)
226 }
227}
228
229impl<V: 'static> From<ListSeparator> for ListItem<V> {
230 fn from(entry: ListSeparator) -> Self {
231 Self::Separator(entry)
232 }
233}
234
235impl<V: 'static> From<ListSubHeader> for ListItem<V> {
236 fn from(entry: ListSubHeader) -> Self {
237 Self::Header(entry)
238 }
239}
240
241impl<V: 'static> ListItem<V> {
242 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
243 match self {
244 ListItem::Entry(entry) => div().child(entry.render(view, cx)),
245 ListItem::Separator(separator) => div().child(separator.render(view, cx)),
246 ListItem::Header(header) => div().child(header.render(view, cx)),
247 ListItem::Details(details) => div().child(details.render(view, cx)),
248 }
249 }
250
251 pub fn new(label: Label) -> Self {
252 Self::Entry(ListEntry::new(label))
253 }
254
255 pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
256 if let Self::Entry(entry) = self {
257 Some(entry)
258 } else {
259 None
260 }
261 }
262}
263
264#[derive(Component)]
265pub struct ListEntry {
266 disclosure_control_style: DisclosureControlVisibility,
267 indent_level: u32,
268 label: Label,
269 left_content: Option<LeftContent>,
270 variant: ListItemVariant,
271 size: ListEntrySize,
272 state: InteractionState,
273 toggle: Option<ToggleState>,
274 overflow: OverflowStyle,
275}
276
277impl ListEntry {
278 pub fn new(label: Label) -> Self {
279 Self {
280 disclosure_control_style: DisclosureControlVisibility::default(),
281 indent_level: 0,
282 label,
283 variant: ListItemVariant::default(),
284 left_content: None,
285 size: ListEntrySize::default(),
286 state: InteractionState::default(),
287 // TODO: Should use Toggleable::NotToggleable
288 // or remove Toggleable::NotToggleable from the system
289 toggle: None,
290 overflow: OverflowStyle::Hidden,
291 }
292 }
293
294 pub fn variant(mut self, variant: ListItemVariant) -> Self {
295 self.variant = variant;
296 self
297 }
298
299 pub fn indent_level(mut self, indent_level: u32) -> Self {
300 self.indent_level = indent_level;
301 self
302 }
303
304 pub fn toggle(mut self, toggle: ToggleState) -> Self {
305 self.toggle = Some(toggle);
306 self
307 }
308
309 pub fn left_content(mut self, left_content: LeftContent) -> Self {
310 self.left_content = Some(left_content);
311 self
312 }
313
314 pub fn left_icon(mut self, left_icon: Icon) -> Self {
315 self.left_content = Some(LeftContent::Icon(left_icon));
316 self
317 }
318
319 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
320 self.left_content = Some(LeftContent::Avatar(left_avatar.into()));
321 self
322 }
323
324 pub fn state(mut self, state: InteractionState) -> Self {
325 self.state = state;
326 self
327 }
328
329 pub fn size(mut self, size: ListEntrySize) -> Self {
330 self.size = size;
331 self
332 }
333
334 pub fn disclosure_control_style(
335 mut self,
336 disclosure_control_style: DisclosureControlVisibility,
337 ) -> Self {
338 self.disclosure_control_style = disclosure_control_style;
339 self
340 }
341
342 fn label_color(&self) -> LabelColor {
343 match self.state {
344 InteractionState::Disabled => LabelColor::Disabled,
345 _ => Default::default(),
346 }
347 }
348
349 fn icon_color(&self) -> IconColor {
350 match self.state {
351 InteractionState::Disabled => IconColor::Disabled,
352 _ => Default::default(),
353 }
354 }
355
356 fn disclosure_control<V: 'static>(
357 &mut self,
358 cx: &mut ViewContext<V>,
359 ) -> Option<impl Component<V>> {
360 let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
361 IconElement::new(Icon::ChevronDown)
362 } else {
363 IconElement::new(Icon::ChevronRight)
364 }
365 .color(IconColor::Muted)
366 .size(IconSize::Small);
367
368 match (self.toggle, self.disclosure_control_style) {
369 (Some(_), DisclosureControlVisibility::OnHover) => {
370 Some(div().absolute().neg_left_5().child(disclosure_control_icon))
371 }
372 (Some(_), DisclosureControlVisibility::Always) => {
373 Some(div().child(disclosure_control_icon))
374 }
375 (None, _) => None,
376 }
377 }
378
379 fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
380 let settings = user_settings(cx);
381
382 let left_content = match self.left_content.clone() {
383 Some(LeftContent::Icon(i)) => Some(
384 h_stack().child(
385 IconElement::new(i)
386 .size(IconSize::Small)
387 .color(IconColor::Muted),
388 ),
389 ),
390 Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
391 None => None,
392 };
393
394 let sized_item = match self.size {
395 ListEntrySize::Small => div().h_6(),
396 ListEntrySize::Medium => div().h_7(),
397 };
398
399 div()
400 .relative()
401 .group("")
402 .bg(cx.theme().colors().surface)
403 .when(self.state == InteractionState::Focused, |this| {
404 this.border()
405 .border_color(cx.theme().colors().border_focused)
406 })
407 .child(
408 sized_item
409 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
410 // .ml(rems(0.75 * self.indent_level as f32))
411 .children((0..self.indent_level).map(|_| {
412 div()
413 .w(*settings.list_indent_depth)
414 .h_full()
415 .flex()
416 .justify_center()
417 .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
418 .child(
419 h_stack()
420 .child(div().w_px().h_full())
421 .child(div().w_px().h_full().bg(cx.theme().colors().border)),
422 )
423 }))
424 .flex()
425 .gap_1()
426 .items_center()
427 .relative()
428 .children(self.disclosure_control(cx))
429 .children(left_content)
430 .child(self.label),
431 )
432 }
433}
434
435struct ListDetailsEntryHandlers<V: 'static> {
436 click: Option<ClickHandler<V>>,
437}
438
439impl<V: 'static> Default for ListDetailsEntryHandlers<V> {
440 fn default() -> Self {
441 Self { click: None }
442 }
443}
444
445#[derive(Component)]
446pub struct ListDetailsEntry<V: 'static> {
447 label: SharedString,
448 meta: Option<SharedString>,
449 left_content: Option<LeftContent>,
450 handlers: ListDetailsEntryHandlers<V>,
451 actions: Option<Vec<Button<V>>>,
452 // TODO: make this more generic instead of
453 // specifically for notifications
454 seen: bool,
455}
456
457impl<V: 'static> ListDetailsEntry<V> {
458 pub fn new(label: impl Into<SharedString>) -> Self {
459 Self {
460 label: label.into(),
461 meta: None,
462 left_content: None,
463 handlers: ListDetailsEntryHandlers::default(),
464 actions: None,
465 seen: false,
466 }
467 }
468
469 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
470 self.meta = Some(meta.into());
471 self
472 }
473
474 pub fn seen(mut self, seen: bool) -> Self {
475 self.seen = seen;
476 self
477 }
478
479 pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
480 self.handlers.click = Some(handler);
481 self
482 }
483
484 pub fn actions(mut self, actions: Vec<Button<V>>) -> Self {
485 self.actions = Some(actions);
486 self
487 }
488
489 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
490 let settings = user_settings(cx);
491
492 let (item_bg, item_bg_hover, item_bg_active) = (
493 cx.theme().colors().ghost_element,
494 cx.theme().colors().ghost_element_hover,
495 cx.theme().colors().ghost_element_active,
496 );
497
498 let label_color = match self.seen {
499 true => LabelColor::Muted,
500 false => LabelColor::Default,
501 };
502
503 div()
504 .relative()
505 .group("")
506 .bg(item_bg)
507 .px_2()
508 .py_1p5()
509 .w_full()
510 .z_index(1)
511 .when(!self.seen, |this| {
512 this.child(
513 div()
514 .absolute()
515 .left(px(3.0))
516 .top_3()
517 .rounded_full()
518 .border_2()
519 .border_color(cx.theme().colors().surface)
520 .w(px(9.0))
521 .h(px(9.0))
522 .z_index(2)
523 .bg(cx.theme().status().info),
524 )
525 })
526 .child(
527 v_stack()
528 .w_full()
529 .line_height(relative(1.2))
530 .gap_1()
531 .child(
532 div()
533 .w_5()
534 .h_5()
535 .rounded_full()
536 .bg(cx.theme().colors().icon_accent),
537 )
538 .child(Label::new(self.label.clone()).color(label_color))
539 .children(
540 self.meta
541 .map(|meta| Label::new(meta).color(LabelColor::Muted)),
542 )
543 .child(
544 h_stack()
545 .gap_1()
546 .justify_end()
547 .children(self.actions.unwrap_or_default()),
548 ),
549 )
550 }
551}
552
553#[derive(Clone, Component)]
554pub struct ListSeparator;
555
556impl ListSeparator {
557 pub fn new() -> Self {
558 Self
559 }
560
561 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
562 div().h_px().w_full().bg(cx.theme().colors().border_variant)
563 }
564}
565
566#[derive(Component)]
567pub struct List<V: 'static> {
568 items: Vec<ListItem<V>>,
569 empty_message: SharedString,
570 header: Option<ListHeader>,
571 toggleable: Toggleable,
572}
573
574impl<V: 'static> List<V> {
575 pub fn new(items: Vec<ListItem<V>>) -> Self {
576 Self {
577 items,
578 empty_message: "No items".into(),
579 header: None,
580 toggleable: Toggleable::default(),
581 }
582 }
583
584 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
585 self.empty_message = empty_message.into();
586 self
587 }
588
589 pub fn header(mut self, header: ListHeader) -> Self {
590 self.header = Some(header);
591 self
592 }
593
594 pub fn toggle(mut self, toggle: ToggleState) -> Self {
595 self.toggleable = toggle.into();
596 self
597 }
598
599 fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
600 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
601 let is_toggled = Toggleable::is_toggled(&self.toggleable);
602
603 let list_content = match (self.items.is_empty(), is_toggled) {
604 (false, _) => div().children(self.items),
605 (true, false) => div(),
606 (true, true) => {
607 div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
608 }
609 };
610
611 v_stack()
612 .w_full()
613 .py_1()
614 .children(self.header.map(|header| header.toggleable(self.toggleable)))
615 .child(list_content)
616 }
617}