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