Skip to main content

gpui_base/
collapsible.rs

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/// An unstyled controlled region whose content can be expanded or collapsed.
13#[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    /// Keeps content mounted and reveals it at normalized `progress`.
43    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}