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