1use crate::root::WindowState;
2use gpui_base::TestSupportExt as _;
3use std::{rc::Rc, sync::LazyLock, time::Duration};
4
5use gpui::{
6 Action, Anchor, Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Edges,
7 FocusHandle, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
8 SharedString, StyleRefinement, Styled, Window, WindowControlArea, anchored, div, hsla, point,
9 prelude::FluentBuilder, px,
10};
11use gpui_base::{ElementExt as _, TextSelectionScopeId};
12use rust_i18n::t;
13
14use crate::{
15 ActiveTheme as _, IconName, Sizable as _, StyledExt, TITLE_BAR_HEIGHT, WindowExt as _,
16 animation::cubic_bezier,
17 button::{Button, ButtonVariant, ButtonVariants as _},
18 dialog::{DialogContent, DialogDispatchAnchor, DialogTitle},
19 scroll::ScrollableElement as _,
20 v_flex,
21};
22
23pub static ANIMATION_DURATION: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs_f64(0.25));
24pub use gpui_base::actions::{Cancel, Confirm};
25
26type OkHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
27type CancelHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
28type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
29
30fn merge_field<T>(slot: &mut Option<T>, value: Option<T>) {
32 if value.is_some() {
33 *slot = value;
34 }
35}
36
37#[derive(Clone, Default)]
46pub struct DialogButtonProps {
47 pub(crate) ok_text: Option<SharedString>,
48 pub(crate) ok_variant: Option<ButtonVariant>,
49 pub(crate) cancel_text: Option<SharedString>,
50 pub(crate) cancel_variant: Option<ButtonVariant>,
51 pub(crate) show_cancel: Option<bool>,
52 pub(crate) on_ok: Option<OkHandler>,
53 pub(crate) on_cancel: Option<CancelHandler>,
54 pub(crate) on_close: Option<CloseHandler>,
55}
56
57impl DialogButtonProps {
58 pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
60 self.ok_text = Some(ok_text.into());
61 self
62 }
63
64 pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
66 self.ok_variant = Some(ok_variant);
67 self
68 }
69
70 pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
72 self.cancel_text = Some(cancel_text.into());
73 self
74 }
75
76 pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
78 self.cancel_variant = Some(cancel_variant);
79 self
80 }
81
82 pub fn show_cancel(mut self, show_cancel: bool) -> Self {
84 self.show_cancel = Some(show_cancel);
85 self
86 }
87
88 pub fn on_ok(
92 mut self,
93 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
94 ) -> Self {
95 self.on_ok = Some(Rc::new(on_ok));
96 self
97 }
98
99 pub fn on_cancel(
103 mut self,
104 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
105 ) -> Self {
106 self.on_cancel = Some(Rc::new(on_cancel));
107 self
108 }
109
110 pub(crate) fn merge(&mut self, other: Self) {
112 merge_field(&mut self.ok_text, other.ok_text);
113 merge_field(&mut self.ok_variant, other.ok_variant);
114 merge_field(&mut self.cancel_text, other.cancel_text);
115 merge_field(&mut self.cancel_variant, other.cancel_variant);
116 merge_field(&mut self.show_cancel, other.show_cancel);
117 merge_field(&mut self.on_ok, other.on_ok);
118 merge_field(&mut self.on_cancel, other.on_cancel);
119 merge_field(&mut self.on_close, other.on_close);
120 }
121
122 pub(crate) fn is_cancel_shown(&self) -> bool {
124 self.show_cancel.unwrap_or(false)
125 }
126
127 pub(crate) fn ok_handler(&self) -> OkHandler {
129 self.on_ok
130 .clone()
131 .unwrap_or_else(|| Rc::new(|_, _, _| true))
132 }
133
134 pub(crate) fn cancel_handler(&self) -> CancelHandler {
136 self.on_cancel
137 .clone()
138 .unwrap_or_else(|| Rc::new(|_, _, _| true))
139 }
140
141 pub(crate) fn close_handler(&self) -> CloseHandler {
143 self.on_close
144 .clone()
145 .unwrap_or_else(|| Rc::new(|_, _, _| {}))
146 }
147
148 pub(crate) fn render_ok(&self, _: &mut Window, _: &mut App) -> AnyElement {
149 let ok_text = self
150 .ok_text
151 .clone()
152 .unwrap_or_else(|| t!("Dialog.ok").into());
153
154 DialogButton {
155 anchor_key: "dialog-ok-anchor",
156 button: Button::new("ok")
157 .label(ok_text)
158 .with_variant(self.ok_variant.unwrap_or(ButtonVariant::Primary)),
159 action: Rc::new(Confirm { secondary: false }),
160 }
161 .into_any_element()
162 }
163
164 pub(crate) fn render_cancel(&self, _: &mut Window, _: &mut App) -> AnyElement {
165 let cancel_text = self
166 .cancel_text
167 .clone()
168 .unwrap_or_else(|| t!("Dialog.cancel").into());
169
170 DialogButton {
171 anchor_key: "dialog-cancel-anchor",
172 button: Button::new("cancel")
173 .label(cancel_text)
174 .with_variant(self.cancel_variant.unwrap_or_default()),
175 action: Rc::new(Cancel),
176 }
177 .into_any_element()
178 }
179}
180
181#[derive(IntoElement)]
184struct DialogButton {
185 anchor_key: &'static str,
187 button: Button,
188 action: Rc<dyn Action>,
189}
190
191impl RenderOnce for DialogButton {
192 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
193 let anchor = DialogDispatchAnchor::new(self.anchor_key, window, cx);
194 self.button
195 .child(anchor.element())
196 .on_click(move |_, window, cx| anchor.dispatch(&*self.action, window, cx))
197 }
198}
199
200type ContentBuilderFn = Rc<dyn Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static>;
201
202#[derive(Clone)]
203pub(crate) struct DialogProps {
204 width: Pixels,
205 max_width: Option<Pixels>,
206 margin_top: Option<Pixels>,
207 close_button: bool,
208
209 overlay: bool,
210 overlay_closable: bool,
211 pub(crate) overlay_visible: bool,
212 keyboard: bool,
213}
214
215impl Default for DialogProps {
216 fn default() -> Self {
217 Self {
218 margin_top: None,
219 width: px(448.),
220 max_width: None,
221 overlay: true,
222 keyboard: true,
223 overlay_visible: false,
224 close_button: true,
225 overlay_closable: true,
226 }
227 }
228}
229
230enum BaseDialogRoot {
231 Dialog(gpui_base::Dialog),
232 AlertDialog(gpui_base::AlertDialog),
233}
234
235macro_rules! map_base_root {
236 ($self:expr, $method:ident($($arg:expr),* $(,)?)) => {
237 match $self {
238 BaseDialogRoot::Dialog(root) => BaseDialogRoot::Dialog(root.$method($($arg),*)),
239 BaseDialogRoot::AlertDialog(root) => {
240 BaseDialogRoot::AlertDialog(root.$method($($arg),*))
241 }
242 }
243 };
244}
245
246impl BaseDialogRoot {
247 fn layer(self, index: usize, topmost: bool) -> Self {
248 map_base_root!(self, layer(index, topmost))
249 }
250 fn focus_handle(self, focus: FocusHandle) -> Self {
251 map_base_root!(self, focus_handle(focus))
252 }
253 fn close_on_escape(self, value: bool) -> Self {
254 map_base_root!(self, close_on_escape(value))
255 }
256 fn close_on_backdrop_press(self, value: bool) -> Self {
257 match self {
258 Self::Dialog(root) => Self::Dialog(root.close_on_backdrop_press(value)),
259 Self::AlertDialog(root) => Self::AlertDialog(root),
260 }
261 }
262 fn dismiss_below_y(self, value: Pixels) -> Self {
263 map_base_root!(self, dismiss_below_y(value))
264 }
265 fn backdrop(self, element: impl IntoElement) -> Self {
266 map_base_root!(self, backdrop(element))
267 }
268 fn popup(self, element: impl IntoElement) -> Self {
269 map_base_root!(self, popup(element))
270 }
271 fn on_ok(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static) -> Self {
272 map_base_root!(self, on_ok(handler))
273 }
274 fn on_cancel(
275 self,
276 handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
277 ) -> Self {
278 map_base_root!(self, on_cancel(handler))
279 }
280 fn on_close(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
281 map_base_root!(self, on_close(handler))
282 }
283 fn request_close(self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
284 map_base_root!(self, request_close(handler))
285 }
286}
287
288impl IntoElement for BaseDialogRoot {
289 type Element = <gpui_base::Dialog as IntoElement>::Element;
290 fn into_element(self) -> Self::Element {
291 match self {
292 Self::Dialog(root) => root.into_element(),
293 Self::AlertDialog(root) => root.into_element(),
294 }
295 }
296}
297
298#[derive(IntoElement)]
300pub struct Dialog {
301 base: Option<BaseDialogRoot>,
302 pub(crate) style: StyleRefinement,
303 children: Vec<AnyElement>,
304 trigger: Option<AnyElement>,
305 title: Option<AnyElement>,
306 pub(crate) header: Option<AnyElement>,
307 pub(crate) footer: Option<AnyElement>,
308 pub(crate) content_builder: Option<ContentBuilderFn>,
309 pub(crate) props: DialogProps,
310
311 pub(super) button_props: DialogButtonProps,
312
313 pub(crate) focus_handle: FocusHandle,
315 pub(crate) layer_ix: usize,
316 pub(crate) selection_scope: TextSelectionScopeId,
317}
318
319pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla {
320 if !overlay {
321 return hsla(0., 0., 0., 0.);
322 }
323
324 cx.theme().overlay
325}
326
327impl Dialog {
328 pub fn new(cx: &mut App) -> Self {
330 Self {
331 base: Some(BaseDialogRoot::Dialog(gpui_base::Dialog::new(cx))),
332 focus_handle: cx.focus_handle(),
333 style: StyleRefinement::default(),
334 trigger: None,
335 title: None,
336 header: None,
337 footer: None,
338 content_builder: None,
339 props: DialogProps::default(),
340 children: Vec::new(),
341 layer_ix: 0,
342 selection_scope: TextSelectionScopeId::default(),
343 button_props: DialogButtonProps::default(),
344 }
345 }
346
347 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
351 self.trigger = Some(trigger.into_any_element());
352 self
353 }
354
355 pub fn content<F>(mut self, builder: F) -> Self
357 where
358 F: Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static,
359 {
360 self.content_builder = Some(Rc::new(builder));
361 self
362 }
363
364 pub fn title(mut self, title: impl IntoElement) -> Self {
366 self.title = Some(title.into_any_element());
367 self
368 }
369
370 pub(crate) fn header(mut self, header: impl IntoElement) -> Self {
374 self.header = Some(header.into_any_element());
375 self
376 }
377
378 pub fn footer(mut self, footer: impl IntoElement) -> Self {
382 self.footer = Some(footer.into_any_element());
383 self
384 }
385
386 pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
392 self.button_props.merge(button_props);
393 self
394 }
395
396 pub(crate) fn with_base_alert_dialog(mut self, base: gpui_base::AlertDialog) -> Self {
397 self.base = Some(BaseDialogRoot::AlertDialog(base));
398 self.props.overlay_closable = false;
399 self
400 }
401
402 pub fn on_close(
406 mut self,
407 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
408 ) -> Self {
409 self.button_props.on_close = Some(Rc::new(on_close));
410 self
411 }
412
413 pub fn on_ok(
417 mut self,
418 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
419 ) -> Self {
420 self.button_props = self.button_props.on_ok(on_ok);
421 self
422 }
423
424 pub fn on_cancel(
428 mut self,
429 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
430 ) -> Self {
431 self.button_props = self.button_props.on_cancel(on_cancel);
432 self
433 }
434
435 pub fn close_button(mut self, close_button: bool) -> Self {
437 self.props.close_button = close_button;
438 self
439 }
440
441 pub fn margin_top(mut self, margin_top: impl Into<Pixels>) -> Self {
443 self.props.margin_top = Some(margin_top.into());
444 self
445 }
446
447 pub fn w(mut self, width: impl Into<Pixels>) -> Self {
453 self.props.width = width.into();
454 self
455 }
456
457 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
461 self.props.width = width.into();
462 self
463 }
464
465 pub fn max_w(mut self, max_width: impl Into<Pixels>) -> Self {
467 self.props.max_width = Some(max_width.into());
468 self
469 }
470
471 pub fn overlay(mut self, overlay: bool) -> Self {
473 self.props.overlay = overlay;
474 self
475 }
476
477 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
481 self.props.overlay_closable = overlay_closable;
482 self
483 }
484
485 pub fn keyboard(mut self, keyboard: bool) -> Self {
487 self.props.keyboard = keyboard;
488 self
489 }
490
491 pub(crate) fn has_overlay(&self) -> bool {
492 self.props.overlay
493 }
494
495 pub(crate) fn with_props(mut self, props: DialogProps) -> Self {
496 self.props = props;
497 self
498 }
499
500 fn defer_close_dialog(window: &mut Window, cx: &mut App) {
501 WindowState::update(window, cx, |root, window, cx| {
502 root.defer_close_dialog(window, cx);
503 });
504 }
505}
506
507impl ParentElement for Dialog {
508 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
509 self.children.extend(elements);
510 }
511}
512
513impl Styled for Dialog {
514 fn style(&mut self) -> &mut gpui::StyleRefinement {
515 &mut self.style
516 }
517}
518
519impl Dialog {
520 fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
521 let content_builder = self.content_builder.clone();
522 let style = self.style.clone();
523 let props = self.props.clone();
524 let button_props = self.button_props.clone();
525
526 gpui_base::DialogTrigger::new(trigger)
527 .on_open(move |window, cx| {
528 let content_builder = content_builder.clone();
529 let style = style.clone();
530 let props = props.clone();
531 let button_props = button_props.clone();
532 window.open_dialog(cx, move |dialog, _, _| {
533 dialog
534 .refine_style(&style)
535 .button_props(button_props.clone())
536 .with_props(props.clone())
537 .content({
538 let content_builder = content_builder.clone();
539 move |content, window, cx| {
540 if let Some(builder) = content_builder.clone() {
541 builder(content, window, cx)
542 } else {
543 content
544 }
545 }
546 })
547 });
548 })
549 .into_any_element()
550 }
551}
552
553impl RenderOnce for Dialog {
554 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
555 if let Some(trigger) = self.trigger.take() {
556 return self.render_trigger(trigger, window, cx);
557 }
558
559 let layer_ix = self.layer_ix;
560 let selection_scope = self.selection_scope;
561 let on_close = self.button_props.close_handler();
562 let on_ok = self.button_props.ok_handler();
563 let on_cancel = self.button_props.cancel_handler();
564
565 let window_paddings = crate::window_border::window_paddings(window);
566 let view_size = window.viewport_size()
567 - gpui::size(
568 window_paddings.left + window_paddings.right,
569 window_paddings.top + window_paddings.bottom,
570 );
571 let margin = cx.theme().spacing_tokens().lg;
577 let layer_offset = px(layer_ix as f32 * 16.);
578 let y = self.props.margin_top.unwrap_or(view_size.height / 10.) + layer_offset;
579 let width = self
580 .props
581 .width
582 .min((view_size.width - margin * 2.).max(px(0.)));
583 let x = (view_size.width - width) / 2.;
584 let max_height = (view_size.height - margin * 2. - layer_offset).max(px(0.));
585
586 let base_size = window.text_style().font_size;
587 let rem_size = window.rem_size();
588
589 let mut paddings = Edges::all(px(16.));
590 if let Some(pl) = self.style.padding.left {
591 paddings.left = pl.to_pixels(base_size, rem_size);
592 }
593 if let Some(pr) = self.style.padding.right {
594 paddings.right = pr.to_pixels(base_size, rem_size);
595 }
596 if let Some(pt) = self.style.padding.top {
597 paddings.top = pt.to_pixels(base_size, rem_size);
598 }
599 if let Some(pb) = self.style.padding.bottom {
600 paddings.bottom = pb.to_pixels(base_size, rem_size);
601 }
602
603 let animation = Animation::new(*ANIMATION_DURATION).with_easing(cubic_bezier(
608 1. / 3.,
609 0.72,
610 2. / 3.,
611 1.,
612 ));
613
614 anchored()
615 .position(point(window_paddings.left, window_paddings.top))
616 .snap_to_window()
617 .child(
618 div()
619 .id("dialog")
620 .test_support()
621 .occlude()
622 .w(view_size.width)
623 .h(view_size.height)
624 .child(
625 self.base
626 .take()
627 .expect("Dialog base host is always present")
628 .layer(
629 layer_ix,
630 (self.layer_ix + 1)
631 == WindowState::read(window, cx).active_dialogs.len(),
632 )
633 .focus_handle(self.focus_handle.clone())
634 .close_on_escape(self.props.keyboard)
635 .close_on_backdrop_press(self.props.overlay_closable)
636 .dismiss_below_y(TITLE_BAR_HEIGHT)
637 .when(self.props.overlay, |this| {
638 this.backdrop(
639 div()
640 .absolute()
641 .size_full()
642 .window_control_area(WindowControlArea::Drag)
643 .when(self.props.overlay_visible, |overlay| {
644 overlay.bg(overlay_color(true, cx))
645 }),
646 )
647 })
648 .on_ok(move |event, window, cx| on_ok(event, window, cx))
649 .on_cancel(move |event, window, cx| on_cancel(event, window, cx))
650 .on_close(move |event, window, cx| on_close(event, window, cx))
651 .request_close(move |deferred, window, cx| {
652 if deferred {
653 Self::defer_close_dialog(window, cx);
654 } else {
655 window.close_dialog(cx);
656 }
657 })
658 .popup(
659 gpui_base::Positioner::corner(
660 Anchor::TopLeft,
661 point(window_paddings.left + x, window_paddings.top + y),
662 )
663 .margin(margin)
664 .child(
665 v_flex()
666 .id(layer_ix)
667 .test_support()
668 .debug_selector(move || format!("dialog-{layer_ix}"))
669 .bg(cx.theme().tokens.background)
670 .border_1()
671 .border_color(cx.theme().border)
672 .rounded(cx.theme().radius_lg)
673 .min_h_24()
674 .pt(paddings.top)
675 .pb(paddings.bottom)
676 .gap(paddings.top.max(px(8.)))
677 .refine_style(&self.style)
678 .px_0()
679 .occlude()
680 .w(width)
681 .when_some(self.props.max_width, |this, w| this.max_w(w))
682 .max_h(max_height)
683 .child(
684 v_flex()
685 .flex_1()
686 .overflow_hidden()
687 .gap_y_2()
688 .when_some(self.header, |this, header| {
689 this.child(
690 div()
691 .pl(paddings.left)
692 .pr(paddings.right)
693 .child(header),
694 )
695 })
696 .when_some(self.title, |this, title| {
697 this.child(
698 DialogTitle::new()
699 .pl(paddings.left)
700 .pr(paddings.right)
701 .child(title),
702 )
703 })
704 .when_some(self.content_builder, |this, builder| {
705 this.child(builder(
706 DialogContent::new()
707 .gap(paddings.bottom)
708 .pl(paddings.left)
709 .pr(paddings.right),
710 window,
711 cx,
712 ))
713 })
714 .when(!self.children.is_empty(), |this| {
715 this.child(
716 div().flex_1().overflow_hidden().child(
717 v_flex()
719 .size_full()
720 .overflow_y_scrollbar()
721 .pl(paddings.left)
722 .pr(paddings.right)
723 .children(self.children),
724 ),
725 )
726 }),
727 )
728 .when_some(self.footer, |this, footer| {
729 this.child(
730 div()
731 .pl(paddings.left)
732 .pr(paddings.right)
733 .child(footer),
734 )
735 })
736 .children(self.props.close_button.then(|| {
737 let top = (paddings.top - px(10.)).max(px(8.));
738 let right = (paddings.right - px(10.)).max(px(8.));
739
740 gpui_base::DialogClose::new()
741 .absolute()
742 .top(top)
743 .right(right)
744 .trigger(|button| {
745 Button::new("close")
746 .with_base(button)
747 .small()
748 .ghost()
749 .icon(IconName::Close)
750 })
751 }))
752 .with_animation(
753 "slide-down-shadow",
754 animation.clone(),
755 move |this, delta| {
756 let shadow = vec![
758 BoxShadow {
759 color: hsla(0., 0., 0., 0.1 * delta),
760 offset: point(px(0.), px(20.)),
761 blur_radius: px(25.),
762 spread_radius: px(-5.),
763 inset: false,
764 },
765 BoxShadow {
766 color: hsla(0., 0., 0., 0.1 * delta),
767 offset: point(px(0.), px(8.)),
768 blur_radius: px(10.),
769 spread_radius: px(-6.),
770 inset: false,
771 },
772 ];
773 this.shadow(shadow)
774 },
775 )
776 .text_selection_scope(selection_scope),
777 )
778 .with_animation(
779 "slide-down",
780 animation.clone(),
781 move |this, delta| {
782 this.position(point(
783 window_paddings.left + x,
784 window_paddings.top + y * delta,
785 ))
786 },
787 ),
788 ),
789 )
790 .with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
791 )
792 .into_any_element()
793 }
794}
795
796#[cfg(test)]
797pub(crate) mod tests {
798 use super::*;
799 use gpui::{AppContext as _, Bounds, Context, Render, TestAppContext, VisualTestContext, size};
800
801 struct DialogHost;
802
803 impl Render for DialogHost {
804 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
805 div().size_full()
806 }
807 }
808
809 pub(crate) fn window(
812 cx: &mut TestAppContext,
813 window_size: gpui::Size<Pixels>,
814 ) -> &mut VisualTestContext {
815 cx.update(|cx| {
816 crate::init(cx);
817 cx.set_reduce_motion(true);
818 });
819 let (_, cx) = cx.add_window_view(|window, cx| {
820 let view = cx.new(|_| DialogHost);
821 crate::Root::new(view, window, cx)
822 });
823 cx.simulate_resize(window_size);
824 cx.update(|window, cx| window.draw(cx).clear(cx));
825 cx
826 }
827
828 fn open(
829 cx: &mut VisualTestContext,
830 build: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
831 ) {
832 cx.update(|window, cx| window.open_dialog(cx, build));
833 cx.run_until_parked();
834 cx.update(|window, cx| window.draw(cx).clear(cx));
836 cx.update(|window, cx| window.draw(cx).clear(cx));
837 }
838
839 fn surface(cx: &mut VisualTestContext, layer_ix: usize) -> Bounds<Pixels> {
840 let selector = ["dialog-0", "dialog-1"][layer_ix];
841 cx.debug_bounds(selector)
842 .unwrap_or_else(|| panic!("dialog layer {layer_ix} was not painted"))
843 }
844
845 #[gpui::test]
848 fn a_dialog_that_fits_keeps_its_default_width_and_top_offset(cx: &mut TestAppContext) {
849 let cx = window(cx, size(px(1000.), px(800.)));
850 open(cx, |dialog, _, _| dialog.title("Fits").child("body"));
851
852 let bounds = surface(cx, 0);
853 assert_eq!(bounds.size.width, px(448.));
854 assert_eq!(bounds.origin.x, px(276.));
855 assert_eq!(bounds.origin.y, px(80.));
856 }
857
858 #[gpui::test]
863 fn a_dialog_larger_than_the_window_stays_inside_it(cx: &mut TestAppContext) {
864 let viewport = size(px(400.), px(300.));
865 let cx = window(cx, viewport);
866 open(cx, |dialog, _, _| {
867 dialog
868 .w(px(800.))
869 .title("Too big")
870 .child(div().h(px(1000.)).child("tall body"))
871 .footer(div().h(px(32.)).debug_selector(|| "footer-probe".into()))
872 });
873
874 let bounds = surface(cx, 0);
875 let footer = cx.debug_bounds("footer-probe").unwrap();
876 let margin = px(16.);
877 assert!(
878 bounds.origin.x >= margin && bounds.right() <= viewport.width - margin,
879 "the dialog ran off the sides: {bounds:?}"
880 );
881 assert!(
882 bounds.bottom() <= viewport.height - margin,
883 "the dialog ran off the bottom: {bounds:?}"
884 );
885 assert_eq!(bounds.origin.y, margin);
886 assert!(
887 footer.bottom() <= bounds.bottom(),
888 "the footer was clipped below the dialog: footer {footer:?}, dialog {bounds:?}"
889 );
890 }
891
892 #[gpui::test]
894 fn dialog_button_props_merge_with_what_the_dialog_already_carries(cx: &mut TestAppContext) {
895 let cx = window(cx, size(px(400.), px(300.)));
896 cx.update(|_, cx| {
897 let dialog = Dialog::new(cx)
898 .button_props(DialogButtonProps::default().cancel_text("Keep"))
899 .button_props(DialogButtonProps::default().ok_text("Delete"))
900 .button_props(DialogButtonProps::default().ok_variant(ButtonVariant::Danger));
901
902 assert_eq!(dialog.button_props.ok_text.as_deref(), Some("Delete"));
903 assert_eq!(dialog.button_props.cancel_text.as_deref(), Some("Keep"));
904 assert_eq!(dialog.button_props.ok_variant, Some(ButtonVariant::Danger));
905 });
906 }
907
908 #[gpui::test]
911 fn stacked_dialogs_each_fit_the_window(cx: &mut TestAppContext) {
912 let viewport = size(px(400.), px(300.));
913 let cx = window(cx, viewport);
914 open(cx, |dialog, _, _| {
915 dialog.title("First").child(div().h(px(1000.)))
916 });
917 open(cx, |dialog, _, _| {
918 dialog.title("Second").child(div().h(px(1000.)))
919 });
920
921 let first = surface(cx, 0);
922 let second = surface(cx, 1);
923 assert_eq!(second.origin.y, first.origin.y + px(16.));
924 assert!(first.bottom() <= viewport.height - px(16.), "{first:?}");
925 assert!(second.bottom() <= viewport.height - px(16.), "{second:?}");
926 assert!(second.size.height < first.size.height);
927 }
928}