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 label: SharedString,
12 start_slot: Option<AnyElement>,
14 end_slot: Option<AnyElement>,
17 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}