Skip to main content

liora_components/
collapse.rs

1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{AnyElement, Context, IntoElement, Render, SharedString, Window, div, prelude::*, px};
4use liora_core::{Config, unique_id};
5use liora_icons::Icon;
6use liora_icons_lucide::IconName;
7use std::collections::HashSet;
8use std::sync::Arc;
9
10pub struct CollapseItem {
11    pub name: SharedString,
12    pub title: SharedString,
13    pub content: Arc<dyn Fn(&mut Window, &mut Context<Collapse>) -> AnyElement + 'static>,
14}
15
16pub struct Collapse {
17    items: Vec<CollapseItem>,
18    active_names: HashSet<SharedString>,
19    accordion: bool,
20    id: SharedString,
21}
22
23impl Collapse {
24    pub fn new() -> Self {
25        Self {
26            items: vec![],
27            active_names: HashSet::new(),
28            accordion: false,
29            id: unique_id("collapse"),
30        }
31    }
32
33    pub fn accordion(mut self) -> Self {
34        self.accordion = true;
35        self
36    }
37
38    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
39        self.id = id.into();
40        self
41    }
42
43    pub fn item<F, E>(
44        mut self,
45        name: impl Into<SharedString>,
46        title: impl Into<SharedString>,
47        f: F,
48    ) -> Self
49    where
50        F: Fn(&mut Window, &mut Context<Self>) -> E + 'static,
51        E: IntoElement,
52    {
53        self.items.push(CollapseItem {
54            name: name.into(),
55            title: title.into(),
56            content: Arc::new(move |window, cx| f(window, cx).into_any_element()),
57        });
58        self
59    }
60
61    fn toggle(&mut self, name: SharedString, cx: &mut Context<Self>) {
62        if self.active_names.contains(&name) {
63            self.active_names.remove(&name);
64        } else {
65            if self.accordion {
66                self.active_names.clear();
67            }
68            self.active_names.insert(name);
69        }
70        cx.notify();
71    }
72}
73
74impl Render for Collapse {
75    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
76        let theme = cx.global::<Config>().theme.clone();
77
78        div()
79            .flex()
80            .flex_col()
81            .border_1()
82            .border_color(theme.neutral.border)
83            .rounded(px(theme.radius.md))
84            .children(self.items.iter().enumerate().map(|(i, item)| {
85                let name = item.name.clone();
86                let is_active = self.active_names.contains(&name);
87                let is_last = i == self.items.len() - 1;
88                let header_id = format!("{}-header-{}", self.id, name);
89                let content_motion_id = format!("{}-content-motion-{}", self.id, name);
90
91                div()
92                    .flex()
93                    .flex_col()
94                    .child(
95                        div()
96                            .id(element_id(header_id))
97                            .cursor_pointer()
98                            .px_4()
99                            .py_3()
100                            .flex()
101                            .flex_row()
102                            .items_center()
103                            .justify_between()
104                            .bg(if is_active {
105                                theme.neutral.hover
106                            } else {
107                                theme.neutral.card
108                            })
109                            .hover(|s| s.bg(theme.neutral.hover))
110                            .when(!is_last, |s| {
111                                s.border_b_1().border_color(theme.neutral.border)
112                            })
113                            .on_click(cx.listener(move |this, _, _, cx| {
114                                this.toggle(name.clone(), cx);
115                            }))
116                            .child(
117                                div()
118                                    .font_weight(gpui::FontWeight::BOLD)
119                                    .child(item.title.clone()),
120                            )
121                            .child(
122                                Icon::new(if is_active {
123                                    IconName::ChevronDown
124                                } else {
125                                    IconName::ChevronRight
126                                })
127                                .size(px(16.0))
128                                .color(theme.neutral.icon),
129                            ),
130                    )
131                    .when(is_active, |s| {
132                        s.child(pop_in(
133                            element_id(content_motion_id),
134                            div()
135                                .p_4()
136                                .bg(theme.neutral.card)
137                                .when(!is_last, |s| {
138                                    s.border_b_1().border_color(theme.neutral.border)
139                                })
140                                .child((item.content)(_window, cx)),
141                        ))
142                    })
143            }))
144    }
145}