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::devtools::ProbedAny;
30use crate::icon::IconName;
31use crate::paper::apply_shadow;
32use crate::theme::{theme, Size};
33
34type ToggleHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
35
36#[derive(IntoElement)]
45pub struct Panel {
46 id: Option<ElementId>,
47 title: Option<SharedString>,
48 description: Option<SharedString>,
49 icon: Option<AnyElement>,
50 actions: Vec<AnyElement>,
51 footer: Option<AnyElement>,
52 children: Vec<AnyElement>,
53 padding: Size,
54 radius: Option<Size>,
55 with_border: bool,
56 shadow: Option<Size>,
57 collapsible: bool,
58 collapsed: bool,
59 on_toggle: Option<ToggleHandler>,
60}
61
62impl Panel {
63 pub fn new() -> Self {
64 Panel {
65 id: None,
66 title: None,
67 description: None,
68 icon: None,
69 actions: Vec::new(),
70 footer: None,
71 children: Vec::new(),
72 padding: Size::Lg,
73 radius: Some(Size::Md),
74 with_border: true,
75 shadow: Some(Size::Sm),
76 collapsible: false,
77 collapsed: false,
78 on_toggle: None,
79 }
80 }
81
82 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
85 self.id = Some(id.into());
86 self
87 }
88
89 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
91 self.title = Some(title.into());
92 self
93 }
94
95 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
97 self.description = Some(description.into());
98 self
99 }
100
101 pub fn icon(mut self, icon: impl IntoElement) -> Self {
103 self.icon = Some(icon.into_any_element());
104 self
105 }
106
107 pub fn action(mut self, action: impl IntoElement) -> Self {
109 self.actions.push(action.into_any_element());
110 self
111 }
112
113 pub fn actions(mut self, actions: Vec<AnyElement>) -> Self {
115 self.actions = actions;
116 self
117 }
118
119 pub fn footer(mut self, footer: impl IntoElement) -> Self {
121 self.footer = Some(footer.into_any_element());
122 self
123 }
124
125 pub fn padding(mut self, padding: Size) -> Self {
126 self.padding = padding;
127 self
128 }
129
130 pub fn radius(mut self, radius: Size) -> Self {
131 self.radius = Some(radius);
132 self
133 }
134
135 pub fn with_border(mut self, with_border: bool) -> Self {
136 self.with_border = with_border;
137 self
138 }
139
140 pub fn shadow(mut self, shadow: Size) -> Self {
141 self.shadow = Some(shadow);
142 self
143 }
144
145 pub fn collapsible(mut self) -> Self {
148 self.collapsible = true;
149 self
150 }
151
152 pub fn collapsed(mut self, collapsed: bool) -> Self {
154 self.collapsed = collapsed;
155 self
156 }
157
158 pub fn on_toggle(
161 mut self,
162 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
163 ) -> Self {
164 self.on_toggle = Some(Rc::new(handler));
165 self
166 }
167}
168
169impl Default for Panel {
170 fn default() -> Self {
171 Panel::new()
172 }
173}
174
175impl ParentElement for Panel {
176 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
177 self.children.extend(elements);
178 }
179}
180
181impl RenderOnce for Panel {
182 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
183 let t = theme(cx);
184 let radius = t.radius(self.radius.unwrap_or(Size::Md));
185 let pad = t.spacing(self.padding);
186 let border = t.border().hsla();
187 let text = t.text().hsla();
188 let dimmed = t.dimmed().hsla();
189 let font_md = t.font_size(Size::Md);
190 let font_sm = t.font_size(Size::Sm);
191 let surface = t.surface().hsla();
192
193 let body_visible = !(self.collapsible && self.collapsed);
194 let has_header = self.title.is_some()
195 || self.description.is_some()
196 || self.icon.is_some()
197 || !self.actions.is_empty()
198 || self.collapsible;
199 let has_body = !self.children.is_empty();
200
201 let mut root = div().flex().flex_col().bg(surface).rounded(px(radius));
202 if self.with_border {
203 root = root.border_1().border_color(border);
204 }
205 root = apply_shadow(root, self.shadow);
206
207 if has_header {
208 let mut left = div().flex().items_center().gap(px(10.0));
209
210 if self.collapsible {
211 let chevron = if self.collapsed {
212 IconName::ChevronRight
213 } else {
214 IconName::ChevronDown
215 };
216 let label = if self.collapsed { "Expand" } else { "Collapse" };
217 let mut toggle = ActionIcon::new("guise-panel-toggle", chevron)
218 .label(label)
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 let element = match self.id {
286 Some(id) => root.id(id).into_any_element(),
287 None => root.into_any_element(),
288 };
289
290 element.probe_any("Panel")
291 }
292}