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