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 {
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 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#[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}