1use std::marker::PhantomData;
2
3use gpui3::{div, Div};
4
5use crate::prelude::*;
6use crate::settings::user_settings;
7use crate::{h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor};
8
9#[derive(Clone, Copy, Default, Debug, PartialEq)]
10pub enum ListItemVariant {
11 /// The list item extends to the far left and right of the list.
12 FullWidth,
13 #[default]
14 Inset,
15}
16
17#[derive(Element, Clone)]
18pub struct ListHeader<S: 'static + Send + Sync + Clone> {
19 state_type: PhantomData<S>,
20 label: SharedString,
21 left_icon: Option<Icon>,
22 variant: ListItemVariant,
23 state: InteractionState,
24 toggleable: Toggleable,
25}
26
27impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
28 pub fn new(label: impl Into<SharedString>) -> Self {
29 Self {
30 state_type: PhantomData,
31 label: label.into(),
32 left_icon: None,
33 variant: ListItemVariant::default(),
34 state: InteractionState::default(),
35 toggleable: Toggleable::Toggleable(ToggleState::Toggled),
36 }
37 }
38
39 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
40 self.toggleable = toggle.into();
41 self
42 }
43
44 pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
45 self.toggleable = toggleable;
46 self
47 }
48
49 pub fn set_left_icon(mut self, left_icon: Option<Icon>) -> Self {
50 self.left_icon = left_icon;
51 self
52 }
53
54 pub fn state(mut self, state: InteractionState) -> Self {
55 self.state = state;
56 self
57 }
58
59 fn disclosure_control(&self) -> Div<S> {
60 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
61 let is_toggled = Toggleable::is_toggled(&self.toggleable);
62
63 match (is_toggleable, is_toggled) {
64 (false, _) => div(),
65 (_, true) => div().child(
66 IconElement::new(Icon::ChevronDown)
67 .color(IconColor::Muted)
68 .size(IconSize::Small),
69 ),
70 (_, false) => div().child(
71 IconElement::new(Icon::ChevronRight)
72 .color(IconColor::Muted)
73 .size(IconSize::Small),
74 ),
75 }
76 }
77
78 fn label_color(&self) -> LabelColor {
79 match self.state {
80 InteractionState::Disabled => LabelColor::Disabled,
81 _ => Default::default(),
82 }
83 }
84
85 fn icon_color(&self) -> IconColor {
86 match self.state {
87 InteractionState::Disabled => IconColor::Disabled,
88 _ => Default::default(),
89 }
90 }
91
92 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
93 let color = ThemeColor::new(cx);
94 let system_color = SystemColor::new();
95 let color = ThemeColor::new(cx);
96
97 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
98 let is_toggled = self.toggleable.is_toggled();
99
100 let disclosure_control = self.disclosure_control();
101
102 h_stack()
103 .flex_1()
104 .w_full()
105 .bg(color.surface)
106 .when(self.state == InteractionState::Focused, |this| {
107 this.border().border_color(color.border_focused)
108 })
109 .relative()
110 .child(
111 div()
112 .h_5()
113 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
114 .flex()
115 .flex_1()
116 .w_full()
117 .gap_1()
118 .items_center()
119 .child(
120 div()
121 .flex()
122 .gap_1()
123 .items_center()
124 .children(self.left_icon.map(|i| {
125 IconElement::new(i)
126 .color(IconColor::Muted)
127 .size(IconSize::Small)
128 }))
129 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
130 )
131 .child(disclosure_control),
132 )
133 }
134}
135
136#[derive(Element, Clone)]
137pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
138 state_type: PhantomData<S>,
139 label: SharedString,
140 left_icon: Option<Icon>,
141 variant: ListItemVariant,
142}
143
144impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
145 pub fn new(label: impl Into<SharedString>) -> Self {
146 Self {
147 state_type: PhantomData,
148 label: label.into(),
149 left_icon: None,
150 variant: ListItemVariant::default(),
151 }
152 }
153
154 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
155 self.left_icon = left_icon;
156 self
157 }
158
159 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
160 let color = ThemeColor::new(cx);
161
162 h_stack().flex_1().w_full().relative().py_1().child(
163 div()
164 .h_6()
165 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
166 .flex()
167 .flex_1()
168 .w_full()
169 .gap_1()
170 .items_center()
171 .justify_between()
172 .child(
173 div()
174 .flex()
175 .gap_1()
176 .items_center()
177 .children(self.left_icon.map(|i| {
178 IconElement::new(i)
179 .color(IconColor::Muted)
180 .size(IconSize::Small)
181 }))
182 .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
183 ),
184 )
185 }
186}
187
188#[derive(Clone)]
189pub enum LeftContent {
190 Icon(Icon),
191 Avatar(SharedString),
192}
193
194#[derive(Default, PartialEq, Copy, Clone)]
195pub enum ListEntrySize {
196 #[default]
197 Small,
198 Medium,
199}
200
201#[derive(Clone, Element)]
202pub enum ListItem<S: 'static + Send + Sync + Clone> {
203 Entry(ListEntry<S>),
204 Separator(ListSeparator<S>),
205 Header(ListSubHeader<S>),
206}
207
208impl<S: 'static + Send + Sync + Clone> 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 + Clone> From<ListSeparator<S>> for ListItem<S> {
215 fn from(entry: ListSeparator<S>) -> Self {
216 Self::Separator(entry)
217 }
218}
219
220impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
221 fn from(entry: ListSubHeader<S>) -> Self {
222 Self::Header(entry)
223 }
224}
225
226impl<S: 'static + Send + Sync + Clone> ListItem<S> {
227 fn render(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
228 match self {
229 ListItem::Entry(entry) => div().child(entry.render(view, cx)),
230 ListItem::Separator(separator) => div().child(separator.render(view, cx)),
231 ListItem::Header(header) => div().child(header.render(view, cx)),
232 }
233 }
234
235 pub fn new(label: Label<S>) -> Self {
236 Self::Entry(ListEntry::new(label))
237 }
238
239 pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
240 if let Self::Entry(entry) = self {
241 Some(entry)
242 } else {
243 None
244 }
245 }
246}
247
248#[derive(Element, Clone)]
249pub struct ListEntry<S: 'static + Send + Sync + Clone> {
250 disclosure_control_style: DisclosureControlVisibility,
251 indent_level: u32,
252 label: Label<S>,
253 left_content: Option<LeftContent>,
254 variant: ListItemVariant,
255 size: ListEntrySize,
256 state: InteractionState,
257 toggle: Option<ToggleState>,
258}
259
260impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
261 pub fn new(label: Label<S>) -> Self {
262 Self {
263 disclosure_control_style: DisclosureControlVisibility::default(),
264 indent_level: 0,
265 label,
266 variant: ListItemVariant::default(),
267 left_content: None,
268 size: ListEntrySize::default(),
269 state: InteractionState::default(),
270 // TODO: Should use Toggleable::NotToggleable
271 // or remove Toggleable::NotToggleable from the system
272 toggle: None,
273 }
274 }
275 pub fn set_variant(mut self, variant: ListItemVariant) -> Self {
276 self.variant = variant;
277 self
278 }
279 pub fn set_indent_level(mut self, indent_level: u32) -> Self {
280 self.indent_level = indent_level;
281 self
282 }
283
284 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
285 self.toggle = Some(toggle);
286 self
287 }
288
289 pub fn set_left_content(mut self, left_content: LeftContent) -> Self {
290 self.left_content = Some(left_content);
291 self
292 }
293
294 pub fn set_left_icon(mut self, left_icon: Icon) -> Self {
295 self.left_content = Some(LeftContent::Icon(left_icon));
296 self
297 }
298
299 pub fn set_left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
300 self.left_content = Some(LeftContent::Avatar(left_avatar.into()));
301 self
302 }
303
304 pub fn set_state(mut self, state: InteractionState) -> Self {
305 self.state = state;
306 self
307 }
308
309 pub fn set_size(mut self, size: ListEntrySize) -> Self {
310 self.size = size;
311 self
312 }
313
314 pub fn set_disclosure_control_style(
315 mut self,
316 disclosure_control_style: DisclosureControlVisibility,
317 ) -> Self {
318 self.disclosure_control_style = disclosure_control_style;
319 self
320 }
321
322 fn label_color(&self) -> LabelColor {
323 match self.state {
324 InteractionState::Disabled => LabelColor::Disabled,
325 _ => Default::default(),
326 }
327 }
328
329 fn icon_color(&self) -> IconColor {
330 match self.state {
331 InteractionState::Disabled => IconColor::Disabled,
332 _ => Default::default(),
333 }
334 }
335
336 fn disclosure_control(
337 &mut self,
338 cx: &mut ViewContext<S>,
339 ) -> Option<impl Element<ViewState = S>> {
340 let color = ThemeColor::new(cx);
341
342 let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
343 IconElement::new(Icon::ChevronDown)
344 } else {
345 IconElement::new(Icon::ChevronRight)
346 }
347 .color(IconColor::Muted)
348 .size(IconSize::Small);
349
350 match (self.toggle, self.disclosure_control_style) {
351 (Some(_), DisclosureControlVisibility::OnHover) => {
352 Some(div().absolute().neg_left_5().child(disclosure_control_icon))
353 }
354 (Some(_), DisclosureControlVisibility::Always) => {
355 Some(div().child(disclosure_control_icon))
356 }
357 (None, _) => None,
358 }
359 }
360
361 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
362 let color = ThemeColor::new(cx);
363 let system_color = SystemColor::new();
364 let color = ThemeColor::new(cx);
365 let settings = user_settings(cx);
366
367 let left_content = match self.left_content.clone() {
368 Some(LeftContent::Icon(i)) => Some(
369 h_stack().child(
370 IconElement::new(i)
371 .size(IconSize::Small)
372 .color(IconColor::Muted),
373 ),
374 ),
375 Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
376 None => None,
377 };
378
379 let sized_item = match self.size {
380 ListEntrySize::Small => div().h_6(),
381 ListEntrySize::Medium => div().h_7(),
382 };
383
384 div()
385 .relative()
386 .group("")
387 .bg(color.surface)
388 .when(self.state == InteractionState::Focused, |this| {
389 this.border().border_color(color.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(color.border_focused))
402 .child(
403 h_stack()
404 .child(div().w_px().h_full())
405 .child(div().w_px().h_full().bg(color.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.clone()),
415 )
416 }
417}
418
419#[derive(Clone, Element)]
420pub struct ListSeparator<S: 'static + Send + Sync> {
421 state_type: PhantomData<S>,
422}
423
424impl<S: 'static + Send + Sync> ListSeparator<S> {
425 pub fn new() -> Self {
426 Self {
427 state_type: PhantomData,
428 }
429 }
430
431 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
432 let color = ThemeColor::new(cx);
433
434 div().h_px().w_full().bg(color.border)
435 }
436}
437
438#[derive(Element)]
439pub struct List<S: 'static + Send + Sync + Clone> {
440 items: Vec<ListItem<S>>,
441 empty_message: SharedString,
442 header: Option<ListHeader<S>>,
443 toggleable: Toggleable,
444}
445
446impl<S: 'static + Send + Sync + Clone> List<S> {
447 pub fn new(items: Vec<ListItem<S>>) -> Self {
448 Self {
449 items,
450 empty_message: "No items".into(),
451 header: None,
452 toggleable: Toggleable::default(),
453 }
454 }
455
456 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
457 self.empty_message = empty_message.into();
458 self
459 }
460
461 pub fn header(mut self, header: ListHeader<S>) -> Self {
462 self.header = Some(header);
463 self
464 }
465
466 pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
467 self.toggleable = toggle.into();
468 self
469 }
470
471 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
472 let color = ThemeColor::new(cx);
473 let is_toggleable = self.toggleable != Toggleable::NotToggleable;
474 let is_toggled = Toggleable::is_toggled(&self.toggleable);
475
476 let list_content = match (self.items.is_empty(), is_toggled) {
477 (_, false) => div(),
478 (false, _) => div().children(self.items.iter().cloned()),
479 (true, _) => {
480 div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
481 }
482 };
483
484 v_stack()
485 .py_1()
486 .children(
487 self.header
488 .clone()
489 .map(|header| header.set_toggleable(self.toggleable)),
490 )
491 .child(list_content)
492 }
493}