list.rs

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