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)]
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)]
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 ListEntry {
238    pub fn new(label: Label) -> Self {
239        Self {
240            disabled: false,
241            indent_level: 0,
242            label,
243            left_slot: None,
244            overflow: OverflowStyle::Hidden,
245            size: ListEntrySize::default(),
246            toggle: Toggle::NotToggleable,
247            variant: ListItemVariant::default(),
248            on_click: Default::default(),
249        }
250    }
251
252    pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
253        self.on_click = Some(action.into());
254        self
255    }
256
257    pub fn variant(mut self, variant: ListItemVariant) -> Self {
258        self.variant = variant;
259        self
260    }
261
262    pub fn indent_level(mut self, indent_level: u32) -> Self {
263        self.indent_level = indent_level;
264        self
265    }
266
267    pub fn toggle(mut self, toggle: Toggle) -> Self {
268        self.toggle = toggle;
269        self
270    }
271
272    pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
273        self.left_slot = Some(left_content);
274        self
275    }
276
277    pub fn left_icon(mut self, left_icon: Icon) -> Self {
278        self.left_slot = Some(GraphicSlot::Icon(left_icon));
279        self
280    }
281
282    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
283        self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
284        self
285    }
286
287    pub fn size(mut self, size: ListEntrySize) -> Self {
288        self.size = size;
289        self
290    }
291
292    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
293        let settings = user_settings(cx);
294
295        let left_content = match self.left_slot.clone() {
296            Some(GraphicSlot::Icon(i)) => Some(
297                h_stack().child(
298                    IconElement::new(i)
299                        .size(IconSize::Small)
300                        .color(TextColor::Muted),
301                ),
302            ),
303            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
304            Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
305            None => None,
306        };
307
308        let sized_item = match self.size {
309            ListEntrySize::Small => div().h_6(),
310            ListEntrySize::Medium => div().h_7(),
311        };
312        div()
313            .relative()
314            .hover(|mut style| {
315                style.background = Some(cx.theme().colors().editor_background.into());
316                style
317            })
318            .on_mouse_down(gpui::MouseButton::Left, {
319                let action = self.on_click.map(|action| action.boxed_clone());
320
321                move |entry: &mut V, event, cx| {
322                    if let Some(action) = action.as_ref() {
323                        cx.dispatch_action(action.boxed_clone());
324                    }
325                }
326            })
327            .group("")
328            .bg(cx.theme().colors().surface_background)
329            // TODO: Add focus state
330            // .when(self.state == InteractionState::Focused, |this| {
331            //     this.border()
332            //         .border_color(cx.theme().colors().border_focused)
333            // })
334            .child(
335                sized_item
336                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
337                    // .ml(rems(0.75 * self.indent_level as f32))
338                    .children((0..self.indent_level).map(|_| {
339                        div()
340                            .w(*settings.list_indent_depth)
341                            .h_full()
342                            .flex()
343                            .justify_center()
344                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
345                            .child(
346                                h_stack()
347                                    .child(div().w_px().h_full())
348                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
349                            )
350                    }))
351                    .flex()
352                    .gap_1()
353                    .items_center()
354                    .relative()
355                    .child(disclosure_control(self.toggle))
356                    .children(left_content)
357                    .child(self.label),
358            )
359    }
360}
361
362#[derive(Clone, Component)]
363pub struct ListSeparator;
364
365impl ListSeparator {
366    pub fn new() -> Self {
367        Self
368    }
369
370    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
371        div().h_px().w_full().bg(cx.theme().colors().border_variant)
372    }
373}
374
375#[derive(Component)]
376pub struct List {
377    items: Vec<ListItem>,
378    /// Message to display when the list is empty
379    /// Defaults to "No items"
380    empty_message: SharedString,
381    header: Option<ListHeader>,
382    toggle: Toggle,
383}
384
385impl List {
386    pub fn new(items: Vec<ListItem>) -> Self {
387        Self {
388            items,
389            empty_message: "No items".into(),
390            header: None,
391            toggle: Toggle::NotToggleable,
392        }
393    }
394
395    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
396        self.empty_message = empty_message.into();
397        self
398    }
399
400    pub fn header(mut self, header: ListHeader) -> Self {
401        self.header = Some(header);
402        self
403    }
404
405    pub fn toggle(mut self, toggle: Toggle) -> Self {
406        self.toggle = toggle;
407        self
408    }
409
410    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
411        let list_content = match (self.items.is_empty(), self.toggle) {
412            (false, _) => div().children(self.items),
413            (true, Toggle::Toggled(false)) => div(),
414            (true, _) => {
415                div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
416            }
417        };
418
419        v_stack()
420            .w_full()
421            .py_1()
422            .children(self.header.map(|header| header))
423            .child(list_content)
424    }
425}