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