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