Skip to main content

guise/data/
accordion.rs

1//! `Accordion` — collapsible sections (gpui entity).
2
3use gpui::prelude::*;
4use gpui::{div, px, App, Context, IntoElement, SharedString, Window};
5
6use super::Content;
7use crate::theme::{theme, Size};
8
9struct AccItem {
10    label: SharedString,
11    content: Content,
12}
13
14/// A set of collapsible panels. Create with
15/// `cx.new(|cx| Accordion::new(cx).item("Title", |_, _| ...))`.
16pub struct Accordion {
17    items: Vec<AccItem>,
18    open: Vec<bool>,
19    multiple: bool,
20}
21
22impl Accordion {
23    pub fn new(_cx: &mut Context<Self>) -> Self {
24        Accordion {
25            items: Vec::new(),
26            open: Vec::new(),
27            multiple: false,
28        }
29    }
30
31    /// Add a panel. `content` is rebuilt each render.
32    pub fn item<E>(
33        mut self,
34        label: impl Into<SharedString>,
35        content: impl Fn(&mut Window, &mut App) -> E + 'static,
36    ) -> Self
37    where
38        E: IntoElement,
39    {
40        self.items.push(AccItem {
41            label: label.into(),
42            content: Box::new(move |window, cx| content(window, cx).into_any_element()),
43        });
44        self.open.push(false);
45        self
46    }
47
48    /// Allow multiple panels to be open at once (default: only one).
49    pub fn multiple(mut self, multiple: bool) -> Self {
50        self.multiple = multiple;
51        self
52    }
53
54    /// Open the panel at `index` initially.
55    pub fn default_open(mut self, index: usize) -> Self {
56        if let Some(slot) = self.open.get_mut(index) {
57            *slot = true;
58        }
59        self
60    }
61}
62
63impl Render for Accordion {
64    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
65        let t = theme(cx);
66        let text = t.text().hsla();
67        let dimmed = t.dimmed().hsla();
68        let line = t.border().hsla();
69        let surface_hover = t.surface_hover().hsla();
70        let radius = t.radius(Size::Md);
71        let font = t.font_size(Size::Md);
72        let font_sm = t.font_size(Size::Sm);
73
74        let mut root = div().flex().flex_col().gap(px(8.0));
75
76        for (i, item) in self.items.iter().enumerate() {
77            let is_open = self.open.get(i).copied().unwrap_or(false);
78
79            let header = div()
80                .id(("guise-accordion-header", i))
81                .flex()
82                .items_center()
83                .justify_between()
84                .px(px(14.0))
85                .py(px(12.0))
86                .text_size(px(font))
87                .text_color(text)
88                .hover(move |s| s.bg(surface_hover))
89                .child(item.label.clone())
90                .child(
91                    div()
92                        .text_color(dimmed)
93                        .child(SharedString::new_static(if is_open {
94                            "\u{25be}"
95                        } else {
96                            "\u{25b8}"
97                        })),
98                )
99                .on_click(cx.listener(move |this, _ev, _window, cx| {
100                    let was = this.open.get(i).copied().unwrap_or(false);
101                    if !this.multiple {
102                        this.open.iter_mut().for_each(|o| *o = false);
103                    }
104                    if let Some(slot) = this.open.get_mut(i) {
105                        *slot = !was;
106                    }
107                    cx.notify();
108                }));
109
110            let mut panel = div()
111                .flex()
112                .flex_col()
113                .rounded(px(radius))
114                .border_1()
115                .border_color(line)
116                .child(header);
117
118            if is_open {
119                let body = (item.content)(window, cx);
120                panel = panel.child(
121                    div()
122                        .px(px(14.0))
123                        .pb(px(12.0))
124                        .text_size(px(font_sm))
125                        .text_color(dimmed)
126                        .child(body),
127                );
128            }
129
130            root = root.child(panel);
131        }
132
133        root
134    }
135}