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