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