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