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, IconElement, IconSize, Label, Toggle,
9};
10use crate::{prelude::*, GraphicSlot};
11
12#[derive(Clone, Copy, Default, Debug, PartialEq)]
13pub enum ListItemVariant {
14 /// The list item extends to the far left and right of the list.
15 FullWidth,
16 #[default]
17 Inset,
18}
19
20pub enum ListHeaderMeta {
21 // TODO: These should be IconButtons
22 Tools(Vec<Icon>),
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| {
49 IconElement::new(i)
50 .color(Color::Muted)
51 .size(IconSize::Small)
52 })),
53 ),
54 Some(ListHeaderMeta::Button(label)) => div().child(label),
55 Some(ListHeaderMeta::Text(label)) => div().child(label),
56 None => div(),
57 };
58
59 h_stack()
60 .w_full()
61 .bg(cx.theme().colors().surface_background)
62 .relative()
63 .child(
64 div()
65 .h_5()
66 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
67 .flex()
68 .flex_1()
69 .items_center()
70 .justify_between()
71 .w_full()
72 .gap_1()
73 .child(
74 h_stack()
75 .gap_1()
76 .child(
77 div()
78 .flex()
79 .gap_1()
80 .items_center()
81 .children(self.left_icon.map(|i| {
82 IconElement::new(i)
83 .color(Color::Muted)
84 .size(IconSize::Small)
85 }))
86 .child(Label::new(self.label.clone()).color(Color::Muted)),
87 )
88 .child(disclosure_control),
89 )
90 .child(meta),
91 )
92 }
93}
94
95impl ListHeader {
96 pub fn new(label: impl Into<SharedString>) -> Self {
97 Self {
98 label: label.into(),
99 left_icon: None,
100 meta: None,
101 variant: ListItemVariant::default(),
102 toggle: Toggle::NotToggleable,
103 }
104 }
105
106 pub fn toggle(mut self, toggle: Toggle) -> Self {
107 self.toggle = toggle;
108 self
109 }
110
111 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
112 self.left_icon = left_icon;
113 self
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(IntoElement)]
235pub struct ListItem {
236 id: ElementId,
237 disabled: bool,
238 selected: bool,
239 // TODO: Reintroduce this
240 // disclosure_control_style: DisclosureControlVisibility,
241 indent_level: u32,
242 left_slot: Option<GraphicSlot>,
243 overflow: OverflowStyle,
244 toggle: Toggle,
245 variant: ListItemVariant,
246 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
247 children: SmallVec<[AnyElement; 2]>,
248}
249
250impl ListItem {
251 pub fn new(id: impl Into<ElementId>) -> Self {
252 Self {
253 id: id.into(),
254 disabled: false,
255 selected: false,
256 indent_level: 0,
257 left_slot: None,
258 overflow: OverflowStyle::Hidden,
259 toggle: Toggle::NotToggleable,
260 variant: ListItemVariant::default(),
261 on_click: Default::default(),
262 children: SmallVec::new(),
263 }
264 }
265
266 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
267 self.on_click = Some(Rc::new(handler));
268 self
269 }
270
271 pub fn variant(mut self, variant: ListItemVariant) -> Self {
272 self.variant = variant;
273 self
274 }
275
276 pub fn indent_level(mut self, indent_level: u32) -> Self {
277 self.indent_level = indent_level;
278 self
279 }
280
281 pub fn toggle(mut self, toggle: Toggle) -> Self {
282 self.toggle = toggle;
283 self
284 }
285
286 pub fn selected(mut self, selected: bool) -> Self {
287 self.selected = selected;
288 self
289 }
290
291 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
292 self.left_slot = Some(left_content);
293 self
294 }
295
296 pub fn left_icon(mut self, left_icon: Icon) -> Self {
297 self.left_slot = Some(GraphicSlot::Icon(left_icon));
298 self
299 }
300
301 pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
302 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
303 self
304 }
305}
306
307impl RenderOnce for ListItem {
308 type Rendered = Stateful<Div>;
309
310 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
311 let left_content = match self.left_slot.clone() {
312 Some(GraphicSlot::Icon(i)) => Some(
313 h_stack().child(
314 IconElement::new(i)
315 .size(IconSize::Small)
316 .color(Color::Muted),
317 ),
318 ),
319 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
320 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
321 None => None,
322 };
323
324 div()
325 .id(self.id)
326 .relative()
327 .hover(|mut style| {
328 style.background = Some(cx.theme().colors().editor_background.into());
329 style
330 })
331 .on_click({
332 let on_click = self.on_click.clone();
333 move |event, cx| {
334 if let Some(on_click) = &on_click {
335 (on_click)(event, cx)
336 }
337 }
338 })
339 // TODO: Add focus state
340 // .when(self.state == InteractionState::Focused, |this| {
341 // this.border()
342 // .border_color(cx.theme().colors().border_focused)
343 // })
344 .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
345 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
346 .when(self.selected, |this| {
347 this.bg(cx.theme().colors().ghost_element_selected)
348 })
349 .child(
350 div()
351 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
352 // .ml(rems(0.75 * self.indent_level as f32))
353 .children((0..self.indent_level).map(|_| {
354 div()
355 .w(px(4.))
356 .h_full()
357 .flex()
358 .justify_center()
359 .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
360 .child(
361 h_stack()
362 .child(div().w_px().h_full())
363 .child(div().w_px().h_full().bg(cx.theme().colors().border)),
364 )
365 }))
366 .flex()
367 .gap_1()
368 .items_center()
369 .relative()
370 .child(disclosure_control(self.toggle))
371 .children(left_content)
372 .children(self.children),
373 )
374 }
375}
376
377impl ParentElement for ListItem {
378 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
379 &mut self.children
380 }
381}
382
383#[derive(IntoElement, Clone)]
384pub struct ListSeparator;
385
386impl ListSeparator {
387 pub fn new() -> Self {
388 Self
389 }
390}
391
392impl RenderOnce for ListSeparator {
393 type Rendered = Div;
394
395 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
396 div().h_px().w_full().bg(cx.theme().colors().border_variant)
397 }
398}
399
400#[derive(IntoElement)]
401pub struct List {
402 /// Message to display when the list is empty
403 /// Defaults to "No items"
404 empty_message: SharedString,
405 header: Option<ListHeader>,
406 toggle: Toggle,
407 children: SmallVec<[AnyElement; 2]>,
408}
409
410impl RenderOnce for List {
411 type Rendered = Div;
412
413 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
414 let list_content = match (self.children.is_empty(), self.toggle) {
415 (false, _) => div().children(self.children),
416 (true, Toggle::Toggled(false)) => div(),
417 (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
418 };
419
420 v_stack()
421 .w_full()
422 .py_1()
423 .children(self.header.map(|header| header))
424 .child(list_content)
425 }
426}
427
428impl List {
429 pub fn new() -> Self {
430 Self {
431 empty_message: "No items".into(),
432 header: None,
433 toggle: Toggle::NotToggleable,
434 children: SmallVec::new(),
435 }
436 }
437
438 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
439 self.empty_message = empty_message.into();
440 self
441 }
442
443 pub fn header(mut self, header: ListHeader) -> Self {
444 self.header = Some(header);
445 self
446 }
447
448 pub fn toggle(mut self, toggle: Toggle) -> Self {
449 self.toggle = toggle;
450 self
451 }
452}
453
454impl ParentElement for List {
455 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
456 &mut self.children
457 }
458}