1use crate::{IconButtonShape, prelude::*};
2
3use gpui::{prelude::FluentBuilder, *};
4use smallvec::SmallVec;
5use theme::ActiveTheme;
6
7#[derive(IntoElement, RegisterComponent)]
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 .bg(semantic::elevated_surface(cx))
72 .border_1()
73 .border_color(semantic::border(cx))
74 .rounded_lg()
75 .shadow_level(crate::styles::Shadow::Xl)
76 .child(self.header)
77 .child(
78 v_flex()
79 .id(self.container_id.clone())
80 .w_full()
81 .flex_1()
82 .p(DynamicSpacing::Base24.rems(cx))
83 .gap(DynamicSpacing::Base08.rems(cx))
84 .when_some(
85 self.container_scroll_handler,
86 |this, container_scroll_handle| {
87 this.overflow_y_scroll()
88 .track_scroll(&container_scroll_handle)
89 },
90 )
91 .children(self.children),
92 )
93 .children(self.footer)
94 }
95}
96
97#[derive(IntoElement)]
98pub struct ModalHeader {
99 icon: Option<Icon>,
100 headline: Option<SharedString>,
101 description: Option<SharedString>,
102 children: SmallVec<[AnyElement; 2]>,
103 show_dismiss_button: bool,
104 show_back_button: bool,
105}
106
107impl Default for ModalHeader {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl ModalHeader {
114 pub fn new() -> Self {
115 Self {
116 icon: None,
117 headline: None,
118 description: None,
119 children: SmallVec::new(),
120 show_dismiss_button: false,
121 show_back_button: false,
122 }
123 }
124
125 pub fn icon(mut self, icon: Icon) -> Self {
126 self.icon = Some(icon);
127 self
128 }
129
130 pub fn headline(mut self, headline: impl Into<SharedString>) -> Self {
135 self.headline = Some(headline.into());
136 self
137 }
138
139 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
140 self.description = Some(description.into());
141 self
142 }
143
144 pub fn show_dismiss_button(mut self, show: bool) -> Self {
145 self.show_dismiss_button = show;
146 self
147 }
148
149 pub fn show_back_button(mut self, show: bool) -> Self {
150 self.show_back_button = show;
151 self
152 }
153}
154
155impl ParentElement for ModalHeader {
156 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
157 self.children.extend(elements)
158 }
159}
160
161impl RenderOnce for ModalHeader {
162 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
163 let mut children = self.children;
164
165 if let Some(headline) = self.headline {
166 children.insert(
167 0,
168 Headline::new(headline)
169 .size(HeadlineSize::Small)
170 .color(Color::Muted)
171 .into_any_element(),
172 );
173 }
174
175 h_flex()
176 .min_w_0()
177 .flex_none()
178 .justify_between()
179 .w_full()
180 .px(DynamicSpacing::Base12.rems(cx))
181 .pt(DynamicSpacing::Base08.rems(cx))
182 .pb(DynamicSpacing::Base04.rems(cx))
183 .gap(DynamicSpacing::Base08.rems(cx))
184 .border_b_1()
185 .border_color(semantic::border_muted(cx))
186 .when(self.show_back_button, |this| {
187 this.child(
188 IconButton::new("back", IconName::ArrowLeft)
189 .shape(IconButtonShape::Square)
190 .on_click(|_, window, cx| {
191 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
192 }),
193 )
194 })
195 .child(
196 v_flex()
197 .min_w_0()
198 .flex_1()
199 .child(
200 h_flex()
201 .w_full()
202 .gap_1()
203 .justify_between()
204 .child(
205 h_flex()
206 .gap_1()
207 .when_some(self.icon, |this, icon| this.child(icon))
208 .children(children),
209 )
210 .when(self.show_dismiss_button, |this| {
211 this.child(
212 IconButton::new("dismiss", IconName::Close)
213 .icon_size(IconSize::Small)
214 .on_click(|_, window, cx| {
215 window.dispatch_action(menu::Cancel.boxed_clone(), cx);
216 }),
217 )
218 }),
219 )
220 .when_some(self.description, |this, description| {
221 this.child(Label::new(description).color(Color::Muted).mb_2().flex_1())
222 }),
223 )
224 }
225}
226
227#[derive(IntoElement)]
228pub struct ModalRow {
229 children: SmallVec<[AnyElement; 2]>,
230}
231
232impl Default for ModalRow {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238impl ModalRow {
239 pub fn new() -> Self {
240 Self {
241 children: SmallVec::new(),
242 }
243 }
244}
245
246impl ParentElement for ModalRow {
247 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
248 self.children.extend(elements)
249 }
250}
251
252impl RenderOnce for ModalRow {
253 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
254 h_flex().w_full().py_1().children(self.children)
255 }
256}
257
258#[derive(IntoElement)]
259pub struct ModalFooter {
260 start_slot: Option<AnyElement>,
261 end_slot: Option<AnyElement>,
262}
263
264impl Default for ModalFooter {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl ModalFooter {
271 pub fn new() -> Self {
272 Self {
273 start_slot: None,
274 end_slot: None,
275 }
276 }
277
278 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
279 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
280 self
281 }
282
283 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
284 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
285 self
286 }
287}
288
289impl RenderOnce for ModalFooter {
290 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
291 h_flex()
292 .w_full()
293 .p(DynamicSpacing::Base08.rems(cx))
294 .flex_none()
295 .justify_between()
296 .gap(DynamicSpacing::Base12.rems(cx))
297 .border_t_1()
298 .border_color(semantic::border_muted(cx))
299 .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
300 .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
301 }
302}
303
304#[derive(IntoElement)]
305pub struct Section {
306 contained: bool,
307 padded: bool,
308 header: Option<SectionHeader>,
309 meta: Option<SharedString>,
310 children: SmallVec<[AnyElement; 2]>,
311}
312
313impl Default for Section {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319impl Section {
320 pub fn new() -> Self {
321 Self {
322 contained: false,
323 padded: true,
324 header: None,
325 meta: None,
326 children: SmallVec::new(),
327 }
328 }
329
330 pub fn new_contained() -> Self {
331 Self {
332 contained: true,
333 padded: true,
334 header: None,
335 meta: None,
336 children: SmallVec::new(),
337 }
338 }
339
340 pub fn contained(mut self, contained: bool) -> Self {
341 self.contained = contained;
342 self
343 }
344
345 pub fn header(mut self, header: SectionHeader) -> Self {
346 self.header = Some(header);
347 self
348 }
349
350 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
351 self.meta = Some(meta.into());
352 self
353 }
354 pub fn padded(mut self, padded: bool) -> Self {
355 self.padded = padded;
356 self
357 }
358}
359
360impl ParentElement for Section {
361 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
362 self.children.extend(elements)
363 }
364}
365
366impl RenderOnce for Section {
367 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
368 let mut section_bg = cx.theme().colors().text;
369 section_bg.fade_out(0.96);
370
371 let children = if self.contained {
372 v_flex()
373 .flex_1()
374 .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx)))
375 .child(
376 v_flex()
377 .w_full()
378 .rounded_sm()
379 .border_1()
380 .border_color(cx.theme().colors().border)
381 .bg(section_bg)
382 .child(
383 div()
384 .flex()
385 .flex_1()
386 .pb_2()
387 .size_full()
388 .children(self.children),
389 ),
390 )
391 } else {
392 v_flex()
393 .w_full()
394 .flex_1()
395 .gap_y(DynamicSpacing::Base04.rems(cx))
396 .pb_2()
397 .when(self.padded, |this| {
398 this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx))
399 })
400 .children(self.children)
401 };
402
403 v_flex()
404 .size_full()
405 .flex_1()
406 .child(
407 v_flex()
408 .flex_none()
409 .px(DynamicSpacing::Base12.rems(cx))
410 .children(self.header)
411 .when_some(self.meta, |this, meta| {
412 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
413 }),
414 )
415 .child(children)
416 .child(div().flex().flex_1())
418 }
419}
420
421#[derive(IntoElement)]
422pub struct SectionHeader {
423 label: SharedString,
425 end_slot: Option<AnyElement>,
428}
429
430impl SectionHeader {
431 pub fn new(label: impl Into<SharedString>) -> Self {
432 Self {
433 label: label.into(),
434 end_slot: None,
435 }
436 }
437
438 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
439 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
440 self
441 }
442}
443
444impl RenderOnce for SectionHeader {
445 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
446 h_flex()
447 .id(self.label.clone())
448 .w_full()
449 .px(DynamicSpacing::Base08.rems(cx))
450 .child(
451 div()
452 .h_7()
453 .flex()
454 .items_center()
455 .justify_between()
456 .w_full()
457 .gap(DynamicSpacing::Base04.rems(cx))
458 .child(
459 div().flex_1().child(
460 Label::new(self.label.clone())
461 .size(LabelSize::Small)
462 .into_element(),
463 ),
464 )
465 .child(h_flex().children(self.end_slot)),
466 )
467 }
468}
469
470impl From<SharedString> for SectionHeader {
471 fn from(val: SharedString) -> Self {
472 SectionHeader::new(val)
473 }
474}
475
476impl From<&'static str> for SectionHeader {
477 fn from(val: &'static str) -> Self {
478 let label: SharedString = val.into();
479 SectionHeader::new(label)
480 }
481}
482
483impl component::Component for Modal {
484 fn scope() -> ComponentScope {
485 ComponentScope::Overlays
486 }
487
488 fn description() -> Option<&'static str> {
489 Some("A dialog surface with header/body/footer sections, raised above the app content.")
490 }
491
492 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
493 Some(
494 v_flex()
495 .gap_6()
496 .child(example_group(vec![single_example(
497 "Basic",
498 div()
499 .w(px(420.))
500 .h(px(280.))
501 .child(
502 Modal::new("modal-preview", None)
503 .header(
504 ModalHeader::new()
505 .headline("Modal title")
506 .show_dismiss_button(true),
507 )
508 .section(
509 Section::new()
510 .child(Label::new("Modal body content goes here.")),
511 )
512 .footer(
513 ModalFooter::new().end_slot(
514 h_flex()
515 .gap_2()
516 .child(
517 Button::new("cancel", "Cancel").color(Color::Muted),
518 )
519 .child(Button::new("confirm", "Confirm").primary()),
520 ),
521 ),
522 )
523 .into_any_element(),
524 )]))
525 .into_any_element(),
526 )
527 }
528}