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}