Skip to main content

ui/components/list/
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, FontWeight};
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| this.bg(semantic::active_bg(cx)))
98                    .flex()
99                    .flex_1()
100                    .items_center()
101                    .justify_between()
102                    .w_full()
103                    .gap(DynamicSpacing::Base04.rems(cx))
104                    .child(
105                        h_flex()
106                            .gap(DynamicSpacing::Base04.rems(cx))
107                            .children(self.toggle.map(|is_open| {
108                                Disclosure::new("toggle", is_open)
109                                    .on_toggle_expanded(self.on_toggle.clone())
110                            }))
111                            .child(
112                                div()
113                                    .id("label_container")
114                                    .flex()
115                                    .gap(DynamicSpacing::Base04.rems(cx))
116                                    .items_center()
117                                    .children(self.start_slot)
118                                    .child(
119                                        Label::new(self.label.clone())
120                                            .size(LabelSize::Small)
121                                            .weight(FontWeight::MEDIUM)
122                                            .color(Color::Muted),
123                                    )
124                                    .when_some(self.on_toggle, |this, on_toggle| {
125                                        this.on_click(move |event, window, cx| {
126                                            on_toggle(event, window, cx)
127                                        })
128                                    }),
129                            ),
130                    )
131                    .child(h_flex().children(self.end_slot))
132                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
133                        this.child(
134                            div()
135                                .absolute()
136                                .right_0()
137                                .visible_on_hover("list_header")
138                                .child(end_hover_slot),
139                        )
140                    }),
141            )
142    }
143}
144
145impl Component for ListHeader {
146    fn scope() -> ComponentScope {
147        ComponentScope::DataDisplay
148    }
149
150    fn description() -> Option<&'static str> {
151        Some(
152            "A header component for lists with support for icons, actions, and collapsible sections.",
153        )
154    }
155
156    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
157        Some(
158            v_flex()
159                .gap_6()
160                .children(vec![
161                    example_group_with_title(
162                        "Basic Headers",
163                        vec![
164                            single_example(
165                                "Simple",
166                                ListHeader::new("Section Header").into_any_element(),
167                            ),
168                            single_example(
169                                "With Icon",
170                                ListHeader::new("Files")
171                                    .start_slot(Icon::new(IconName::File))
172                                    .into_any_element(),
173                            ),
174                            single_example(
175                                "With End Slot",
176                                ListHeader::new("Recent")
177                                    .end_slot(Label::new("5").color(Color::Muted))
178                                    .into_any_element(),
179                            ),
180                        ],
181                    ),
182                    example_group_with_title(
183                        "Collapsible Headers",
184                        vec![
185                            single_example(
186                                "Expanded",
187                                ListHeader::new("Expanded Section")
188                                    .toggle(Some(true))
189                                    .into_any_element(),
190                            ),
191                            single_example(
192                                "Collapsed",
193                                ListHeader::new("Collapsed Section")
194                                    .toggle(Some(false))
195                                    .into_any_element(),
196                            ),
197                        ],
198                    ),
199                    example_group_with_title(
200                        "States",
201                        vec![
202                            single_example(
203                                "Selected",
204                                ListHeader::new("Selected Header")
205                                    .toggle_state(true)
206                                    .into_any_element(),
207                            ),
208                            single_example(
209                                "Inset",
210                                ListHeader::new("Inset Header")
211                                    .inset(true)
212                                    .into_any_element(),
213                            ),
214                        ],
215                    ),
216                ])
217                .into_any_element(),
218        )
219    }
220}