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