list.rs

  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}