gpui_component/
accordion.rs

1use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
2
3use gpui::{
4    div, prelude::FluentBuilder as _, rems, AnyElement, App, ElementId, InteractiveElement as _,
5    IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled,
6    Window,
7};
8
9use crate::{h_flex, v_flex, ActiveTheme as _, Icon, IconName, Sizable, Size};
10
11/// Accordion element.
12#[derive(IntoElement)]
13pub struct Accordion {
14    id: ElementId,
15    multiple: bool,
16    size: Size,
17    bordered: bool,
18    disabled: bool,
19    children: Vec<AccordionItem>,
20    on_toggle_click: Option<Arc<dyn Fn(&[usize], &mut Window, &mut App) + Send + Sync>>,
21}
22
23impl Accordion {
24    /// Create a new Accordion with the given ID.
25    pub fn new(id: impl Into<ElementId>) -> Self {
26        Self {
27            id: id.into(),
28            multiple: false,
29            size: Size::default(),
30            bordered: true,
31            children: Vec::new(),
32            disabled: false,
33            on_toggle_click: None,
34        }
35    }
36
37    /// Set whether multiple accordion items can be opened simultaneously, default: false
38    pub fn multiple(mut self, multiple: bool) -> Self {
39        self.multiple = multiple;
40        self
41    }
42
43    /// Set whether the accordion items have borders, default: true
44    pub fn bordered(mut self, bordered: bool) -> Self {
45        self.bordered = bordered;
46        self
47    }
48
49    /// Set whether the accordion is disabled, default: false
50    pub fn disabled(mut self, disabled: bool) -> Self {
51        self.disabled = disabled;
52        self
53    }
54
55    /// Adds an AccordionItem to the Accordion.
56    pub fn item<F>(mut self, child: F) -> Self
57    where
58        F: FnOnce(AccordionItem) -> AccordionItem,
59    {
60        let item = child(AccordionItem::new());
61        self.children.push(item);
62        self
63    }
64
65    /// Sets the on_toggle_click callback for the AccordionGroup.
66    ///
67    /// The first argument `Vec<usize>` is the indices of the open accordions.
68    pub fn on_toggle_click(
69        mut self,
70        on_toggle_click: impl Fn(&[usize], &mut Window, &mut App) + Send + Sync + 'static,
71    ) -> Self {
72        self.on_toggle_click = Some(Arc::new(on_toggle_click));
73        self
74    }
75}
76
77impl Sizable for Accordion {
78    fn with_size(mut self, size: impl Into<Size>) -> Self {
79        self.size = size.into();
80        self
81    }
82}
83
84impl RenderOnce for Accordion {
85    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
86        let open_ixs = Rc::new(RefCell::new(HashSet::new()));
87        let is_multiple = self.multiple;
88
89        v_flex()
90            .id(self.id)
91            .size_full()
92            .when(self.bordered, |this| this.gap_1())
93            .children(
94                self.children
95                    .into_iter()
96                    .enumerate()
97                    .map(|(ix, accordion)| {
98                        if accordion.open {
99                            open_ixs.borrow_mut().insert(ix);
100                        }
101
102                        accordion
103                            .index(ix)
104                            .with_size(self.size)
105                            .bordered(self.bordered)
106                            .disabled(self.disabled)
107                            .on_toggle_click({
108                                let open_ixs = Rc::clone(&open_ixs);
109                                move |open, _, _| {
110                                    let mut open_ixs = open_ixs.borrow_mut();
111                                    if *open {
112                                        if !is_multiple {
113                                            open_ixs.clear();
114                                        }
115                                        open_ixs.insert(ix);
116                                    } else {
117                                        open_ixs.remove(&ix);
118                                    }
119                                }
120                            })
121                    }),
122            )
123            .when_some(
124                self.on_toggle_click.filter(|_| !self.disabled),
125                move |this, on_toggle_click| {
126                    let open_ixs = Rc::clone(&open_ixs);
127                    this.on_click(move |_, window, cx| {
128                        let open_ixs: Vec<usize> = open_ixs.borrow().iter().map(|&ix| ix).collect();
129
130                        on_toggle_click(&open_ixs, window, cx);
131                    })
132                },
133            )
134    }
135}
136
137/// An Accordion is a vertically stacked list of items, each of which can be expanded to reveal the content associated with it.
138#[derive(IntoElement)]
139pub struct AccordionItem {
140    index: usize,
141    icon: Option<Icon>,
142    title: AnyElement,
143    children: Vec<AnyElement>,
144    open: bool,
145    size: Size,
146    bordered: bool,
147    disabled: bool,
148    on_toggle_click: Option<Arc<dyn Fn(&bool, &mut Window, &mut App)>>,
149}
150
151impl AccordionItem {
152    /// Create a new AccordionItem.
153    pub fn new() -> Self {
154        Self {
155            index: 0,
156            icon: None,
157            title: SharedString::default().into_any_element(),
158            children: Vec::new(),
159            open: false,
160            disabled: false,
161            on_toggle_click: None,
162            size: Size::default(),
163            bordered: true,
164        }
165    }
166
167    /// Set the icon for the accordion item.
168    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
169        self.icon = Some(icon.into());
170        self
171    }
172
173    /// Set the title for the accordion item.
174    pub fn title(mut self, title: impl IntoElement) -> Self {
175        self.title = title.into_any_element();
176        self
177    }
178
179    pub fn bordered(mut self, bordered: bool) -> Self {
180        self.bordered = bordered;
181        self
182    }
183
184    pub fn open(mut self, open: bool) -> Self {
185        self.open = open;
186        self
187    }
188
189    pub fn disabled(mut self, disabled: bool) -> Self {
190        self.disabled = disabled;
191        self
192    }
193
194    fn index(mut self, index: usize) -> Self {
195        self.index = index;
196        self
197    }
198
199    fn on_toggle_click(
200        mut self,
201        on_toggle_click: impl Fn(&bool, &mut Window, &mut App) + 'static,
202    ) -> Self {
203        self.on_toggle_click = Some(Arc::new(on_toggle_click));
204        self
205    }
206}
207
208impl ParentElement for AccordionItem {
209    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
210        self.children.extend(elements);
211    }
212}
213
214impl Sizable for AccordionItem {
215    fn with_size(mut self, size: impl Into<Size>) -> Self {
216        self.size = size.into();
217        self
218    }
219}
220
221impl RenderOnce for AccordionItem {
222    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
223        let text_size = match self.size {
224            Size::XSmall => rems(0.875),
225            Size::Small => rems(0.875),
226            _ => rems(1.0),
227        };
228
229        div().flex_1().child(
230            v_flex()
231                .w_full()
232                .bg(cx.theme().accordion)
233                .overflow_hidden()
234                .when(self.bordered, |this| {
235                    this.border_1()
236                        .rounded(cx.theme().radius)
237                        .border_color(cx.theme().border)
238                })
239                .text_size(text_size)
240                .child(
241                    h_flex()
242                        .id(self.index)
243                        .justify_between()
244                        .gap_3()
245                        .map(|this| match self.size {
246                            Size::XSmall => this.py_0().px_1p5(),
247                            Size::Small => this.py_0p5().px_2(),
248                            Size::Large => this.py_1p5().px_4(),
249                            _ => this.py_1().px_3(),
250                        })
251                        .when(self.open, |this| {
252                            this.when(self.bordered, |this| {
253                                this.text_color(cx.theme().foreground)
254                                    .border_b_1()
255                                    .border_color(cx.theme().border)
256                            })
257                        })
258                        .when(!self.bordered, |this| {
259                            this.border_b_1().border_color(cx.theme().border)
260                        })
261                        .child(
262                            h_flex()
263                                .items_center()
264                                .map(|this| match self.size {
265                                    Size::XSmall => this.gap_1(),
266                                    Size::Small => this.gap_1(),
267                                    _ => this.gap_2(),
268                                })
269                                .when_some(self.icon, |this, icon| {
270                                    this.child(
271                                        icon.with_size(self.size)
272                                            .text_color(cx.theme().muted_foreground),
273                                    )
274                                })
275                                .child(self.title),
276                        )
277                        .when(!self.disabled, |this| {
278                            this.hover(|this| this.bg(cx.theme().accordion_hover))
279                                .child(
280                                    Icon::new(if self.open {
281                                        IconName::ChevronUp
282                                    } else {
283                                        IconName::ChevronDown
284                                    })
285                                    .xsmall()
286                                    .text_color(cx.theme().muted_foreground),
287                                )
288                                .when_some(self.on_toggle_click, |this, on_toggle_click| {
289                                    this.on_click({
290                                        move |_, window, cx| {
291                                            on_toggle_click(&!self.open, window, cx);
292                                        }
293                                    })
294                                })
295                        }),
296                )
297                .when(self.open, |this| {
298                    this.child(
299                        div()
300                            .map(|this| match self.size {
301                                Size::XSmall => this.p_1p5(),
302                                Size::Small => this.p_2(),
303                                Size::Large => this.p_4(),
304                                _ => this.p_3(),
305                            })
306                            .children(self.children),
307                    )
308                }),
309        )
310    }
311}