1use std::marker::PhantomData;
2
3use gpui3::{div, Div};
4
5use crate::prelude::*;
6use crate::theme::theme;
7use crate::{
8 h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor,
9 LabelSize,
10};
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, Clone)]
21pub struct ListHeader<S: 'static + Send + Sync + Clone> {
22 state_type: PhantomData<S>,
23 label: &'static str,
24 left_icon: Option<Icon>,
25 variant: ListItemVariant,
26 state: InteractionState,
27 toggleable: Toggleable,
28}
29
30impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
31 pub fn new(label: &'static str) -> Self {
32 Self {
33 state_type: PhantomData,
34 label,
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 theme = theme(cx);
97 let token = token();
98 let system_color = SystemColor::new();
99 let color = ThemeColor::new(cx);
100
101 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
102 let is_toggled = Toggleable::is_toggled(&self.toggleable);
103
104 let disclosure_control = self.disclosure_control();
105
106 h_stack()
107 .flex_1()
108 .w_full()
109 .fill(color.surface)
110 .when(self.state == InteractionState::Focused, |this| {
111 this.border().border_color(color.border_focused)
112 })
113 .relative()
114 .child(
115 div()
116 .h_5()
117 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
118 .flex()
119 .flex_1()
120 .w_full()
121 .gap_1()
122 .items_center()
123 .child(
124 div()
125 .flex()
126 .gap_1()
127 .items_center()
128 .children(self.left_icon.map(|i| {
129 IconElement::new(i)
130 .color(IconColor::Muted)
131 .size(IconSize::Small)
132 }))
133 .child(
134 Label::new(self.label)
135 .color(LabelColor::Muted)
136 .size(LabelSize::Small),
137 ),
138 )
139 .child(disclosure_control),
140 )
141 }
142}
143
144#[derive(Element, Clone)]
145pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
146 state_type: PhantomData<S>,
147 label: &'static str,
148 left_icon: Option<Icon>,
149 variant: ListItemVariant,
150}
151
152impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
153 pub fn new(label: &'static str) -> Self {
154 Self {
155 state_type: PhantomData,
156 label,
157 left_icon: None,
158 variant: ListItemVariant::default(),
159 }
160 }
161
162 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
163 self.left_icon = left_icon;
164 self
165 }
166
167 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
168 let theme = theme(cx);
169 let token = token();
170
171 h_stack().flex_1().w_full().relative().py_1().child(
172 div()
173 .h_6()
174 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
175 .flex()
176 .flex_1()
177 .w_full()
178 .gap_1()
179 .items_center()
180 .justify_between()
181 .child(
182 div()
183 .flex()
184 .gap_1()
185 .items_center()
186 .children(self.left_icon.map(|i| {
187 IconElement::new(i)
188 .color(IconColor::Muted)
189 .size(IconSize::Small)
190 }))
191 .child(
192 Label::new(self.label)
193 .color(LabelColor::Muted)
194 .size(LabelSize::Small),
195 ),
196 ),
197 )
198 }
199}
200
201#[derive(Clone)]
202pub enum LeftContent {
203 Icon(Icon),
204 Avatar(&'static str),
205}
206
207#[derive(Default, PartialEq, Copy, Clone)]
208pub enum ListEntrySize {
209 #[default]
210 Small,
211 Medium,
212}
213
214#[derive(Clone, Element)]
215pub enum ListItem<S: 'static + Send + Sync + Clone> {
216 Entry(ListEntry<S>),
217 Separator(ListSeparator<S>),
218 Header(ListSubHeader<S>),
219}
220
221impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
222 fn from(entry: ListEntry<S>) -> Self {
223 Self::Entry(entry)
224 }
225}
226
227impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
228 fn from(entry: ListSeparator<S>) -> Self {
229 Self::Separator(entry)
230 }
231}
232
233impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
234 fn from(entry: ListSubHeader<S>) -> Self {
235 Self::Header(entry)
236 }
237}
238
239impl<S: 'static + Send + Sync + Clone> ListItem<S> {
240 fn render(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
241 match self {
242 ListItem::Entry(entry) => div().child(entry.render(view, cx)),
243 ListItem::Separator(separator) => div().child(separator.render(view, cx)),
244 ListItem::Header(header) => div().child(header.render(view, cx)),
245 }
246 }
247
248 pub fn new(label: Label<S>) -> Self {
249 Self::Entry(ListEntry::new(label))
250 }
251
252 pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
253 if let Self::Entry(entry) = self {
254 Some(entry)
255 } else {
256 None
257 }
258 }
259}
260
261#[derive(Element, Clone)]
262pub struct ListEntry<S: 'static + Send + Sync + Clone> {
263 disclosure_control_style: DisclosureControlVisibility,
264 indent_level: u32,
265 label: Label<S>,
266 left_content: Option<LeftContent>,
267 variant: ListItemVariant,
268 size: ListEntrySize,
269 state: InteractionState,
270 toggle: Option<ToggleState>,
271}
272
273impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
274 pub fn new(label: Label<S>) -> Self {
275 Self {
276 disclosure_control_style: DisclosureControlVisibility::default(),
277 indent_level: 0,
278 label,
279 variant: ListItemVariant::default(),
280 left_content: None,
281 size: ListEntrySize::default(),
282 state: InteractionState::default(),
283 // TODO: Should use Toggleable::NotToggleable
284 // or remove Toggleable::NotToggleable from the system
285 toggle: None,
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: &'static str) -> Self {
313 self.left_content = Some(LeftContent::Avatar(left_avatar));
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 theme = theme(cx);
354 let token = token();
355
356 let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
357 IconElement::new(Icon::ChevronDown)
358 } else {
359 IconElement::new(Icon::ChevronRight)
360 }
361 .color(IconColor::Muted)
362 .size(IconSize::Small);
363
364 match (self.toggle, self.disclosure_control_style) {
365 (Some(_), DisclosureControlVisibility::OnHover) => {
366 Some(div().absolute().neg_left_5().child(disclosure_control_icon))
367 }
368 (Some(_), DisclosureControlVisibility::Always) => {
369 Some(div().child(disclosure_control_icon))
370 }
371 (None, _) => None,
372 }
373 }
374
375 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
376 let theme = theme(cx);
377 let token = token();
378 let system_color = SystemColor::new();
379 let color = ThemeColor::new(cx);
380
381 let left_content = match self.left_content {
382 Some(LeftContent::Icon(i)) => Some(
383 h_stack().child(
384 IconElement::new(i)
385 .size(IconSize::Small)
386 .color(IconColor::Muted),
387 ),
388 ),
389 Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
390 None => None,
391 };
392
393 let sized_item = match self.size {
394 ListEntrySize::Small => div().h_6(),
395 ListEntrySize::Medium => div().h_7(),
396 };
397
398 div()
399 .relative()
400 .group("")
401 .fill(color.surface)
402 .when(self.state == InteractionState::Focused, |this| {
403 this.border().border_color(color.border_focused)
404 })
405 .child(
406 sized_item
407 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
408 // .ml(rems(0.75 * self.indent_level as f32))
409 .children((0..self.indent_level).map(|_| {
410 div()
411 .w(token.list_indent_depth)
412 .h_full()
413 .flex()
414 .justify_center()
415 .group_hover("")
416 .fill(color.border_focused)
417 .child(
418 h_stack()
419 .child(div().w_px().h_full())
420 .child(div().w_px().h_full().fill(color.border)),
421 )
422 }))
423 .flex()
424 .gap_1()
425 .items_center()
426 .relative()
427 .children(self.disclosure_control(cx))
428 .children(left_content)
429 .child(self.label.clone()),
430 )
431 }
432}
433
434#[derive(Clone, Element)]
435pub struct ListSeparator<S: 'static + Send + Sync> {
436 state_type: PhantomData<S>,
437}
438
439impl<S: 'static + Send + Sync> ListSeparator<S> {
440 pub fn new() -> Self {
441 Self {
442 state_type: PhantomData,
443 }
444 }
445
446 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
447 let color = ThemeColor::new(cx);
448
449 div().h_px().w_full().fill(color.border)
450 }
451}
452
453#[derive(Element)]
454pub struct List<S: 'static + Send + Sync + Clone> {
455 items: Vec<ListItem<S>>,
456 empty_message: &'static str,
457 header: Option<ListHeader<S>>,
458 toggleable: Toggleable,
459}
460
461impl<S: 'static + Send + Sync + Clone> List<S> {
462 pub fn new(items: Vec<ListItem<S>>) -> Self {
463 Self {
464 items,
465 empty_message: "No items",
466 header: None,
467 toggleable: Toggleable::default(),
468 }
469 }
470
471 pub fn empty_message(mut self, empty_message: &'static str) -> Self {
472 self.empty_message = empty_message;
473 self
474 }
475
476 pub fn header(mut self, header: ListHeader<S>) -> Self {
477 self.header = Some(header);
478 self
479 }
480
481 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
482 self.toggleable = toggle.into();
483 self
484 }
485
486 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
487 let theme = theme(cx);
488 let token = token();
489 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
490 let is_toggled = Toggleable::is_toggled(&self.toggleable);
491
492 let list_content = match (self.items.is_empty(), is_toggled) {
493 (_, false) => div(),
494 (false, _) => div().children(self.items.iter().cloned()),
495 (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
496 };
497
498 v_stack()
499 .py_1()
500 .children(
501 self.header
502 .clone()
503 .map(|header| header.set_toggleable(self.toggleable)),
504 )
505 .child(list_content)
506 }
507}