1use std::rc::Rc;
22
23use gpui::prelude::*;
24use gpui::{
25 div, px, AnyElement, App, ClickEvent, ElementId, FontWeight, IntoElement, SharedString, Window,
26};
27
28use crate::actionicon::ActionIcon;
29use crate::icon::IconName;
30use crate::paper::apply_shadow;
31use crate::theme::{theme, Size};
32
33type ToggleHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
34
35#[derive(IntoElement)]
44pub struct Panel {
45 id: Option<ElementId>,
46 title: Option<SharedString>,
47 description: Option<SharedString>,
48 icon: Option<AnyElement>,
49 actions: Vec<AnyElement>,
50 footer: Option<AnyElement>,
51 children: Vec<AnyElement>,
52 padding: Size,
53 radius: Option<Size>,
54 with_border: bool,
55 shadow: Option<Size>,
56 collapsible: bool,
57 collapsed: bool,
58 on_toggle: Option<ToggleHandler>,
59}
60
61impl Panel {
62 pub fn new() -> Self {
63 Panel {
64 id: None,
65 title: None,
66 description: None,
67 icon: None,
68 actions: Vec::new(),
69 footer: None,
70 children: Vec::new(),
71 padding: Size::Lg,
72 radius: Some(Size::Md),
73 with_border: true,
74 shadow: Some(Size::Sm),
75 collapsible: false,
76 collapsed: false,
77 on_toggle: None,
78 }
79 }
80
81 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
84 self.id = Some(id.into());
85 self
86 }
87
88 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
90 self.title = Some(title.into());
91 self
92 }
93
94 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
96 self.description = Some(description.into());
97 self
98 }
99
100 pub fn icon(mut self, icon: impl IntoElement) -> Self {
102 self.icon = Some(icon.into_any_element());
103 self
104 }
105
106 pub fn action(mut self, action: impl IntoElement) -> Self {
108 self.actions.push(action.into_any_element());
109 self
110 }
111
112 pub fn actions(mut self, actions: Vec<AnyElement>) -> Self {
114 self.actions = actions;
115 self
116 }
117
118 pub fn footer(mut self, footer: impl IntoElement) -> Self {
120 self.footer = Some(footer.into_any_element());
121 self
122 }
123
124 pub fn padding(mut self, padding: Size) -> Self {
125 self.padding = padding;
126 self
127 }
128
129 pub fn radius(mut self, radius: Size) -> Self {
130 self.radius = Some(radius);
131 self
132 }
133
134 pub fn with_border(mut self, with_border: bool) -> Self {
135 self.with_border = with_border;
136 self
137 }
138
139 pub fn shadow(mut self, shadow: Size) -> Self {
140 self.shadow = Some(shadow);
141 self
142 }
143
144 pub fn collapsible(mut self) -> Self {
147 self.collapsible = true;
148 self
149 }
150
151 pub fn collapsed(mut self, collapsed: bool) -> Self {
153 self.collapsed = collapsed;
154 self
155 }
156
157 pub fn on_toggle(
160 mut self,
161 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
162 ) -> Self {
163 self.on_toggle = Some(Rc::new(handler));
164 self
165 }
166}
167
168impl Default for Panel {
169 fn default() -> Self {
170 Panel::new()
171 }
172}
173
174impl ParentElement for Panel {
175 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
176 self.children.extend(elements);
177 }
178}
179
180impl RenderOnce for Panel {
181 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
182 let t = theme(cx);
183 let radius = t.radius(self.radius.unwrap_or(Size::Md));
184 let pad = t.spacing(self.padding);
185 let border = t.border().hsla();
186 let text = t.text().hsla();
187 let dimmed = t.dimmed().hsla();
188 let font_md = t.font_size(Size::Md);
189 let font_sm = t.font_size(Size::Sm);
190 let surface = t.surface().hsla();
191
192 let body_visible = !(self.collapsible && self.collapsed);
193 let has_header = self.title.is_some()
194 || self.description.is_some()
195 || self.icon.is_some()
196 || !self.actions.is_empty()
197 || self.collapsible;
198 let has_body = !self.children.is_empty();
199
200 let mut root = div().flex().flex_col().bg(surface).rounded(px(radius));
201 if self.with_border {
202 root = root.border_1().border_color(border);
203 }
204 root = apply_shadow(root, self.shadow);
205
206 if has_header {
207 let mut left = div().flex().items_center().gap(px(10.0));
208
209 if self.collapsible {
210 let chevron = if self.collapsed {
211 IconName::ChevronRight
212 } else {
213 IconName::ChevronDown
214 };
215 let mut toggle = ActionIcon::new(
216 "guise-panel-toggle",
217 SharedString::new_static(chevron.glyph()),
218 )
219 .size(Size::Sm);
220 if let Some(handler) = self.on_toggle.clone() {
221 toggle = toggle.on_click(move |ev, window, cx| handler(ev, window, cx));
222 }
223 left = left.child(toggle);
224 }
225 if let Some(icon) = self.icon {
226 left = left.child(icon);
227 }
228
229 let mut heading = div().flex().flex_col().gap(px(2.0));
230 if let Some(title) = self.title {
231 heading = heading.child(
232 div()
233 .text_size(px(font_md))
234 .font_weight(FontWeight::SEMIBOLD)
235 .text_color(text)
236 .child(title),
237 );
238 }
239 if let Some(description) = self.description {
240 heading = heading.child(
241 div()
242 .text_size(px(font_sm))
243 .text_color(dimmed)
244 .child(description),
245 );
246 }
247 left = left.child(heading);
248
249 let mut header = div()
250 .flex()
251 .items_center()
252 .justify_between()
253 .gap(px(pad))
254 .px(px(pad))
255 .py(px(pad * 0.75));
256 if body_visible && (has_body || self.footer.is_some()) {
257 header = header.border_b_1().border_color(border);
258 }
259 header = header.child(left);
260 if !self.actions.is_empty() {
261 header = header.child(
262 div()
263 .flex()
264 .items_center()
265 .gap(px(8.0))
266 .children(self.actions),
267 );
268 }
269 root = root.child(header);
270 }
271
272 if body_visible {
273 if has_body {
274 root = root.child(div().flex().flex_col().p(px(pad)).children(self.children));
275 }
276 if let Some(footer) = self.footer {
277 let mut foot = div().px(px(pad)).py(px(pad * 0.75));
278 if has_body {
279 foot = foot.border_t_1().border_color(border);
280 }
281 root = root.child(foot.child(footer));
282 }
283 }
284
285 match self.id {
286 Some(id) => root.id(id).into_any_element(),
287 None => root.into_any_element(),
288 }
289 }
290}