1use std::collections::HashSet;
9
10use gpui::{
11 px, AnyElement, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
12 SharedString, Styled, Window,
13};
14use herogpui_core::element_id;
15use herogpui_theme::ActiveTheme;
16
17use crate::a11y::{self, A11y as _};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct DisclosureRenderState {
23 pub is_expanded: bool,
25 pub is_disabled: bool,
27}
28
29type DisclosureContent = std::sync::Arc<dyn Fn(DisclosureRenderState) -> AnyElement + 'static>;
30
31#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
33#[derive(IntoElement)]
34pub struct Disclosure {
35 id: ElementId,
36 title: SharedString,
37 is_expanded: Option<bool>,
38 default_expanded: bool,
39 is_disabled: bool,
40 children: Vec<AnyElement>,
41 content: Option<DisclosureContent>,
42 on_toggle: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
43 sx: Option<Box<gpui::StyleRefinement>>,
45}
46
47impl Disclosure {
48 pub fn on_expanded_change(
50 mut self,
51 handler: impl Fn(&bool, &mut Window, &mut App) + 'static,
52 ) -> Self {
53 self.on_toggle = Some(std::sync::Arc::new(handler));
54 self
55 }
56
57 pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
59 Self {
60 id: id.into(),
61 title: title.into(),
62 is_expanded: None,
63 default_expanded: false,
64 is_disabled: false,
65 children: Vec::new(),
66 content: None,
67 on_toggle: None,
68 sx: None,
69 }
70 }
71
72 pub fn is_expanded(mut self, v: bool) -> Self {
74 self.is_expanded = Some(v);
75 self
76 }
77
78 pub fn default_expanded(mut self, v: bool) -> Self {
80 self.default_expanded = v;
81 self
82 }
83
84 pub fn is_disabled(mut self, v: bool) -> Self {
86 self.is_disabled = v;
87 self
88 }
89
90 pub fn content(
93 mut self,
94 render: impl Fn(DisclosureRenderState) -> AnyElement + 'static,
95 ) -> Self {
96 self.content = Some(std::sync::Arc::new(render));
97 self
98 }
99
100 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
105 crate::util::refine_sx(&mut self.sx, style);
106 self
107 }
108}
109
110impl ParentElement for Disclosure {
111 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
112 self.children.extend(elements);
113 }
114}
115
116impl RenderOnce for Disclosure {
117 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
118 let (expanded, expanded_own) = crate::util::controlled(
125 window,
126 cx,
127 element_id::scoped(&self.id, "expanded"),
128 self.is_expanded,
129 self.default_expanded,
130 );
131 let cb = self.on_toggle.clone();
132 let indicator_id = element_id::scoped(&self.id, "indicator");
133 let children = match self.content {
134 Some(render) => vec![render(DisclosureRenderState {
135 is_expanded: expanded,
136 is_disabled: self.is_disabled,
137 })],
138 None => self.children,
139 };
140 let trigger = crate::button::Button::new(element_id::scoped(&self.id, "trigger"))
143 .variant(if expanded {
144 herogpui_core::Variant::Secondary
145 } else {
146 herogpui_core::Variant::Tertiary
147 })
148 .label(self.title.clone())
149 .is_disabled(self.is_disabled)
150 .child(crate::anim::rotating_indicator(
153 &indicator_id,
154 expanded,
155 gpui::svg()
156 .size(px(16.))
157 .path(crate::icons::CHEVRON_DOWN)
158 .flex_shrink_0()
159 .text_color(cx.colors().muted),
160 window,
161 cx,
162 ))
163 .on_press(move |_, w, cx| {
164 if let Some(held) = &expanded_own {
165 held.update(cx, |expanded, cx| {
166 *expanded = !*expanded;
167 cx.notify();
168 });
169 }
170 if let Some(f) = &cb {
171 f(&!expanded, w, cx);
172 }
173 });
174
175 let mut el = gpui::div()
176 .id(self.id.clone())
177 .relative()
178 .flex()
179 .flex_col()
180 .child(trigger);
181 let panel_id = element_id::scoped(&self.id, "panel");
185 let panel = gpui::div()
186 .id(panel_id.clone())
193 .a11y_named(a11y::Role::Group, &a11y::Name::labelled(self.title.clone()));
194 let body = gpui::div()
195 .p(px(8.))
197 .flex()
198 .flex_col()
199 .gap(px(6.))
200 .children(children)
201 .into_any_element();
202 if let Some(panel) =
203 crate::anim::collapsible_panel(&panel_id, expanded, panel, body, window, cx)
204 {
205 el = el.child(panel);
206 }
207 crate::util::apply_sx(el, &self.sx).into_any_element()
208 }
209}
210
211#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
213#[derive(IntoElement)]
214pub struct DisclosureGroup {
215 id: ElementId,
216 items: Vec<(SharedString, SharedString, Vec<AnyElement>)>,
217 expanded: Option<HashSet<SharedString>>,
218 default_expanded: HashSet<SharedString>,
219 allows_multiple_expanded: bool,
220 is_disabled: bool,
221 on_expanded_change:
222 Option<std::sync::Arc<dyn Fn(&HashSet<SharedString>, &mut Window, &mut App) + 'static>>,
223 sx: Option<Box<gpui::StyleRefinement>>,
225}
226
227impl DisclosureGroup {
228 pub fn new(id: impl Into<ElementId>) -> Self {
230 Self {
231 id: id.into(),
232 items: Vec::new(),
233 expanded: None,
234 default_expanded: HashSet::new(),
235 allows_multiple_expanded: false,
236 is_disabled: false,
237 on_expanded_change: None,
238 sx: None,
239 }
240 }
241
242 pub fn item(
244 mut self,
245 key: impl Into<SharedString>,
246 title: impl Into<SharedString>,
247 content: impl IntoElement,
248 ) -> Self {
249 self.items
250 .push((key.into(), title.into(), vec![content.into_any_element()]));
251 self
252 }
253
254 pub fn expanded_keys(
256 mut self,
257 keys: impl IntoIterator<Item = impl Into<SharedString>>,
258 ) -> Self {
259 self.expanded = Some(keys.into_iter().map(Into::into).collect());
260 self
261 }
262
263 pub fn default_expanded_keys(
265 mut self,
266 keys: impl IntoIterator<Item = impl Into<SharedString>>,
267 ) -> Self {
268 self.default_expanded = keys.into_iter().map(Into::into).collect();
269 self
270 }
271
272 pub fn allows_multiple_expanded(mut self, v: bool) -> Self {
274 self.allows_multiple_expanded = v;
275 self
276 }
277
278 pub fn is_disabled(mut self, v: bool) -> Self {
280 self.is_disabled = v;
281 self
282 }
283
284 pub fn on_expanded_change(
286 mut self,
287 f: impl Fn(&HashSet<SharedString>, &mut Window, &mut App) + 'static,
288 ) -> Self {
289 self.on_expanded_change = Some(std::sync::Arc::new(f));
290 self
291 }
292
293 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
298 crate::util::refine_sx(&mut self.sx, style);
299 self
300 }
301}
302
303impl RenderOnce for DisclosureGroup {
304 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
305 let (mut expanded, expanded_own) = crate::util::controlled(
306 window,
307 cx,
308 element_id::scoped(&self.id, "expanded"),
309 self.expanded,
310 self.default_expanded,
311 );
312 if !self.allows_multiple_expanded && expanded_own.is_some() && expanded.len() > 1 {
313 let retained = self
314 .items
315 .iter()
316 .find_map(|(key, _, _)| expanded.contains(key).then(|| key.clone()));
317 let next = retained.into_iter().collect::<HashSet<_>>();
318 if let Some(held) = &expanded_own {
319 let held_next = next.clone();
320 held.update(cx, |expanded, cx| {
321 *expanded = held_next;
322 cx.notify();
323 });
324 }
325 if let Some(callback) = self.on_expanded_change.clone() {
326 let reported = next.clone();
327 window.defer(cx, move |window, cx| callback(&reported, window, cx));
328 }
329 expanded = next;
330 }
331 let mut el = gpui::div().id(self.id.clone()).w_full().flex().flex_col();
336 for (key, title, children) in self.items {
337 let is_expanded = expanded.contains(&key);
338 let mut disclosure = Disclosure::new(key.clone(), title)
339 .is_expanded(is_expanded)
340 .is_disabled(self.is_disabled)
341 .children(children);
342 if expanded_own.is_some() || self.on_expanded_change.is_some() {
343 let current = expanded.clone();
344 let own = expanded_own.clone();
345 let cb = self.on_expanded_change.clone();
346 let allows_multiple = self.allows_multiple_expanded;
347 disclosure = disclosure.on_expanded_change(move |_next, window, cx| {
348 let next = crate::accordion::next_expanded(¤t, &key, allows_multiple);
349 if let Some(held) = &own {
350 let held_next = next.clone();
351 held.update(cx, |expanded, cx| {
352 *expanded = held_next;
353 cx.notify();
354 });
355 }
356 if let Some(f) = &cb {
357 f(&next, window, cx);
358 }
359 });
360 }
361 el = el.child(disclosure);
362 }
363 crate::util::apply_sx(el, &self.sx).into_any_element()
364 }
365}
366
367crate::util::impl_component_styled!(Disclosure, DisclosureGroup);