1use std::{rc::Rc, sync::LazyLock, time::Duration};
2
3use gpui::{
4 Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Edges, FocusHandle, Hsla,
5 InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
6 StyleRefinement, Styled, Window, WindowControlArea, anchored, div, hsla, point,
7 prelude::FluentBuilder, px,
8};
9use gpui_base::{ElementExt as _, TextSelectionScopeId};
10use rust_i18n::t;
11
12use crate::{
13 ActiveTheme as _, IconName, Root, Sizable as _, StyledExt, TITLE_BAR_HEIGHT, WindowExt as _,
14 animation::cubic_bezier,
15 button::{Button, ButtonVariant, ButtonVariants as _},
16 dialog::{DialogContent, DialogTitle},
17 scroll::ScrollableElement as _,
18 v_flex,
19};
20
21pub static ANIMATION_DURATION: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs_f64(0.25));
22pub use gpui_base::actions::{Cancel, Confirm};
23
24#[derive(Clone)]
26pub struct DialogButtonProps {
27 pub(crate) ok_text: Option<SharedString>,
28 pub(crate) ok_variant: ButtonVariant,
29 pub(crate) cancel_text: Option<SharedString>,
30 pub(crate) cancel_variant: ButtonVariant,
31 pub(crate) show_cancel: bool,
32 pub(crate) on_ok: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>,
33 pub(crate) on_cancel: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>,
34 pub(crate) on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
35}
36
37impl Default for DialogButtonProps {
38 fn default() -> Self {
39 Self {
40 ok_text: None,
41 ok_variant: ButtonVariant::Primary,
42 cancel_text: None,
43 cancel_variant: ButtonVariant::default(),
44 show_cancel: false,
45 on_ok: Rc::new(|_, _, _| true),
46 on_cancel: Rc::new(|_, _, _| true),
47 on_close: Rc::new(|_, _, _| {}),
48 }
49 }
50}
51
52impl DialogButtonProps {
53 pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
55 self.ok_text = Some(ok_text.into());
56 self
57 }
58
59 pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
61 self.ok_variant = ok_variant;
62 self
63 }
64
65 pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
67 self.cancel_text = Some(cancel_text.into());
68 self
69 }
70
71 pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
73 self.cancel_variant = cancel_variant;
74 self
75 }
76
77 pub fn show_cancel(mut self, show_cancel: bool) -> Self {
79 self.show_cancel = show_cancel;
80 self
81 }
82
83 pub fn on_ok(
87 mut self,
88 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
89 ) -> Self {
90 self.on_ok = Rc::new(on_ok);
91 self
92 }
93
94 pub fn on_cancel(
98 mut self,
99 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
100 ) -> Self {
101 self.on_cancel = Rc::new(on_cancel);
102 self
103 }
104
105 pub(crate) fn render_ok(&self, _: &mut Window, _: &mut App) -> AnyElement {
106 let ok_text = self
107 .ok_text
108 .clone()
109 .unwrap_or_else(|| t!("Dialog.ok").into());
110 let ok_variant = self.ok_variant;
111
112 Button::new("ok")
113 .label(ok_text)
114 .with_variant(ok_variant)
115 .on_click(|_, window, cx| {
116 window.dispatch_action(Box::new(Confirm { secondary: false }), cx)
117 })
118 .into_any_element()
119 }
120
121 pub(crate) fn render_cancel(&self, _: &mut Window, _: &mut App) -> AnyElement {
122 let cancel_text = self
123 .cancel_text
124 .clone()
125 .unwrap_or_else(|| t!("Dialog.cancel").into());
126 let cancel_variant = self.cancel_variant;
127
128 Button::new("cancel")
129 .label(cancel_text)
130 .with_variant(cancel_variant)
131 .on_click(|_, window, cx| window.dispatch_action(Box::new(Cancel), cx))
132 .into_any_element()
133 }
134}
135
136type ContentBuilderFn = Rc<dyn Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static>;
137
138#[derive(Clone)]
139pub(crate) struct DialogProps {
140 width: Pixels,
141 max_width: Option<Pixels>,
142 margin_top: Option<Pixels>,
143 close_button: bool,
144
145 overlay: bool,
146 overlay_closable: bool,
147 pub(crate) overlay_visible: bool,
148 keyboard: bool,
149}
150
151impl Default for DialogProps {
152 fn default() -> Self {
153 Self {
154 margin_top: None,
155 width: px(448.),
156 max_width: None,
157 overlay: true,
158 keyboard: true,
159 overlay_visible: false,
160 close_button: true,
161 overlay_closable: true,
162 }
163 }
164}
165
166enum BaseDialogRoot {
167 Dialog(gpui_base::Dialog),
168 AlertDialog(gpui_base::AlertDialog),
169}
170
171macro_rules! map_base_root {
172 ($self:expr, $method:ident($($arg:expr),* $(,)?)) => {
173 match $self {
174 BaseDialogRoot::Dialog(root) => BaseDialogRoot::Dialog(root.$method($($arg),*)),
175 BaseDialogRoot::AlertDialog(root) => {
176 BaseDialogRoot::AlertDialog(root.$method($($arg),*))
177 }
178 }
179 };
180}
181
182impl BaseDialogRoot {
183 fn layer(self, index: usize, topmost: bool) -> Self {
184 map_base_root!(self, layer(index, topmost))
185 }
186 fn focus_handle(self, focus: FocusHandle) -> Self {
187 map_base_root!(self, focus_handle(focus))
188 }
189 fn close_on_escape(self, value: bool) -> Self {
190 map_base_root!(self, close_on_escape(value))
191 }
192 fn close_on_backdrop_press(self, value: bool) -> Self {
193 match self {
194 Self::Dialog(root) => Self::Dialog(root.close_on_backdrop_press(value)),
195 Self::AlertDialog(root) => Self::AlertDialog(root),
196 }
197 }
198 fn dismiss_below_y(self, value: Pixels) -> Self {
199 map_base_root!(self, dismiss_below_y(value))
200 }
201 fn backdrop(self, element: impl IntoElement) -> Self {
202 map_base_root!(self, backdrop(element))
203 }
204 fn popup(self, element: impl IntoElement) -> Self {
205 map_base_root!(self, popup(element))
206 }
207 fn on_ok(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static) -> Self {
208 map_base_root!(self, on_ok(handler))
209 }
210 fn on_cancel(
211 self,
212 handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
213 ) -> Self {
214 map_base_root!(self, on_cancel(handler))
215 }
216 fn on_close(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
217 map_base_root!(self, on_close(handler))
218 }
219 fn request_close(self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
220 map_base_root!(self, request_close(handler))
221 }
222}
223
224impl IntoElement for BaseDialogRoot {
225 type Element = <gpui_base::Dialog as IntoElement>::Element;
226 fn into_element(self) -> Self::Element {
227 match self {
228 Self::Dialog(root) => root.into_element(),
229 Self::AlertDialog(root) => root.into_element(),
230 }
231 }
232}
233
234#[derive(IntoElement)]
236pub struct Dialog {
237 base: Option<BaseDialogRoot>,
238 pub(crate) style: StyleRefinement,
239 children: Vec<AnyElement>,
240 trigger: Option<AnyElement>,
241 title: Option<AnyElement>,
242 pub(crate) header: Option<AnyElement>,
243 pub(crate) footer: Option<AnyElement>,
244 pub(crate) content_builder: Option<ContentBuilderFn>,
245 pub(crate) props: DialogProps,
246
247 pub(super) button_props: DialogButtonProps,
248
249 pub(crate) focus_handle: FocusHandle,
251 pub(crate) layer_ix: usize,
252 pub(crate) selection_scope: TextSelectionScopeId,
253}
254
255pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla {
256 if !overlay {
257 return hsla(0., 0., 0., 0.);
258 }
259
260 cx.theme().overlay
261}
262
263impl Dialog {
264 pub fn new(cx: &mut App) -> Self {
266 Self {
267 base: Some(BaseDialogRoot::Dialog(gpui_base::Dialog::new(cx))),
268 focus_handle: cx.focus_handle(),
269 style: StyleRefinement::default(),
270 trigger: None,
271 title: None,
272 header: None,
273 footer: None,
274 content_builder: None,
275 props: DialogProps::default(),
276 children: Vec::new(),
277 layer_ix: 0,
278 selection_scope: TextSelectionScopeId::default(),
279 button_props: DialogButtonProps::default(),
280 }
281 }
282
283 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
287 self.trigger = Some(trigger.into_any_element());
288 self
289 }
290
291 pub fn content<F>(mut self, builder: F) -> Self
293 where
294 F: Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static,
295 {
296 self.content_builder = Some(Rc::new(builder));
297 self
298 }
299
300 pub fn title(mut self, title: impl IntoElement) -> Self {
302 self.title = Some(title.into_any_element());
303 self
304 }
305
306 pub(crate) fn header(mut self, header: impl IntoElement) -> Self {
310 self.header = Some(header.into_any_element());
311 self
312 }
313
314 pub fn footer(mut self, footer: impl IntoElement) -> Self {
318 self.footer = Some(footer.into_any_element());
319 self
320 }
321
322 pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
324 self.button_props = button_props;
325 self
326 }
327 pub(crate) fn with_base_alert_dialog(mut self, base: gpui_base::AlertDialog) -> Self {
328 self.base = Some(BaseDialogRoot::AlertDialog(base));
329 self.props.overlay_closable = false;
330 self
331 }
332
333 pub fn on_close(
337 mut self,
338 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
339 ) -> Self {
340 self.button_props.on_close = Rc::new(on_close);
341 self
342 }
343
344 pub fn on_ok(
348 mut self,
349 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
350 ) -> Self {
351 self.button_props = self.button_props.on_ok(on_ok);
352 self
353 }
354
355 pub fn on_cancel(
359 mut self,
360 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
361 ) -> Self {
362 self.button_props = self.button_props.on_cancel(on_cancel);
363 self
364 }
365
366 pub fn close_button(mut self, close_button: bool) -> Self {
368 self.props.close_button = close_button;
369 self
370 }
371
372 pub fn margin_top(mut self, margin_top: impl Into<Pixels>) -> Self {
374 self.props.margin_top = Some(margin_top.into());
375 self
376 }
377
378 pub fn w(mut self, width: impl Into<Pixels>) -> Self {
382 self.props.width = width.into();
383 self
384 }
385
386 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
388 self.props.width = width.into();
389 self
390 }
391
392 pub fn max_w(mut self, max_width: impl Into<Pixels>) -> Self {
394 self.props.max_width = Some(max_width.into());
395 self
396 }
397
398 pub fn overlay(mut self, overlay: bool) -> Self {
400 self.props.overlay = overlay;
401 self
402 }
403
404 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
408 self.props.overlay_closable = overlay_closable;
409 self
410 }
411
412 pub fn keyboard(mut self, keyboard: bool) -> Self {
414 self.props.keyboard = keyboard;
415 self
416 }
417
418 pub(crate) fn has_overlay(&self) -> bool {
419 self.props.overlay
420 }
421
422 pub(crate) fn with_props(mut self, props: DialogProps) -> Self {
423 self.props = props;
424 self
425 }
426
427 fn defer_close_dialog(window: &mut Window, cx: &mut App) {
428 Root::update(window, cx, |root, window, cx| {
429 root.defer_close_dialog(window, cx);
430 });
431 }
432}
433
434impl ParentElement for Dialog {
435 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
436 self.children.extend(elements);
437 }
438}
439
440impl Styled for Dialog {
441 fn style(&mut self) -> &mut gpui::StyleRefinement {
442 &mut self.style
443 }
444}
445
446impl Dialog {
447 fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
448 let content_builder = self.content_builder.clone();
449 let style = self.style.clone();
450 let props = self.props.clone();
451 let button_props = self.button_props.clone();
452
453 gpui_base::DialogTrigger::new(trigger)
454 .on_open(move |window, cx| {
455 let content_builder = content_builder.clone();
456 let style = style.clone();
457 let props = props.clone();
458 let button_props = button_props.clone();
459 window.open_dialog(cx, move |dialog, _, _| {
460 dialog
461 .refine_style(&style)
462 .button_props(button_props.clone())
463 .with_props(props.clone())
464 .content({
465 let content_builder = content_builder.clone();
466 move |content, window, cx| {
467 if let Some(builder) = content_builder.clone() {
468 builder(content, window, cx)
469 } else {
470 content
471 }
472 }
473 })
474 });
475 })
476 .into_any_element()
477 }
478}
479
480impl RenderOnce for Dialog {
481 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
482 if let Some(trigger) = self.trigger.take() {
483 return self.render_trigger(trigger, window, cx);
484 }
485
486 let layer_ix = self.layer_ix;
487 let selection_scope = self.selection_scope;
488 let on_close = self.button_props.on_close.clone();
489 let on_ok = self.button_props.on_ok.clone();
490 let on_cancel = self.button_props.on_cancel.clone();
491
492 let window_paddings = crate::window_border::window_paddings(window);
493 let view_size = window.viewport_size()
494 - gpui::size(
495 window_paddings.left + window_paddings.right,
496 window_paddings.top + window_paddings.bottom,
497 );
498 let y = self.props.margin_top.unwrap_or(view_size.height / 10.) + px(layer_ix as f32 * 16.);
499 let x = view_size.width / 2. - self.props.width / 2.;
500
501 let base_size = window.text_style().font_size;
502 let rem_size = window.rem_size();
503
504 let mut paddings = Edges::all(px(16.));
505 if let Some(pl) = self.style.padding.left {
506 paddings.left = pl.to_pixels(base_size, rem_size);
507 }
508 if let Some(pr) = self.style.padding.right {
509 paddings.right = pr.to_pixels(base_size, rem_size);
510 }
511 if let Some(pt) = self.style.padding.top {
512 paddings.top = pt.to_pixels(base_size, rem_size);
513 }
514 if let Some(pb) = self.style.padding.bottom {
515 paddings.bottom = pb.to_pixels(base_size, rem_size);
516 }
517
518 let animation = Animation::new(*ANIMATION_DURATION).with_easing(cubic_bezier(
523 1. / 3.,
524 0.72,
525 2. / 3.,
526 1.,
527 ));
528
529 anchored()
530 .position(point(window_paddings.left, window_paddings.top))
531 .snap_to_window()
532 .child(
533 div()
534 .id("dialog")
535 .occlude()
536 .w(view_size.width)
537 .h(view_size.height)
538 .child(
539 self.base
540 .take()
541 .expect("Dialog base host is always present")
542 .layer(
543 layer_ix,
544 (self.layer_ix + 1) == Root::read(window, cx).active_dialogs.len(),
545 )
546 .focus_handle(self.focus_handle.clone())
547 .close_on_escape(self.props.keyboard)
548 .close_on_backdrop_press(self.props.overlay_closable)
549 .dismiss_below_y(TITLE_BAR_HEIGHT)
550 .when(self.props.overlay, |this| {
551 this.backdrop(
552 div()
553 .absolute()
554 .size_full()
555 .window_control_area(WindowControlArea::Drag)
556 .when(self.props.overlay_visible, |overlay| {
557 overlay.bg(overlay_color(true, cx))
558 }),
559 )
560 })
561 .on_ok(move |event, window, cx| on_ok(event, window, cx))
562 .on_cancel(move |event, window, cx| on_cancel(event, window, cx))
563 .on_close(move |event, window, cx| on_close(event, window, cx))
564 .request_close(move |deferred, window, cx| {
565 if deferred {
566 Self::defer_close_dialog(window, cx);
567 } else {
568 window.close_dialog(cx);
569 }
570 })
571 .popup(
572 v_flex()
573 .id(layer_ix)
574 .bg(cx.theme().tokens.background)
575 .border_1()
576 .border_color(cx.theme().border)
577 .rounded(cx.theme().radius_lg)
578 .min_h_24()
579 .pt(paddings.top)
580 .pb(paddings.bottom)
581 .gap(paddings.top.max(px(8.)))
582 .refine_style(&self.style)
583 .px_0()
584 .absolute()
586 .occlude()
587 .relative()
588 .left(x)
589 .top(y)
590 .w(self.props.width)
591 .when_some(self.props.max_width, |this, w| this.max_w(w))
592 .child(
593 v_flex()
594 .flex_1()
595 .overflow_hidden()
596 .gap_y_2()
597 .when_some(self.header, |this, header| {
598 this.child(
599 div()
600 .pl(paddings.left)
601 .pr(paddings.right)
602 .child(header),
603 )
604 })
605 .when_some(self.title, |this, title| {
606 this.child(
607 DialogTitle::new()
608 .pl(paddings.left)
609 .pr(paddings.right)
610 .child(title),
611 )
612 })
613 .when_some(self.content_builder, |this, builder| {
614 this.child(builder(
615 DialogContent::new()
616 .gap(paddings.bottom)
617 .pl(paddings.left)
618 .pr(paddings.right),
619 window,
620 cx,
621 ))
622 })
623 .when(!self.children.is_empty(), |this| {
624 this.child(
625 div().flex_1().overflow_hidden().child(
626 v_flex()
628 .size_full()
629 .overflow_y_scrollbar()
630 .pl(paddings.left)
631 .pr(paddings.right)
632 .children(self.children),
633 ),
634 )
635 }),
636 )
637 .when_some(self.footer, |this, footer| {
638 this.child(
639 div()
640 .pl(paddings.left)
641 .pr(paddings.right)
642 .child(footer),
643 )
644 })
645 .children(self.props.close_button.then(|| {
646 let top = (paddings.top - px(10.)).max(px(8.));
647 let right = (paddings.right - px(10.)).max(px(8.));
648
649 gpui_base::DialogClose::new()
650 .absolute()
651 .top(top)
652 .right(right)
653 .child(
654 Button::new("close")
655 .small()
656 .ghost()
657 .icon(IconName::Close),
658 )
659 }))
660 .with_animation(
661 "slide-down",
662 animation.clone(),
663 move |this, delta| {
664 let shadow = vec![
666 BoxShadow {
667 color: hsla(0., 0., 0., 0.1 * delta),
668 offset: point(px(0.), px(20.)),
669 blur_radius: px(25.),
670 spread_radius: px(-5.),
671 inset: false,
672 },
673 BoxShadow {
674 color: hsla(0., 0., 0., 0.1 * delta),
675 offset: point(px(0.), px(8.)),
676 blur_radius: px(10.),
677 spread_radius: px(-6.),
678 inset: false,
679 },
680 ];
681 this.top(y * delta).shadow(shadow)
682 },
683 )
684 .text_selection_scope(selection_scope),
685 ),
686 )
687 .with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
688 )
689 .into_any_element()
690 }
691}