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