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#[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 {
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 pub fn multiple(mut self, multiple: bool) -> Self {
39 self.multiple = multiple;
40 self
41 }
42
43 pub fn bordered(mut self, bordered: bool) -> Self {
45 self.bordered = bordered;
46 self
47 }
48
49 pub fn disabled(mut self, disabled: bool) -> Self {
51 self.disabled = disabled;
52 self
53 }
54
55 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 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#[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 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 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
169 self.icon = Some(icon.into());
170 self
171 }
172
173 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}