1use gpui::{
2 AnyElement, App, Div, ElementId, IntoElement, ParentElement, RenderOnce, Styled, Window, div,
3};
4
5use crate::MotionReveal;
6
7enum Child {
8 Element(AnyElement),
9 Content(AnyElement),
10}
11
12#[derive(IntoElement)]
14pub struct Collapsible {
15 base: Div,
16 children: Vec<Child>,
17 open: bool,
18 reveal: Option<(ElementId, f32)>,
19}
20
21impl Collapsible {
22 pub fn new() -> Self {
23 Self {
24 base: div(),
25 children: Vec::new(),
26 open: false,
27 reveal: None,
28 }
29 }
30
31 pub fn open(mut self, open: bool) -> Self {
32 self.open = open;
33 self
34 }
35
36 pub fn content(mut self, content: impl IntoElement) -> Self {
37 self.children
38 .push(Child::Content(content.into_any_element()));
39 self
40 }
41
42 pub fn reveal(mut self, id: impl Into<ElementId>, progress: f32) -> Self {
44 self.reveal = Some((id.into(), progress));
45 self
46 }
47}
48
49impl Default for Collapsible {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl Styled for Collapsible {
56 fn style(&mut self) -> &mut gpui::StyleRefinement {
57 self.base.style()
58 }
59}
60
61impl ParentElement for Collapsible {
62 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
63 self.children
64 .extend(elements.into_iter().map(Child::Element));
65 }
66}
67
68impl RenderOnce for Collapsible {
69 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
70 self.base
71 .children(self.children.into_iter().filter_map(|child| match child {
72 Child::Element(element) => Some(element),
73 Child::Content(content) => match &self.reveal {
74 Some((id, progress)) => {
75 Some(MotionReveal::new(id.clone(), *progress, content).into_any_element())
76 }
77 None => self.open.then_some(content),
78 },
79 }))
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use gpui::{Context, InteractiveElement as _, Render, TestAppContext, px};
86
87 use super::*;
88
89 struct Harness(bool);
90
91 impl Render for Harness {
92 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
93 Collapsible::new()
94 .open(self.0)
95 .child(div().debug_selector(|| "trigger".into()).size(px(10.)))
96 .content(div().debug_selector(|| "content".into()).size(px(10.)))
97 }
98 }
99
100 #[gpui::test]
101 fn content_is_only_rendered_while_open(cx: &mut TestAppContext) {
102 let (_, cx) = cx.add_window_view(|_, _| Harness(false));
103 cx.update(|window, cx| window.draw(cx).clear(cx));
104 assert!(cx.debug_bounds("trigger").is_some());
105 assert!(cx.debug_bounds("content").is_none());
106
107 let (_, cx) = cx.add_window_view(|_, _| Harness(true));
108 cx.update(|window, cx| window.draw(cx).clear(cx));
109 assert!(cx.debug_bounds("trigger").is_some());
110 assert!(cx.debug_bounds("content").is_some());
111 }
112}