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