1use std::marker::PhantomData;
2
3use gpui3::{div, Div, Hsla, WindowContext};
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 #[default]
16 FullWidth,
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::default(),
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 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(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
69 (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
70 }
71 }
72
73 fn background_color(&self, cx: &WindowContext) -> Hsla {
74 let theme = theme(cx);
75 let system_color = SystemColor::new();
76
77 match self.state {
78 InteractionState::Hovered => theme.lowest.base.hovered.background,
79 InteractionState::Active => theme.lowest.base.pressed.background,
80 InteractionState::Enabled => theme.lowest.on.default.background,
81 _ => system_color.transparent,
82 }
83 }
84
85 fn label_color(&self) -> LabelColor {
86 match self.state {
87 InteractionState::Disabled => LabelColor::Disabled,
88 _ => Default::default(),
89 }
90 }
91
92 fn icon_color(&self) -> IconColor {
93 match self.state {
94 InteractionState::Disabled => IconColor::Disabled,
95 _ => Default::default(),
96 }
97 }
98
99 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
100 let theme = theme(cx);
101 let token = token();
102 let system_color = SystemColor::new();
103 let background_color = self.background_color(cx);
104
105 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
106 let is_toggled = Toggleable::is_toggled(&self.toggleable);
107
108 let disclosure_control = self.disclosure_control();
109
110 h_stack()
111 .flex_1()
112 .w_full()
113 .fill(background_color)
114 .when(self.state == InteractionState::Focused, |this| {
115 this.border()
116 .border_color(theme.lowest.accent.default.border)
117 })
118 .relative()
119 .py_1()
120 .child(
121 div()
122 .h_6()
123 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
124 .flex()
125 .flex_1()
126 .w_full()
127 .gap_1()
128 .items_center()
129 .justify_between()
130 .child(
131 div()
132 .flex()
133 .gap_1()
134 .items_center()
135 .children(self.left_icon.map(|i| {
136 IconElement::new(i)
137 .color(IconColor::Muted)
138 .size(IconSize::Small)
139 }))
140 .child(
141 Label::new(self.label.clone())
142 .color(LabelColor::Muted)
143 .size(LabelSize::Small),
144 ),
145 )
146 .child(disclosure_control),
147 )
148 }
149}
150
151#[derive(Element, Clone)]
152pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
153 state_type: PhantomData<S>,
154 label: &'static str,
155 left_icon: Option<Icon>,
156 variant: ListItemVariant,
157}
158
159impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
160 pub fn new(label: &'static str) -> Self {
161 Self {
162 state_type: PhantomData,
163 label,
164 left_icon: None,
165 variant: ListItemVariant::default(),
166 }
167 }
168
169 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
170 self.left_icon = left_icon;
171 self
172 }
173
174 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
175 let theme = theme(cx);
176 let token = token();
177
178 h_stack().flex_1().w_full().relative().py_1().child(
179 div()
180 .h_6()
181 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
182 .flex()
183 .flex_1()
184 .w_full()
185 .gap_1()
186 .items_center()
187 .justify_between()
188 .child(
189 div()
190 .flex()
191 .gap_1()
192 .items_center()
193 .children(self.left_icon.map(|i| {
194 IconElement::new(i)
195 .color(IconColor::Muted)
196 .size(IconSize::Small)
197 }))
198 .child(
199 Label::new(self.label.clone())
200 .color(LabelColor::Muted)
201 .size(LabelSize::Small),
202 ),
203 ),
204 )
205 }
206}
207
208#[derive(Clone)]
209pub enum LeftContent {
210 Icon(Icon),
211 Avatar(&'static str),
212}
213
214#[derive(Default, PartialEq, Copy, Clone)]
215pub enum ListEntrySize {
216 #[default]
217 Small,
218 Medium,
219}
220
221#[derive(Clone, Element)]
222pub enum ListItem<S: 'static + Send + Sync + Clone> {
223 Entry(ListEntry<S>),
224 Separator(ListSeparator<S>),
225 Header(ListSubHeader<S>),
226}
227
228impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
229 fn from(entry: ListEntry<S>) -> Self {
230 Self::Entry(entry)
231 }
232}
233
234impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
235 fn from(entry: ListSeparator<S>) -> Self {
236 Self::Separator(entry)
237 }
238}
239
240impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
241 fn from(entry: ListSubHeader<S>) -> Self {
242 Self::Header(entry)
243 }
244}
245
246impl<S: 'static + Send + Sync + Clone> ListItem<S> {
247 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
248 match self {
249 ListItem::Entry(entry) => div().child(entry.render(cx)),
250 ListItem::Separator(separator) => div().child(separator.render(cx)),
251 ListItem::Header(header) => div().child(header.render(cx)),
252 }
253 }
254
255 pub fn new(label: Label<S>) -> Self {
256 Self::Entry(ListEntry::new(label))
257 }
258
259 pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
260 if let Self::Entry(entry) = self {
261 Some(entry)
262 } else {
263 None
264 }
265 }
266}
267
268#[derive(Element, Clone)]
269pub struct ListEntry<S: 'static + Send + Sync + Clone> {
270 disclosure_control_style: DisclosureControlVisibility,
271 indent_level: u32,
272 label: Label<S>,
273 left_content: Option<LeftContent>,
274 variant: ListItemVariant,
275 size: ListEntrySize,
276 state: InteractionState,
277 toggle: Option<ToggleState>,
278}
279
280impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
281 pub fn new(label: Label<S>) -> Self {
282 Self {
283 disclosure_control_style: DisclosureControlVisibility::default(),
284 indent_level: 0,
285 label,
286 variant: ListItemVariant::default(),
287 left_content: None,
288 size: ListEntrySize::default(),
289 state: InteractionState::default(),
290 toggle: None,
291 }
292 }
293 pub fn variant(mut self, variant: ListItemVariant) -> Self {
294 self.variant = variant;
295 self
296 }
297 pub fn indent_level(mut self, indent_level: u32) -> Self {
298 self.indent_level = indent_level;
299 self
300 }
301
302 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
303 self.toggle = Some(toggle);
304 self
305 }
306
307 pub fn left_content(mut self, left_content: LeftContent) -> Self {
308 self.left_content = Some(left_content);
309 self
310 }
311
312 pub fn left_icon(mut self, left_icon: Icon) -> Self {
313 self.left_content = Some(LeftContent::Icon(left_icon));
314 self
315 }
316
317 pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
318 self.left_content = Some(LeftContent::Avatar(left_avatar));
319 self
320 }
321
322 pub fn state(mut self, state: InteractionState) -> Self {
323 self.state = state;
324 self
325 }
326
327 pub fn size(mut self, size: ListEntrySize) -> Self {
328 self.size = size;
329 self
330 }
331
332 pub fn disclosure_control_style(
333 mut self,
334 disclosure_control_style: DisclosureControlVisibility,
335 ) -> Self {
336 self.disclosure_control_style = disclosure_control_style;
337 self
338 }
339
340 fn background_color(&self, cx: &WindowContext) -> Hsla {
341 let theme = theme(cx);
342 let system_color = SystemColor::new();
343
344 match self.state {
345 InteractionState::Hovered => theme.lowest.base.hovered.background,
346 InteractionState::Active => theme.lowest.base.pressed.background,
347 InteractionState::Enabled => theme.lowest.on.default.background,
348 _ => system_color.transparent,
349 }
350 }
351
352 fn label_color(&self) -> LabelColor {
353 match self.state {
354 InteractionState::Disabled => LabelColor::Disabled,
355 _ => Default::default(),
356 }
357 }
358
359 fn icon_color(&self) -> IconColor {
360 match self.state {
361 InteractionState::Disabled => IconColor::Disabled,
362 _ => Default::default(),
363 }
364 }
365
366 fn disclosure_control(&mut self, cx: &mut ViewContext<S>) -> Option<impl Element<State = S>> {
367 let theme = theme(cx);
368 let token = token();
369
370 let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
371 IconElement::new(Icon::ChevronDown)
372 } else {
373 IconElement::new(Icon::ChevronRight)
374 }
375 .color(IconColor::Muted)
376 .size(IconSize::Small);
377
378 match (self.toggle, self.disclosure_control_style) {
379 (Some(_), DisclosureControlVisibility::OnHover) => {
380 Some(div().absolute().neg_left_5().child(disclosure_control_icon))
381 }
382 (Some(_), DisclosureControlVisibility::Always) => {
383 Some(div().child(disclosure_control_icon))
384 }
385 (None, _) => None,
386 }
387 }
388
389 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
390 let theme = theme(cx);
391 let token = token();
392 let system_color = SystemColor::new();
393 let background_color = self.background_color(cx);
394
395 let left_content = match self.left_content {
396 Some(LeftContent::Icon(i)) => {
397 Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
398 }
399 Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
400 None => None,
401 };
402
403 let sized_item = match self.size {
404 ListEntrySize::Small => div().h_6(),
405 ListEntrySize::Medium => div().h_7(),
406 };
407
408 div()
409 .fill(background_color)
410 .when(self.state == InteractionState::Focused, |this| {
411 this.border()
412 .border_color(theme.lowest.accent.default.border)
413 })
414 .relative()
415 .py_1()
416 .child(
417 sized_item
418 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
419 // .ml(rems(0.75 * self.indent_level as f32))
420 .children((0..self.indent_level).map(|_| {
421 div()
422 .w(token.list_indent_depth)
423 .h_full()
424 .flex()
425 .justify_center()
426 .child(h_stack().child(div().w_px().h_full()).child(
427 div().w_px().h_full().fill(theme.middle.base.default.border),
428 ))
429 }))
430 .flex()
431 .gap_1()
432 .items_center()
433 .relative()
434 .children(self.disclosure_control(cx))
435 .children(left_content)
436 .child(self.label.clone()),
437 )
438 }
439}
440
441#[derive(Clone, Element)]
442pub struct ListSeparator<S: 'static + Send + Sync> {
443 state_type: PhantomData<S>,
444}
445
446impl<S: 'static + Send + Sync> ListSeparator<S> {
447 pub fn new() -> Self {
448 Self {
449 state_type: PhantomData,
450 }
451 }
452
453 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
454 let theme = theme(cx);
455
456 div().h_px().w_full().fill(theme.lowest.base.default.border)
457 }
458}
459
460#[derive(Element)]
461pub struct List<S: 'static + Send + Sync + Clone> {
462 items: Vec<ListItem<S>>,
463 empty_message: &'static str,
464 header: Option<ListHeader<S>>,
465 toggleable: Toggleable,
466}
467
468impl<S: 'static + Send + Sync + Clone> List<S> {
469 pub fn new(items: Vec<ListItem<S>>) -> Self {
470 Self {
471 items,
472 empty_message: "No items",
473 header: None,
474 toggleable: Toggleable::default(),
475 }
476 }
477
478 pub fn empty_message(mut self, empty_message: &'static str) -> Self {
479 self.empty_message = empty_message;
480 self
481 }
482
483 pub fn header(mut self, header: ListHeader<S>) -> Self {
484 self.header = Some(header);
485 self
486 }
487
488 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
489 self.toggleable = toggle.into();
490 self
491 }
492
493 fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
494 let theme = theme(cx);
495 let token = token();
496 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
497 let is_toggled = Toggleable::is_toggled(&self.toggleable);
498
499 let list_content = match (self.items.is_empty(), is_toggled) {
500 (_, false) => div(),
501 (false, _) => div().children(self.items.iter().cloned()),
502 (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
503 };
504
505 v_stack()
506 .py_1()
507 .children(
508 self.header
509 .clone()
510 .map(|header| header.set_toggleable(self.toggleable)),
511 )
512 .child(list_content)
513 }
514}