list_header.rs

  1use std::sync::Arc;
  2
  3use crate::{Disclosure, prelude::*};
  4use component::{Component, ComponentScope, example_group_with_title, single_example};
  5use gpui::{AnyElement, ClickEvent};
  6use theme::UiDensity;
  7
  8#[derive(IntoElement, RegisterComponent)]
  9pub struct ListHeader {
 10    /// The label of the header.
 11    label: SharedString,
 12    /// A slot for content that appears before the label, like an icon or avatar.
 13    start_slot: Option<AnyElement>,
 14    /// A slot for content that appears after the label, usually on the other side of the header.
 15    /// This might be a button, a disclosure arrow, a face pile, etc.
 16    end_slot: Option<AnyElement>,
 17    /// A slot for content that appears on hover after the label
 18    /// It will obscure the `end_slot` when visible.
 19    end_hover_slot: Option<AnyElement>,
 20    toggle: Option<bool>,
 21    on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 22    inset: bool,
 23    selected: bool,
 24}
 25
 26impl ListHeader {
 27    pub fn new(label: impl Into<SharedString>) -> Self {
 28        Self {
 29            label: label.into(),
 30            start_slot: None,
 31            end_slot: None,
 32            end_hover_slot: None,
 33            inset: false,
 34            toggle: None,
 35            on_toggle: None,
 36            selected: false,
 37        }
 38    }
 39
 40    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
 41        self.toggle = toggle.into();
 42        self
 43    }
 44
 45    pub fn on_toggle(
 46        mut self,
 47        on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 48    ) -> Self {
 49        self.on_toggle = Some(Arc::new(on_toggle));
 50        self
 51    }
 52
 53    pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
 54        self.start_slot = start_slot.into().map(IntoElement::into_any_element);
 55        self
 56    }
 57
 58    pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
 59        self.end_slot = end_slot.into().map(IntoElement::into_any_element);
 60        self
 61    }
 62
 63    pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
 64        self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
 65        self
 66    }
 67
 68    pub fn inset(mut self, inset: bool) -> Self {
 69        self.inset = inset;
 70        self
 71    }
 72}
 73
 74impl Toggleable for ListHeader {
 75    fn toggle_state(mut self, selected: bool) -> Self {
 76        self.selected = selected;
 77        self
 78    }
 79}
 80
 81impl RenderOnce for ListHeader {
 82    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 83        let ui_density = theme::theme_settings(cx).ui_density(cx);
 84
 85        h_flex()
 86            .id(self.label.clone())
 87            .w_full()
 88            .relative()
 89            .group("list_header")
 90            .child(
 91                div()
 92                    .map(|this| match ui_density {
 93                        UiDensity::Comfortable => this.h_5(),
 94                        _ => this.h_7(),
 95                    })
 96                    .when(self.inset, |this| this.px_2())
 97                    .when(self.selected, |this| {
 98                        this.bg(cx.theme().colors().ghost_element_selected)
 99                    })
100                    .flex()
101                    .flex_1()
102                    .items_center()
103                    .justify_between()
104                    .w_full()
105                    .gap(DynamicSpacing::Base04.rems(cx))
106                    .child(
107                        h_flex()
108                            .gap(DynamicSpacing::Base04.rems(cx))
109                            .children(self.toggle.map(|is_open| {
110                                Disclosure::new("toggle", is_open)
111                                    .on_toggle_expanded(self.on_toggle.clone())
112                            }))
113                            .child(
114                                div()
115                                    .id("label_container")
116                                    .flex()
117                                    .gap(DynamicSpacing::Base04.rems(cx))
118                                    .items_center()
119                                    .children(self.start_slot)
120                                    .child(Label::new(self.label.clone()).color(Color::Muted))
121                                    .when_some(self.on_toggle, |this, on_toggle| {
122                                        this.on_click(move |event, window, cx| {
123                                            on_toggle(event, window, cx)
124                                        })
125                                    }),
126                            ),
127                    )
128                    .child(h_flex().children(self.end_slot))
129                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
130                        this.child(
131                            div()
132                                .absolute()
133                                .right_0()
134                                .visible_on_hover("list_header")
135                                .child(end_hover_slot),
136                        )
137                    }),
138            )
139    }
140}
141
142impl Component for ListHeader {
143    fn scope() -> ComponentScope {
144        ComponentScope::DataDisplay
145    }
146
147    fn description() -> Option<&'static str> {
148        Some(
149            "A header component for lists with support for icons, actions, and collapsible sections.",
150        )
151    }
152
153    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
154        Some(
155            v_flex()
156                .gap_6()
157                .children(vec![
158                    example_group_with_title(
159                        "Basic Headers",
160                        vec![
161                            single_example(
162                                "Simple",
163                                ListHeader::new("Section Header").into_any_element(),
164                            ),
165                            single_example(
166                                "With Icon",
167                                ListHeader::new("Files")
168                                    .start_slot(Icon::new(IconName::File))
169                                    .into_any_element(),
170                            ),
171                            single_example(
172                                "With End Slot",
173                                ListHeader::new("Recent")
174                                    .end_slot(Label::new("5").color(Color::Muted))
175                                    .into_any_element(),
176                            ),
177                        ],
178                    ),
179                    example_group_with_title(
180                        "Collapsible Headers",
181                        vec![
182                            single_example(
183                                "Expanded",
184                                ListHeader::new("Expanded Section")
185                                    .toggle(Some(true))
186                                    .into_any_element(),
187                            ),
188                            single_example(
189                                "Collapsed",
190                                ListHeader::new("Collapsed Section")
191                                    .toggle(Some(false))
192                                    .into_any_element(),
193                            ),
194                        ],
195                    ),
196                    example_group_with_title(
197                        "States",
198                        vec![
199                            single_example(
200                                "Selected",
201                                ListHeader::new("Selected Header")
202                                    .toggle_state(true)
203                                    .into_any_element(),
204                            ),
205                            single_example(
206                                "Inset",
207                                ListHeader::new("Inset Header")
208                                    .inset(true)
209                                    .into_any_element(),
210                            ),
211                        ],
212                    ),
213                ])
214                .into_any_element(),
215        )
216    }
217}