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