list.rs

  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}