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