1use gpui::{div, AnyElement, Div, RenderOnce, Stateful, StatefulInteractiveElement};
2use smallvec::SmallVec;
3use std::rc::Rc;
4
5use crate::settings::user_settings;
6use crate::{
7 disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
8};
9use crate::{prelude::*, GraphicSlot};
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 FullWidth,
15 #[default]
16 Inset,
17}
18
19pub enum ListHeaderMeta {
20 // TODO: These should be IconButtons
21 Tools(Vec<Icon>),
22 // TODO: This should be a button
23 Button(Label),
24 Text(Label),
25}
26
27#[derive(RenderOnce)]
28pub struct ListHeader {
29 label: SharedString,
30 left_icon: Option<Icon>,
31 meta: Option<ListHeaderMeta>,
32 variant: ListItemVariant,
33 toggle: Toggle,
34}
35
36impl<V: 'static> Component<V> for ListHeader {
37 type Rendered = Div<V>;
38
39 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
40 let disclosure_control = disclosure_control(self.toggle);
41
42 let meta = match self.meta {
43 Some(ListHeaderMeta::Tools(icons)) => div().child(
44 h_stack()
45 .gap_2()
46 .items_center()
47 .children(icons.into_iter().map(|i| {
48 IconElement::new(i)
49 .color(TextColor::Muted)
50 .size(IconSize::Small)
51 })),
52 ),
53 Some(ListHeaderMeta::Button(label)) => div().child(label),
54 Some(ListHeaderMeta::Text(label)) => div().child(label),
55 None => div(),
56 };
57
58 h_stack()
59 .w_full()
60 .bg(cx.theme().colors().surface_background)
61 .relative()
62 .child(
63 div()
64 .h_5()
65 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
66 .flex()
67 .flex_1()
68 .items_center()
69 .justify_between()
70 .w_full()
71 .gap_1()
72 .child(
73 h_stack()
74 .gap_1()
75 .child(
76 div()
77 .flex()
78 .gap_1()
79 .items_center()
80 .children(self.left_icon.map(|i| {
81 IconElement::new(i)
82 .color(TextColor::Muted)
83 .size(IconSize::Small)
84 }))
85 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
86 )
87 .child(disclosure_control),
88 )
89 .child(meta),
90 )
91 }
92}
93
94impl ListHeader {
95 pub fn new(label: impl Into<SharedString>) -> Self {
96 Self {
97 label: label.into(),
98 left_icon: None,
99 meta: None,
100 variant: ListItemVariant::default(),
101 toggle: Toggle::NotToggleable,
102 }
103 }
104
105 pub fn toggle(mut self, toggle: Toggle) -> Self {
106 self.toggle = toggle;
107 self
108 }
109
110 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
111 self.left_icon = left_icon;
112 self
113 }
114
115 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
116 self.meta = meta;
117 self
118 }
119
120 // before_ship!("delete")
121 // fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
122 // let disclosure_control = disclosure_control(self.toggle);
123
124 // let meta = match self.meta {
125 // Some(ListHeaderMeta::Tools(icons)) => div().child(
126 // h_stack()
127 // .gap_2()
128 // .items_center()
129 // .children(icons.into_iter().map(|i| {
130 // IconElement::new(i)
131 // .color(TextColor::Muted)
132 // .size(IconSize::Small)
133 // })),
134 // ),
135 // Some(ListHeaderMeta::Button(label)) => div().child(label),
136 // Some(ListHeaderMeta::Text(label)) => div().child(label),
137 // None => div(),
138 // };
139
140 // h_stack()
141 // .w_full()
142 // .bg(cx.theme().colors().surface_background)
143 // // TODO: Add focus state
144 // // .when(self.state == InteractionState::Focused, |this| {
145 // // this.border()
146 // // .border_color(cx.theme().colors().border_focused)
147 // // })
148 // .relative()
149 // .child(
150 // div()
151 // .h_5()
152 // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
153 // .flex()
154 // .flex_1()
155 // .items_center()
156 // .justify_between()
157 // .w_full()
158 // .gap_1()
159 // .child(
160 // h_stack()
161 // .gap_1()
162 // .child(
163 // div()
164 // .flex()
165 // .gap_1()
166 // .items_center()
167 // .children(self.left_icon.map(|i| {
168 // IconElement::new(i)
169 // .color(TextColor::Muted)
170 // .size(IconSize::Small)
171 // }))
172 // .child(Label::new(self.label.clone()).color(TextColor::Muted)),
173 // )
174 // .child(disclosure_control),
175 // )
176 // .child(meta),
177 // )
178 // }
179}
180
181#[derive(RenderOnce, Clone)]
182pub struct ListSubHeader {
183 label: SharedString,
184 left_icon: Option<Icon>,
185 variant: ListItemVariant,
186}
187
188impl ListSubHeader {
189 pub fn new(label: impl Into<SharedString>) -> Self {
190 Self {
191 label: label.into(),
192 left_icon: None,
193 variant: ListItemVariant::default(),
194 }
195 }
196
197 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
198 self.left_icon = left_icon;
199 self
200 }
201}
202
203impl<V: 'static> Component<V> for ListSubHeader {
204 type Rendered = Div<V>;
205
206 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
207 h_stack().flex_1().w_full().relative().py_1().child(
208 div()
209 .h_6()
210 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
211 .flex()
212 .flex_1()
213 .w_full()
214 .gap_1()
215 .items_center()
216 .justify_between()
217 .child(
218 div()
219 .flex()
220 .gap_1()
221 .items_center()
222 .children(self.left_icon.map(|i| {
223 IconElement::new(i)
224 .color(TextColor::Muted)
225 .size(IconSize::Small)
226 }))
227 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
228 ),
229 )
230 }
231}
232
233#[derive(Default, PartialEq, Copy, Clone)]
234pub enum ListEntrySize {
235 #[default]
236 Small,
237 Medium,
238}
239
240#[derive(RenderOnce)]
241pub struct ListEntry<V: 'static> {
242 id: ElementId,
243 disabled: bool,
244 // TODO: Reintroduce this
245 // disclosure_control_style: DisclosureControlVisibility,
246 indent_level: u32,
247 label: Label,
248 left_slot: Option<GraphicSlot>,
249 overflow: OverflowStyle,
250 size: ListEntrySize,
251 toggle: Toggle,
252 variant: ListItemVariant,
253 on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
254}
255
256impl<V> Clone for ListEntry<V> {
257 fn clone(&self) -> Self {
258 Self {
259 id: self.id.clone(),
260 disabled: self.disabled,
261 indent_level: self.indent_level,
262 label: self.label.clone(),
263 left_slot: self.left_slot.clone(),
264 overflow: self.overflow,
265 size: self.size,
266 toggle: self.toggle,
267 variant: self.variant,
268 on_click: self.on_click.clone(),
269 }
270 }
271}
272
273impl<V: 'static> ListEntry<V> {
274 pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
275 Self {
276 id: id.into(),
277 disabled: false,
278 indent_level: 0,
279 label,
280 left_slot: None,
281 overflow: OverflowStyle::Hidden,
282 size: ListEntrySize::default(),
283 toggle: Toggle::NotToggleable,
284 variant: ListItemVariant::default(),
285 on_click: Default::default(),
286 }
287 }
288
289 pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
290 self.on_click = Some(Rc::new(handler));
291 self
292 }
293
294 pub fn variant(mut self, variant: ListItemVariant) -> Self {
295 self.variant = variant;
296 self
297 }
298
299 pub fn indent_level(mut self, indent_level: u32) -> Self {
300 self.indent_level = indent_level;
301 self
302 }
303
304 pub fn toggle(mut self, toggle: Toggle) -> Self {
305 self.toggle = toggle;
306 self
307 }
308
309 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
310 self.left_slot = Some(left_content);
311 self
312 }
313
314 pub fn left_icon(mut self, left_icon: Icon) -> Self {
315 self.left_slot = Some(GraphicSlot::Icon(left_icon));
316 self
317 }
318
319 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
320 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
321 self
322 }
323
324 pub fn size(mut self, size: ListEntrySize) -> Self {
325 self.size = size;
326 self
327 }
328}
329
330impl<V: 'static> Component<V> for ListEntry<V> {
331 type Rendered = Stateful<V, Div<V>>;
332
333 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
334 let settings = user_settings(cx);
335
336 let left_content = match self.left_slot.clone() {
337 Some(GraphicSlot::Icon(i)) => Some(
338 h_stack().child(
339 IconElement::new(i)
340 .size(IconSize::Small)
341 .color(TextColor::Muted),
342 ),
343 ),
344 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
345 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
346 None => None,
347 };
348
349 let sized_item = match self.size {
350 ListEntrySize::Small => div().h_6(),
351 ListEntrySize::Medium => div().h_7(),
352 };
353 div()
354 .id(self.id)
355 .relative()
356 .hover(|mut style| {
357 style.background = Some(cx.theme().colors().editor_background.into());
358 style
359 })
360 .on_click({
361 let on_click = self.on_click.clone();
362
363 move |view: &mut V, event, cx| {
364 if let Some(on_click) = &on_click {
365 (on_click)(view, cx)
366 }
367 }
368 })
369 .bg(cx.theme().colors().surface_background)
370 // TODO: Add focus state
371 // .when(self.state == InteractionState::Focused, |this| {
372 // this.border()
373 // .border_color(cx.theme().colors().border_focused)
374 // })
375 .child(
376 sized_item
377 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
378 // .ml(rems(0.75 * self.indent_level as f32))
379 .children((0..self.indent_level).map(|_| {
380 div()
381 .w(*settings.list_indent_depth)
382 .h_full()
383 .flex()
384 .justify_center()
385 .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
386 .child(
387 h_stack()
388 .child(div().w_px().h_full())
389 .child(div().w_px().h_full().bg(cx.theme().colors().border)),
390 )
391 }))
392 .flex()
393 .gap_1()
394 .items_center()
395 .relative()
396 .child(disclosure_control(self.toggle))
397 .children(left_content)
398 .child(self.label),
399 )
400 }
401}
402
403#[derive(RenderOnce, Clone)]
404pub struct ListSeparator;
405
406impl ListSeparator {
407 pub fn new() -> Self {
408 Self
409 }
410}
411
412impl<V: 'static> Component<V> for ListSeparator {
413 type Rendered = Div<V>;
414
415 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
416 div().h_px().w_full().bg(cx.theme().colors().border_variant)
417 }
418}
419
420#[derive(RenderOnce)]
421pub struct List<V: 'static> {
422 /// Message to display when the list is empty
423 /// Defaults to "No items"
424 empty_message: SharedString,
425 header: Option<ListHeader>,
426 toggle: Toggle,
427 children: SmallVec<[AnyElement<V>; 2]>,
428}
429
430impl<V: 'static> Component<V> for List<V> {
431 type Rendered = Div<V>;
432
433 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
434 let list_content = match (self.children.is_empty(), self.toggle) {
435 (false, _) => div().children(self.children),
436 (true, Toggle::Toggled(false)) => div(),
437 (true, _) => {
438 div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
439 }
440 };
441
442 v_stack()
443 .w_full()
444 .py_1()
445 .children(self.header.map(|header| header))
446 .child(list_content)
447 }
448}
449
450impl<V: 'static> List<V> {
451 pub fn new() -> Self {
452 Self {
453 empty_message: "No items".into(),
454 header: None,
455 toggle: Toggle::NotToggleable,
456 children: SmallVec::new(),
457 }
458 }
459
460 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
461 self.empty_message = empty_message.into();
462 self
463 }
464
465 pub fn header(mut self, header: ListHeader) -> Self {
466 self.header = Some(header);
467 self
468 }
469
470 pub fn toggle(mut self, toggle: Toggle) -> Self {
471 self.toggle = toggle;
472 self
473 }
474}
475
476impl<V: 'static> ParentElement<V> for List<V> {
477 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
478 &mut self.children
479 }
480}