1use crate::{IconButtonShape, prelude::*};
2
3use gpui::{prelude::FluentBuilder, *};
4use smallvec::SmallVec;
5use theme::ActiveTheme;
6
7#[derive(IntoElement)]
8pub struct Modal {
9 id: ElementId,
10 header: ModalHeader,
11 children: SmallVec<[AnyElement; 2]>,
12 footer: Option<ModalFooter>,
13 container_id: ElementId,
14 container_scroll_handler: Option<ScrollHandle>,
15}
16
17impl Modal {
18 pub fn new(id: impl Into<SharedString>, scroll_handle: Option<ScrollHandle>) -> Self {
19 let id = id.into();
20
21 let container_id = ElementId::Name(format!("{}_container", id).into());
22 Self {
23 id: ElementId::Name(id),
24 header: ModalHeader::new(),
25 children: SmallVec::new(),
26 footer: None,
27 container_id,
28 container_scroll_handler: scroll_handle,
29 }
30 }
31
32 pub fn header(mut self, header: ModalHeader) -> Self {
33 self.header = header;
34 self
35 }
36
37 pub fn section(mut self, section: Section) -> Self {
38 self.children.push(section.into_any_element());
39 self
40 }
41
42 pub fn footer(mut self, footer: ModalFooter) -> Self {
43 self.footer = Some(footer);
44 self
45 }
46
47 pub fn show_dismiss(mut self, show: bool) -> Self {
48 self.header.show_dismiss_button = show;
49 self
50 }
51
52 pub fn show_back(mut self, show: bool) -> Self {
53 self.header.show_back_button = show;
54 self
55 }
56}
57
58impl ParentElement for Modal {
59 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
60 self.children.extend(elements)
61 }
62}
63
64impl RenderOnce for Modal {
65 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
66 v_flex()
67 .id(self.id.clone())
68 .size_full()
69 .flex_1()
70 .overflow_hidden()
71 .child(self.header)
72 .child(
73 v_flex()
74 .id(self.container_id.clone())
75 .w_full()
76 .flex_1()
77 .gap(DynamicSpacing::Base08.rems(cx))
78 .when_some(
79 self.container_scroll_handler,
80 |this, container_scroll_handle| {
81 this.overflow_y_scroll()
82 .track_scroll(&container_scroll_handle)
83 },
84 )
85 .children(self.children),
86 )
87 .children(self.footer)
88 }
89}
90
91#[derive(IntoElement)]
92pub struct ModalHeader {
93 icon: Option<Icon>,
94 headline: Option<SharedString>,
95 description: Option<SharedString>,
96 children: SmallVec<[AnyElement; 2]>,
97 show_dismiss_button: bool,
98 show_back_button: bool,
99}
100
101impl Default for ModalHeader {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl ModalHeader {
108 pub fn new() -> Self {
109 Self {
110 icon: None,
111 headline: None,
112 description: None,
113 children: SmallVec::new(),
114 show_dismiss_button: false,
115 show_back_button: false,
116 }
117 }
118
119 pub fn icon(mut self, icon: Icon) -> Self {
120 self.icon = Some(icon);
121 self
122 }
123
124 /// Set the headline of the modal.
125 ///
126 /// This will insert the headline as the first item
127 /// of `children` if it is not already present.
128 pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
129 self.headline = Some(headline.into());
130 self
131 }
132
133 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
134 self.description = Some(description.into());
135 self
136 }
137
138 pub fn show_dismiss_button(mut self, show: bool) -> Self {
139 self.show_dismiss_button = show;
140 self
141 }
142
143 pub fn show_back_button(mut self, show: bool) -> Self {
144 self.show_back_button = show;
145 self
146 }
147}
148
149impl ParentElement for ModalHeader {
150 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
151 self.children.extend(elements)
152 }
153}
154
155impl RenderOnce for ModalHeader {
156 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
157 let mut children = self.children;
158
159 if let Some(headline) = self.headline {
160 children.insert(
161 0,
162 Headline::new(headline)
163 .size(HeadlineSize::Small)
164 .color(Color::Muted)
165 .into_any_element(),
166 );
167 }
168
169 h_flex()
170 .min_w_0()
171 .flex_none()
172 .justify_between()
173 .w_full()
174 .px(DynamicSpacing::Base12.rems(cx))
175 .pt(DynamicSpacing::Base08.rems(cx))
176 .pb(DynamicSpacing::Base04.rems(cx))
177 .gap(DynamicSpacing::Base08.rems(cx))
178 .when(self.show_back_button, |this| {
179 this.child(
180 IconButton::new("back", IconName::ArrowLeft)
181 .shape(IconButtonShape::Square)
182 .on_click(|_, window, cx| {
183 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
184 }),
185 )
186 })
187 .child(
188 v_flex()
189 .min_w_0()
190 .flex_1()
191 .child(
192 h_flex()
193 .w_full()
194 .gap_1()
195 .justify_between()
196 .child(
197 h_flex()
198 .gap_1()
199 .when_some(self.icon, |this, icon| this.child(icon))
200 .children(children),
201 )
202 .when(self.show_dismiss_button, |this| {
203 this.child(
204 IconButton::new("dismiss", IconName::Close)
205 .icon_size(IconSize::Small)
206 .on_click(|_, window, cx| {
207 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
208 }),
209 )
210 }),
211 )
212 .when_some(self.description, |this, description| {
213 this.child(Label::new(description).color(Color::Muted).mb_2().flex_1())
214 }),
215 )
216 }
217}
218
219#[derive(IntoElement)]
220pub struct ModalRow {
221 children: SmallVec<[AnyElement; 2]>,
222}
223
224impl Default for ModalRow {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl ModalRow {
231 pub fn new() -> Self {
232 Self {
233 children: SmallVec::new(),
234 }
235 }
236}
237
238impl ParentElement for ModalRow {
239 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
240 self.children.extend(elements)
241 }
242}
243
244impl RenderOnce for ModalRow {
245 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
246 h_flex().w_full().py_1().children(self.children)
247 }
248}
249
250#[derive(IntoElement)]
251pub struct ModalFooter {
252 start_slot: Option<AnyElement>,
253 end_slot: Option<AnyElement>,
254}
255
256impl Default for ModalFooter {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262impl ModalFooter {
263 pub fn new() -> Self {
264 Self {
265 start_slot: None,
266 end_slot: None,
267 }
268 }
269
270 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
271 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
272 self
273 }
274
275 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
276 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
277 self
278 }
279}
280
281impl RenderOnce for ModalFooter {
282 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
283 h_flex()
284 .w_full()
285 .p(DynamicSpacing::Base08.rems(cx))
286 .flex_none()
287 .justify_between()
288 .gap_1()
289 .border_t_1()
290 .border_color(cx.theme().colors().border_variant)
291 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
292 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
293 }
294}
295
296#[derive(IntoElement)]
297pub struct Section {
298 contained: bool,
299 padded: bool,
300 header: Option<SectionHeader>,
301 meta: Option<SharedString>,
302 children: SmallVec<[AnyElement; 2]>,
303}
304
305impl Default for Section {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311impl Section {
312 pub fn new() -> Self {
313 Self {
314 contained: false,
315 padded: true,
316 header: None,
317 meta: None,
318 children: SmallVec::new(),
319 }
320 }
321
322 pub fn new_contained() -> Self {
323 Self {
324 contained: true,
325 padded: true,
326 header: None,
327 meta: None,
328 children: SmallVec::new(),
329 }
330 }
331
332 pub fn contained(mut self, contained: bool) -> Self {
333 self.contained = contained;
334 self
335 }
336
337 pub fn header(mut self, header: SectionHeader) -> Self {
338 self.header = Some(header);
339 self
340 }
341
342 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
343 self.meta = Some(meta.into());
344 self
345 }
346 pub fn padded(mut self, padded: bool) -> Self {
347 self.padded = padded;
348 self
349 }
350}
351
352impl ParentElement for Section {
353 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
354 self.children.extend(elements)
355 }
356}
357
358impl RenderOnce for Section {
359 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
360 let mut section_bg = cx.theme().colors().text;
361 section_bg.fade_out(0.96);
362
363 let children = if self.contained {
364 v_flex()
365 .flex_1()
366 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
367 .child(
368 v_flex()
369 .w_full()
370 .rounded_sm()
371 .border_1()
372 .border_color(cx.theme().colors().border)
373 .bg(section_bg)
374 .child(
375 div()
376 .flex()
377 .flex_1()
378 .pb_2()
379 .size_full()
380 .children(self.children),
381 ),
382 )
383 } else {
384 v_flex()
385 .w_full()
386 .flex_1()
387 .gap_y(DynamicSpacing::Base04.rems(cx))
388 .pb_2()
389 .when(self.padded, |this| {
390 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
391 })
392 .children(self.children)
393 };
394
395 v_flex()
396 .size_full()
397 .flex_1()
398 .child(
399 v_flex()
400 .flex_none()
401 .px(DynamicSpacing::Base12.rems(cx))
402 .children(self.header)
403 .when_some(self.meta, |this, meta| {
404 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
405 }),
406 )
407 .child(children)
408 // fill any leftover space
409 .child(div().flex().flex_1())
410 }
411}
412
413#[derive(IntoElement)]
414pub struct SectionHeader {
415 /// The label of the header.
416 label: SharedString,
417 /// A slot for content that appears after the label, usually on the other side of the header.
418 /// This might be a button, a disclosure arrow, a face pile, etc.
419 end_slot: Option<AnyElement>,
420}
421
422impl SectionHeader {
423 pub fn new(label: impl Into<SharedString>) -> Self {
424 Self {
425 label: label.into(),
426 end_slot: None,
427 }
428 }
429
430 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
431 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
432 self
433 }
434}
435
436impl RenderOnce for SectionHeader {
437 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
438 h_flex()
439 .id(self.label.clone())
440 .w_full()
441 .px(DynamicSpacing::Base08.rems(cx))
442 .child(
443 div()
444 .h_7()
445 .flex()
446 .items_center()
447 .justify_between()
448 .w_full()
449 .gap(DynamicSpacing::Base04.rems(cx))
450 .child(
451 div().flex_1().child(
452 Label::new(self.label.clone())
453 .size(LabelSize::Small)
454 .into_element(),
455 ),
456 )
457 .child(h_flex().children(self.end_slot)),
458 )
459 }
460}
461
462impl From<SharedString> for SectionHeader {
463 fn from(val: SharedString) -> Self {
464 SectionHeader::new(val)
465 }
466}
467
468impl From<&'static str> for SectionHeader {
469 fn from(val: &'static str) -> Self {
470 let label: SharedString = val.into();
471 SectionHeader::new(label)
472 }
473}