1use super::entrance::{DialogEntrance, EntranceSurface};
2use crate::root::WindowState;
3use gpui_base::TestSupportExt as _;
4use std::{rc::Rc, sync::LazyLock, time::Duration};
5
6use gpui::{
7 Action, Anchor, AnyElement, App, BoxShadow, ClickEvent, Edges, FocusHandle, Hsla,
8 InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
9 StyleRefinement, Styled, Window, WindowControlArea, anchored, div, hsla, point,
10 prelude::FluentBuilder, px,
11};
12use gpui_base::{ElementExt as _, TextSelectionScopeId, motion::Transition};
13use rust_i18n::t;
14
15use crate::{
16 ActiveTheme as _, IconName, Sizable as _, StyledExt, TITLE_BAR_HEIGHT, WindowExt as _,
17 animation::cubic_bezier,
18 button::{Button, ButtonVariant, ButtonVariants as _},
19 dialog::{DialogContent, DialogDispatchAnchor, DialogTitle},
20 scroll::ScrollableElement as _,
21 v_flex,
22};
23
24pub static ANIMATION_DURATION: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs_f64(0.25));
25pub use gpui_base::actions::{Cancel, Confirm};
26
27type OkHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
28type CancelHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
29type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
30
31fn merge_field<T>(slot: &mut Option<T>, value: Option<T>) {
33 if value.is_some() {
34 *slot = value;
35 }
36}
37
38#[derive(Clone, Default)]
47pub struct DialogButtonProps {
48 pub(crate) ok_text: Option<SharedString>,
49 pub(crate) ok_variant: Option<ButtonVariant>,
50 pub(crate) cancel_text: Option<SharedString>,
51 pub(crate) cancel_variant: Option<ButtonVariant>,
52 pub(crate) show_cancel: Option<bool>,
53 pub(crate) on_ok: Option<OkHandler>,
54 pub(crate) on_cancel: Option<CancelHandler>,
55 pub(crate) on_close: Option<CloseHandler>,
56}
57
58impl DialogButtonProps {
59 pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
61 self.ok_text = Some(ok_text.into());
62 self
63 }
64
65 pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
67 self.ok_variant = Some(ok_variant);
68 self
69 }
70
71 pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
73 self.cancel_text = Some(cancel_text.into());
74 self
75 }
76
77 pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
79 self.cancel_variant = Some(cancel_variant);
80 self
81 }
82
83 pub fn show_cancel(mut self, show_cancel: bool) -> Self {
85 self.show_cancel = Some(show_cancel);
86 self
87 }
88
89 pub fn on_ok(
93 mut self,
94 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
95 ) -> Self {
96 self.on_ok = Some(Rc::new(on_ok));
97 self
98 }
99
100 pub fn on_cancel(
104 mut self,
105 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
106 ) -> Self {
107 self.on_cancel = Some(Rc::new(on_cancel));
108 self
109 }
110
111 pub(crate) fn merge(&mut self, other: Self) {
113 merge_field(&mut self.ok_text, other.ok_text);
114 merge_field(&mut self.ok_variant, other.ok_variant);
115 merge_field(&mut self.cancel_text, other.cancel_text);
116 merge_field(&mut self.cancel_variant, other.cancel_variant);
117 merge_field(&mut self.show_cancel, other.show_cancel);
118 merge_field(&mut self.on_ok, other.on_ok);
119 merge_field(&mut self.on_cancel, other.on_cancel);
120 merge_field(&mut self.on_close, other.on_close);
121 }
122
123 pub(crate) fn is_cancel_shown(&self) -> bool {
125 self.show_cancel.unwrap_or(false)
126 }
127
128 pub(crate) fn ok_handler(&self) -> OkHandler {
130 self.on_ok
131 .clone()
132 .unwrap_or_else(|| Rc::new(|_, _, _| true))
133 }
134
135 pub(crate) fn cancel_handler(&self) -> CancelHandler {
137 self.on_cancel
138 .clone()
139 .unwrap_or_else(|| Rc::new(|_, _, _| true))
140 }
141
142 pub(crate) fn close_handler(&self) -> CloseHandler {
144 self.on_close
145 .clone()
146 .unwrap_or_else(|| Rc::new(|_, _, _| {}))
147 }
148
149 pub(crate) fn render_ok(&self, _: &mut Window, _: &mut App) -> AnyElement {
150 let ok_text = self
151 .ok_text
152 .clone()
153 .unwrap_or_else(|| t!("Dialog.ok").into());
154
155 DialogButton {
156 anchor_key: "dialog-ok-anchor",
157 button: Button::new("ok")
158 .label(ok_text)
159 .with_variant(self.ok_variant.unwrap_or(ButtonVariant::Primary)),
160 action: Rc::new(Confirm { secondary: false }),
161 }
162 .into_any_element()
163 }
164
165 pub(crate) fn render_cancel(&self, _: &mut Window, _: &mut App) -> AnyElement {
166 let cancel_text = self
167 .cancel_text
168 .clone()
169 .unwrap_or_else(|| t!("Dialog.cancel").into());
170
171 DialogButton {
172 anchor_key: "dialog-cancel-anchor",
173 button: Button::new("cancel")
174 .label(cancel_text)
175 .with_variant(self.cancel_variant.unwrap_or_default()),
176 action: Rc::new(Cancel),
177 }
178 .into_any_element()
179 }
180}
181
182#[derive(IntoElement)]
185struct DialogButton {
186 anchor_key: &'static str,
188 button: Button,
189 action: Rc<dyn Action>,
190}
191
192impl RenderOnce for DialogButton {
193 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
194 let anchor = DialogDispatchAnchor::new(self.anchor_key, window, cx);
195 self.button
196 .child(anchor.element())
197 .on_click(move |_, window, cx| anchor.dispatch(&*self.action, window, cx))
198 }
199}
200
201type ContentBuilderFn = Rc<dyn Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static>;
202
203#[derive(Clone)]
204pub(crate) struct DialogProps {
205 width: Pixels,
206 max_width: Option<Pixels>,
207 margin_top: Option<Pixels>,
208 centered: bool,
209 close_button: bool,
210
211 overlay: bool,
212 overlay_closable: bool,
213 pub(crate) overlay_visible: bool,
214 keyboard: bool,
215 entrance: DialogEntrance,
216}
217
218impl Default for DialogProps {
219 fn default() -> Self {
220 Self {
221 margin_top: None,
222 centered: false,
223 width: px(448.),
224 max_width: None,
225 overlay: true,
226 keyboard: true,
227 overlay_visible: false,
228 close_button: true,
229 overlay_closable: true,
230 entrance: DialogEntrance::default(),
231 }
232 }
233}
234
235enum BaseDialogRoot {
236 Dialog(gpui_base::Dialog),
237 AlertDialog(gpui_base::AlertDialog),
238}
239
240macro_rules! map_base_root {
241 ($self:expr, $method:ident($($arg:expr),* $(,)?)) => {
242 match $self {
243 BaseDialogRoot::Dialog(root) => BaseDialogRoot::Dialog(root.$method($($arg),*)),
244 BaseDialogRoot::AlertDialog(root) => {
245 BaseDialogRoot::AlertDialog(root.$method($($arg),*))
246 }
247 }
248 };
249}
250
251impl BaseDialogRoot {
252 fn layer(self, index: usize, topmost: bool) -> Self {
253 map_base_root!(self, layer(index, topmost))
254 }
255 fn focus_handle(self, focus: FocusHandle) -> Self {
256 map_base_root!(self, focus_handle(focus))
257 }
258 fn close_on_escape(self, value: bool) -> Self {
259 map_base_root!(self, close_on_escape(value))
260 }
261 fn close_on_backdrop_press(self, value: bool) -> Self {
262 match self {
263 Self::Dialog(root) => Self::Dialog(root.close_on_backdrop_press(value)),
264 Self::AlertDialog(root) => Self::AlertDialog(root),
265 }
266 }
267 fn dismiss_below_y(self, value: Pixels) -> Self {
268 map_base_root!(self, dismiss_below_y(value))
269 }
270 fn backdrop(self, element: impl IntoElement) -> Self {
271 map_base_root!(self, backdrop(element))
272 }
273 fn popup(self, element: impl IntoElement) -> Self {
274 map_base_root!(self, popup(element))
275 }
276 fn on_ok(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static) -> Self {
277 map_base_root!(self, on_ok(handler))
278 }
279 fn on_cancel(
280 self,
281 handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
282 ) -> Self {
283 map_base_root!(self, on_cancel(handler))
284 }
285 fn on_close(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
286 map_base_root!(self, on_close(handler))
287 }
288 fn request_close(self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
289 map_base_root!(self, request_close(handler))
290 }
291}
292
293impl IntoElement for BaseDialogRoot {
294 type Element = <gpui_base::Dialog as IntoElement>::Element;
295 fn into_element(self) -> Self::Element {
296 match self {
297 Self::Dialog(root) => root.into_element(),
298 Self::AlertDialog(root) => root.into_element(),
299 }
300 }
301}
302
303#[derive(Clone, Copy, Debug)]
307pub(crate) struct DialogMotion {
308 pub(crate) visibility: f32,
309 pub(crate) closing: bool,
310}
311
312#[derive(IntoElement)]
314pub struct Dialog {
315 base: Option<BaseDialogRoot>,
316 pub(crate) style: StyleRefinement,
317 children: Vec<AnyElement>,
318 trigger: Option<AnyElement>,
319 title: Option<AnyElement>,
320 pub(crate) header: Option<AnyElement>,
321 pub(crate) footer: Option<AnyElement>,
322 pub(crate) content_builder: Option<ContentBuilderFn>,
323 pub(crate) props: DialogProps,
324
325 pub(super) button_props: DialogButtonProps,
326
327 pub(crate) focus_handle: FocusHandle,
329 pub(crate) layer_ix: usize,
330 pub(crate) selection_scope: TextSelectionScopeId,
331 pub(crate) motion: Option<DialogMotion>,
332 pub(crate) id: usize,
333}
334
335pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla {
336 if !overlay {
337 return hsla(0., 0., 0., 0.);
338 }
339
340 cx.theme().overlay
341}
342
343fn dialog_shadow(delta: f32) -> Vec<BoxShadow> {
344 vec![
345 BoxShadow {
346 color: hsla(0., 0., 0., 0.1 * delta),
347 offset: point(px(0.), px(20.)),
348 blur_radius: px(25.),
349 spread_radius: px(-5.),
350 inset: false,
351 },
352 BoxShadow {
353 color: hsla(0., 0., 0., 0.1 * delta),
354 offset: point(px(0.), px(8.)),
355 blur_radius: px(10.),
356 spread_radius: px(-6.),
357 inset: false,
358 },
359 ]
360}
361
362impl Dialog {
363 pub fn new(cx: &mut App) -> Self {
365 Self {
366 base: Some(BaseDialogRoot::Dialog(gpui_base::Dialog::new(cx))),
367 focus_handle: cx.focus_handle(),
368 style: StyleRefinement::default(),
369 trigger: None,
370 title: None,
371 header: None,
372 footer: None,
373 content_builder: None,
374 props: DialogProps::default(),
375 children: Vec::new(),
376 layer_ix: 0,
377 selection_scope: TextSelectionScopeId::default(),
378 button_props: DialogButtonProps::default(),
379 motion: None,
380 id: 0,
381 }
382 }
383
384 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
388 self.trigger = Some(trigger.into_any_element());
389 self
390 }
391
392 pub fn content<F>(mut self, builder: F) -> Self
394 where
395 F: Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static,
396 {
397 self.content_builder = Some(Rc::new(builder));
398 self
399 }
400
401 pub fn title(mut self, title: impl IntoElement) -> Self {
403 self.title = Some(title.into_any_element());
404 self
405 }
406
407 pub(crate) fn header(mut self, header: impl IntoElement) -> Self {
409 self.header = Some(header.into_any_element());
410 self
411 }
412
413 pub fn footer(mut self, footer: impl IntoElement) -> Self {
422 self.footer = Some(footer.into_any_element());
423 self
424 }
425
426 pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
438 self.button_props.merge(button_props);
439 self
440 }
441
442 pub(crate) fn with_base_alert_dialog(mut self, base: gpui_base::AlertDialog) -> Self {
443 self.base = Some(BaseDialogRoot::AlertDialog(base));
444 self.props.overlay_closable = false;
445 self
446 }
447
448 pub fn on_close(
452 mut self,
453 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
454 ) -> Self {
455 self.button_props.on_close = Some(Rc::new(on_close));
456 self
457 }
458
459 pub fn on_ok(
463 mut self,
464 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
465 ) -> Self {
466 self.button_props = self.button_props.on_ok(on_ok);
467 self
468 }
469
470 pub fn on_cancel(
474 mut self,
475 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
476 ) -> Self {
477 self.button_props = self.button_props.on_cancel(on_cancel);
478 self
479 }
480
481 pub fn close_button(mut self, close_button: bool) -> Self {
483 self.props.close_button = close_button;
484 self
485 }
486
487 pub fn margin_top(mut self, margin_top: impl Into<Pixels>) -> Self {
489 self.props.margin_top = Some(margin_top.into());
490 self
491 }
492
493 pub fn centered(mut self) -> Self {
496 self.props.centered = true;
497 self
498 }
499
500 pub fn w(mut self, width: impl Into<Pixels>) -> Self {
506 self.props.width = width.into();
507 self
508 }
509
510 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
514 self.props.width = width.into();
515 self
516 }
517
518 pub fn max_w(mut self, max_width: impl Into<Pixels>) -> Self {
520 self.props.max_width = Some(max_width.into());
521 self
522 }
523
524 pub fn overlay(mut self, overlay: bool) -> Self {
526 self.props.overlay = overlay;
527 self
528 }
529
530 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
534 self.props.overlay_closable = overlay_closable;
535 self
536 }
537
538 pub fn keyboard(mut self, keyboard: bool) -> Self {
540 self.props.keyboard = keyboard;
541 self
542 }
543
544 pub fn entrance(mut self, entrance: DialogEntrance) -> Self {
550 self.props.entrance = entrance;
551 self
552 }
553
554 pub(crate) fn exit_duration(&self, cx: &App) -> Duration {
559 if cx.reduce_motion() {
560 return Duration::ZERO;
561 }
562
563 match self.props.entrance {
564 DialogEntrance::None => Duration::ZERO,
565 DialogEntrance::SlideDown => *ANIMATION_DURATION,
566 _ => cx.theme().motion_tokens().duration_normal,
567 }
568 }
569
570 pub(crate) fn motion_transition(&self, cx: &App) -> Transition {
571 let transition = Transition::new(self.exit_duration(cx));
572 if self.props.entrance == DialogEntrance::SlideDown {
575 transition.ease(cubic_bezier(1. / 3., 0.72, 2. / 3., 1.))
576 } else {
577 let easing = cx.theme().motion_tokens().easing_enter.clone();
578 transition.ease(move |delta| easing.sample(delta).clamp(0., 1.))
579 }
580 }
581
582 pub(crate) fn has_overlay(&self) -> bool {
583 self.props.overlay
584 }
585
586 pub(crate) fn with_props(mut self, props: DialogProps) -> Self {
587 self.props = props;
588 self
589 }
590
591 fn defer_close_dialog(window: &mut Window, cx: &mut App) {
592 WindowState::update(window, cx, |root, window, cx| {
593 root.defer_close_dialog(window, cx);
594 });
595 }
596}
597
598impl ParentElement for Dialog {
599 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
600 self.children.extend(elements);
601 }
602}
603
604impl Styled for Dialog {
605 fn style(&mut self) -> &mut gpui::StyleRefinement {
606 &mut self.style
607 }
608}
609
610impl Dialog {
611 fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
612 let content_builder = self.content_builder.clone();
613 let style = self.style.clone();
614 let props = self.props.clone();
615 let button_props = self.button_props.clone();
616
617 gpui_base::DialogTrigger::new(trigger)
618 .on_open(move |window, cx| {
619 let content_builder = content_builder.clone();
620 let style = style.clone();
621 let props = props.clone();
622 let button_props = button_props.clone();
623 window.open_dialog(cx, move |dialog, _, _| {
624 dialog
625 .refine_style(&style)
626 .button_props(button_props.clone())
627 .with_props(props.clone())
628 .content({
629 let content_builder = content_builder.clone();
630 move |content, window, cx| {
631 if let Some(builder) = content_builder.clone() {
632 builder(content, window, cx)
633 } else {
634 content
635 }
636 }
637 })
638 });
639 })
640 .into_any_element()
641 }
642}
643
644impl RenderOnce for Dialog {
645 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
646 if let Some(trigger) = self.trigger.take() {
647 return self.render_trigger(trigger, window, cx);
648 }
649
650 let layer_ix = self.layer_ix;
651 let selection_scope = self.selection_scope;
652 let on_close = self.button_props.close_handler();
653 let on_ok = self.button_props.ok_handler();
654 let on_cancel = self.button_props.cancel_handler();
655
656 let window_paddings = crate::window_border::window_paddings(window);
657 let view_size = window.viewport_size()
658 - gpui::size(
659 window_paddings.left + window_paddings.right,
660 window_paddings.top + window_paddings.bottom,
661 );
662 let margin = cx.theme().spacing_tokens().lg;
668 let layer_offset = px(layer_ix as f32 * 16.);
669 let (anchor, y) = if self.props.centered {
670 (Anchor::LeftCenter, view_size.height / 2.)
671 } else {
672 let y = self.props.margin_top.unwrap_or(view_size.height / 10.);
673 (Anchor::TopLeft, y)
674 };
675 let y = y + layer_offset;
676 let entrance = self.props.entrance;
677 let motion = cx.theme().motion_tokens();
678 let closing = self.motion.is_some_and(|motion| motion.closing);
679 let visibility = self
680 .motion
681 .map_or(1.0, |motion| motion.visibility.clamp(0., 1.));
682 let fade_overlay = closing && self.props.overlay_visible;
683 let travel = motion.distance_short.to_pixels(window.rem_size());
684 let width = self
685 .props
686 .width
687 .min((view_size.width - margin * 2.).max(px(0.)));
688 let x = (view_size.width - width) / 2.;
689 let max_height = (view_size.height - margin * 2. - layer_offset).max(px(0.));
690
691 let base_size = window.text_style().font_size;
692 let rem_size = window.rem_size();
693
694 let mut paddings = Edges::all(px(16.));
695 if let Some(pl) = self.style.padding.left {
696 paddings.left = pl.to_pixels(base_size, rem_size);
697 }
698 if let Some(pr) = self.style.padding.right {
699 paddings.right = pr.to_pixels(base_size, rem_size);
700 }
701 if let Some(pt) = self.style.padding.top {
702 paddings.top = pt.to_pixels(base_size, rem_size);
703 }
704 if let Some(pb) = self.style.padding.bottom {
705 paddings.bottom = pb.to_pixels(base_size, rem_size);
706 }
707
708 let topmost = WindowState::read(window, cx)
711 .active_dialogs
712 .iter()
713 .rposition(|dialog| !dialog.closing);
714 let is_topmost = !closing && topmost == Some(layer_ix);
715
716 anchored()
717 .position(point(window_paddings.left, window_paddings.top))
718 .snap_to_window()
719 .child(
720 div()
721 .id("dialog")
722 .test_support()
723 .when(!closing, |this| this.occlude())
726 .w(view_size.width)
727 .h(view_size.height)
728 .when(fade_overlay, |this| {
729 this.child(div().absolute().inset_0().bg(overlay_color(true, cx)))
730 })
731 .child(
732 self.base
733 .take()
734 .expect("Dialog base host is always present")
735 .layer(self.id, is_topmost)
736 .focus_handle(self.focus_handle.clone())
737 .close_on_escape(self.props.keyboard && !closing)
738 .close_on_backdrop_press(self.props.overlay_closable && !closing)
739 .dismiss_below_y(TITLE_BAR_HEIGHT)
740 .when(self.props.overlay && !closing, |this| {
741 this.backdrop(
742 div()
743 .absolute()
744 .size_full()
745 .window_control_area(WindowControlArea::Drag)
746 .when(self.props.overlay_visible, |overlay| {
747 overlay.bg(overlay_color(true, cx))
748 }),
749 )
750 })
751 .on_ok(move |event, window, cx| !closing && on_ok(event, window, cx))
752 .on_cancel(move |event, window, cx| {
753 !closing && on_cancel(event, window, cx)
754 })
755 .on_close(move |event, window, cx| on_close(event, window, cx))
756 .request_close(move |deferred, window, cx| {
757 if closing {
760 return;
761 }
762 if deferred {
763 Self::defer_close_dialog(window, cx);
764 } else {
765 window.close_dialog(cx);
766 }
767 })
768 .popup(
769 gpui_base::Positioner::corner(
770 anchor,
771 point(window_paddings.left + x, window_paddings.top + y),
772 )
773 .margin(margin)
774 .child(
775 v_flex()
776 .id("surface")
777 .test_support()
778 .debug_selector(move || format!("dialog-{layer_ix}"))
779 .bg(cx.theme().tokens.background)
780 .border_1()
781 .border_color(cx.theme().border)
782 .rounded(cx.theme().radius_lg)
783 .min_h_24()
784 .pt(paddings.top)
785 .pb(paddings.bottom)
786 .gap(paddings.top.max(px(8.)))
787 .refine_style(&self.style)
788 .px_0()
789 .occlude()
790 .w(width)
791 .when_some(self.props.max_width, |this, w| this.max_w(w))
792 .max_h(max_height)
793 .child(
794 v_flex()
795 .flex_1()
796 .overflow_hidden()
797 .gap_y_2()
798 .when_some(self.header, |this, header| {
799 this.child(
800 div()
801 .pl(paddings.left)
802 .pr(paddings.right)
803 .child(header),
804 )
805 })
806 .when_some(self.title, |this, title| {
807 this.child(
808 DialogTitle::new()
809 .pl(paddings.left)
810 .pr(paddings.right)
811 .child(title),
812 )
813 })
814 .when_some(self.content_builder, |this, builder| {
815 this.child(builder(
816 DialogContent::new()
817 .gap(paddings.bottom)
818 .pl(paddings.left)
819 .pr(paddings.right),
820 window,
821 cx,
822 ))
823 })
824 .when(!self.children.is_empty(), |this| {
825 this.child(
826 div().flex_1().overflow_hidden().child(
827 v_flex()
829 .size_full()
830 .overflow_y_scrollbar()
831 .pl(paddings.left)
832 .pr(paddings.right)
833 .children(self.children),
834 ),
835 )
836 }),
837 )
838 .when_some(self.footer, |this, footer| {
839 this.child(
840 div()
841 .pl(paddings.left)
842 .pr(paddings.right)
843 .child(footer),
844 )
845 })
846 .children(self.props.close_button.then(|| {
847 let top = (paddings.top - px(10.)).max(px(8.));
848 let right = (paddings.right - px(10.)).max(px(8.));
849
850 gpui_base::DialogClose::new()
851 .absolute()
852 .top(top)
853 .right(right)
854 .trigger(|button| {
855 Button::new("close")
856 .with_base(button)
857 .small()
858 .ghost()
859 .icon(IconName::Close)
860 })
861 }))
862 .when(closing, |surface| {
863 surface
866 .capture_any_mouse_down(|_, _, cx| {
867 cx.stop_propagation()
868 })
869 .capture_any_mouse_up(|_, _, cx| {
870 cx.stop_propagation()
871 })
872 .capture_key_down(|_, window, cx| {
873 window.prevent_default();
874 cx.stop_propagation();
875 })
876 .capture_key_up(|_, _, cx| cx.stop_propagation())
877 .child(
878 div()
879 .absolute()
880 .inset_0()
881 .occlude()
882 .on_any_mouse_down(|_, _, cx| {
883 cx.stop_propagation()
884 })
885 .on_scroll(|_, _, cx| {
886 cx.stop_propagation()
887 }),
888 )
889 })
890 .opacity(visibility)
893 .shadow(dialog_shadow(visibility))
894 .text_selection_scope(selection_scope)
895 .map(|surface| {
896 if entrance == DialogEntrance::FadeSlide {
897 let mut surface = EntranceSurface::new(
898 surface.into_any_element(),
899 travel,
900 window_paddings.top + margin,
901 );
902 surface.progress = visibility;
903 surface.into_any_element()
904 } else {
905 surface.into_any_element()
906 }
907 }),
908 )
909 .when(
910 entrance == DialogEntrance::SlideDown,
911 |positioner| {
912 positioner.position(point(
913 window_paddings.left + x,
914 window_paddings.top + y * visibility,
915 ))
916 },
917 ),
918 ),
919 )
920 .opacity(visibility),
921 )
922 .into_any_element()
923 }
924}
925
926#[cfg(test)]
927pub(crate) mod tests {
928 use super::*;
929 use gpui::{
930 AppContext as _, Bounds, Context, Render, StatefulInteractiveElement as _, TestAppContext,
931 VisualTestContext, size,
932 };
933 use std::{cell::Cell, rc::Rc};
934
935 struct DialogHost {
936 clicks: Rc<Cell<usize>>,
937 }
938
939 impl Render for DialogHost {
940 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
941 let clicks = self.clicks.clone();
942 div().size_full().child(
943 div()
946 .id("background-target")
947 .absolute()
948 .top_0()
949 .left_0()
950 .w_full()
951 .h(px(200.))
952 .on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
953 )
954 }
955 }
956
957 pub(crate) fn window(
960 cx: &mut TestAppContext,
961 window_size: gpui::Size<Pixels>,
962 ) -> &mut VisualTestContext {
963 let (cx, _) = window_with_click_count(cx, window_size);
964 cx
965 }
966
967 fn window_with_click_count(
970 cx: &mut TestAppContext,
971 window_size: gpui::Size<Pixels>,
972 ) -> (&mut VisualTestContext, Rc<Cell<usize>>) {
973 cx.update(|cx| {
974 crate::init(cx);
975 cx.set_reduce_motion(true);
976 });
977 let clicks = Rc::new(Cell::new(0));
978 let counter = clicks.clone();
979 let (_, cx) = cx.add_window_view(|window, cx| {
980 let view = cx.new(|_| DialogHost { clicks: counter });
981 crate::Root::new(view, window, cx)
982 });
983 cx.simulate_resize(window_size);
984 cx.update(|window, cx| window.draw(cx).clear(cx));
985 (cx, clicks)
986 }
987
988 fn open(
989 cx: &mut VisualTestContext,
990 build: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
991 ) {
992 cx.update(|window, cx| window.open_dialog(cx, build));
993 cx.run_until_parked();
994 cx.update(|window, cx| window.draw(cx).clear(cx));
996 cx.update(|window, cx| window.draw(cx).clear(cx));
997 }
998
999 fn frame(cx: &mut VisualTestContext) {
1002 cx.update(|window, _| window.refresh());
1003 cx.run_until_parked();
1004 }
1005
1006 fn surface(cx: &mut VisualTestContext, layer_ix: usize) -> Bounds<Pixels> {
1007 let selector = ["dialog-0", "dialog-1"][layer_ix];
1008 cx.debug_bounds(selector)
1009 .unwrap_or_else(|| panic!("dialog layer {layer_ix} was not painted"))
1010 }
1011
1012 #[gpui::test]
1015 fn a_dialog_that_fits_keeps_its_default_width_and_top_offset(cx: &mut TestAppContext) {
1016 let cx = window(cx, size(px(1000.), px(800.)));
1017 open(cx, |dialog, _, _| dialog.title("Fits").child("body"));
1018
1019 let bounds = surface(cx, 0);
1020 assert_eq!(bounds.size.width, px(448.));
1021 assert_eq!(bounds.origin.x, px(276.));
1022 assert_eq!(bounds.origin.y, px(80.));
1023 }
1024
1025 #[gpui::test]
1030 fn a_dialog_larger_than_the_window_stays_inside_it(cx: &mut TestAppContext) {
1031 let viewport = size(px(400.), px(300.));
1032 let cx = window(cx, viewport);
1033 open(cx, |dialog, _, _| {
1034 dialog
1035 .w(px(800.))
1036 .title("Too big")
1037 .child(div().h(px(1000.)).child("tall body"))
1038 .footer(div().h(px(32.)).debug_selector(|| "footer-probe".into()))
1039 });
1040
1041 let bounds = surface(cx, 0);
1042 let footer = cx.debug_bounds("footer-probe").unwrap();
1043 let margin = px(16.);
1044 assert!(
1045 bounds.origin.x >= margin && bounds.right() <= viewport.width - margin,
1046 "the dialog ran off the sides: {bounds:?}"
1047 );
1048 assert!(
1049 bounds.bottom() <= viewport.height - margin,
1050 "the dialog ran off the bottom: {bounds:?}"
1051 );
1052 assert_eq!(bounds.origin.y, margin);
1053 assert!(
1054 footer.bottom() <= bounds.bottom(),
1055 "the footer was clipped below the dialog: footer {footer:?}, dialog {bounds:?}"
1056 );
1057 }
1058
1059 #[gpui::test]
1061 fn dialog_button_props_merge_with_what_the_dialog_already_carries(cx: &mut TestAppContext) {
1062 let cx = window(cx, size(px(400.), px(300.)));
1063 cx.update(|_, cx| {
1064 let dialog = Dialog::new(cx)
1065 .button_props(DialogButtonProps::default().cancel_text("Keep"))
1066 .button_props(DialogButtonProps::default().ok_text("Delete"))
1067 .button_props(DialogButtonProps::default().ok_variant(ButtonVariant::Danger));
1068
1069 assert_eq!(dialog.button_props.ok_text.as_deref(), Some("Delete"));
1070 assert_eq!(dialog.button_props.cancel_text.as_deref(), Some("Keep"));
1071 assert_eq!(dialog.button_props.ok_variant, Some(ButtonVariant::Danger));
1072 });
1073 }
1074
1075 #[gpui::test]
1078 fn stacked_dialogs_each_fit_the_window(cx: &mut TestAppContext) {
1079 let viewport = size(px(400.), px(300.));
1080 let cx = window(cx, viewport);
1081 open(cx, |dialog, _, _| {
1082 dialog.title("First").child(div().h(px(1000.)))
1083 });
1084 open(cx, |dialog, _, _| {
1085 dialog.title("Second").child(div().h(px(1000.)))
1086 });
1087
1088 let first = surface(cx, 0);
1089 let second = surface(cx, 1);
1090 assert_eq!(second.origin.y, first.origin.y + px(16.));
1091 assert!(first.bottom() <= viewport.height - px(16.), "{first:?}");
1092 assert!(second.bottom() <= viewport.height - px(16.), "{second:?}");
1093 assert!(second.size.height < first.size.height);
1094 }
1095
1096 #[gpui::test]
1097 fn test_dialog_entrance_builder(cx: &mut TestAppContext) {
1098 cx.update(crate::init);
1099 cx.update(|cx| {
1100 assert_eq!(Dialog::new(cx).props.entrance, DialogEntrance::SlideDown);
1101 for entrance in [
1102 DialogEntrance::SlideDown,
1103 DialogEntrance::Fade,
1104 DialogEntrance::FadeSlide,
1105 DialogEntrance::None,
1106 ] {
1107 assert_eq!(Dialog::new(cx).entrance(entrance).props.entrance, entrance);
1108 }
1109 });
1110 }
1111
1112 #[gpui::test]
1113 fn reduced_motion_displays_every_entrance_at_rest(cx: &mut TestAppContext) {
1114 for entrance in [
1115 DialogEntrance::SlideDown,
1116 DialogEntrance::Fade,
1117 DialogEntrance::FadeSlide,
1118 DialogEntrance::None,
1119 ] {
1120 let cx = window(cx, size(px(1000.), px(800.)));
1121 open(cx, move |dialog, _, _| {
1122 dialog
1123 .title("Entrance")
1124 .child("body")
1125 .entrance(entrance)
1126 .margin_top(px(300.))
1127 });
1128 assert_eq!(surface(cx, 0).top(), px(300.));
1129 }
1130 }
1131
1132 #[gpui::test]
1133 fn no_entrance_displays_immediately_and_keeps_keyboard_dismissal(cx: &mut TestAppContext) {
1134 let cx = window(cx, size(px(1000.), px(800.)));
1135 cx.update(|_, cx| cx.set_reduce_motion(false));
1136 open(cx, |dialog, _, _| {
1137 dialog
1138 .title("Immediate")
1139 .child("body")
1140 .entrance(DialogEntrance::None)
1141 .margin_top(px(300.))
1142 });
1143 assert_eq!(surface(cx, 0).top(), px(300.));
1144 cx.simulate_keystrokes("escape");
1145 cx.update(|window, cx| assert!(WindowState::read(window, cx).active_dialogs.is_empty()));
1146 }
1147
1148 #[gpui::test]
1151 fn closing_plays_the_entrance_backwards_before_the_dialog_unmounts(cx: &mut TestAppContext) {
1152 let cx = window(cx, size(px(1000.), px(800.)));
1153 cx.update(|_, cx| cx.set_reduce_motion(false));
1154 open(cx, |dialog, _, _| {
1155 dialog.title("Closing").child("body").margin_top(px(300.))
1156 });
1157
1158 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1159 frame(cx);
1160 cx.update(|window, cx| window.close_dialog(cx));
1161 frame(cx);
1162 cx.update(|window, cx| {
1165 assert_eq!(WindowState::read(window, cx).active_dialogs.len(), 1);
1166 assert!(!window.has_active_dialog(cx));
1167 });
1168 assert_eq!(surface(cx, 0).top(), px(300.));
1169
1170 cx.background_executor
1172 .advance_clock(*ANIMATION_DURATION / 2);
1173 frame(cx);
1174 cx.update(|window, cx| assert_eq!(WindowState::read(window, cx).active_dialogs.len(), 1));
1175 let halfway = surface(cx, 0).top();
1176 assert!(
1177 halfway > px(0.) && halfway < px(300.),
1178 "the closing dialog should slide back up: {halfway:?}"
1179 );
1180
1181 cx.background_executor
1183 .advance_clock(*ANIMATION_DURATION / 2 + Duration::from_millis(1));
1184 frame(cx);
1185 cx.update(|window, cx| {
1186 assert!(WindowState::read(window, cx).active_dialogs.is_empty());
1187 assert!(!window.has_active_dialog(cx));
1188 });
1189 assert!(cx.debug_bounds("dialog-0").is_none());
1190 }
1191
1192 #[gpui::test]
1193 fn reduced_motion_closes_in_a_single_frame(cx: &mut TestAppContext) {
1194 let cx = window(cx, size(px(1000.), px(800.)));
1195 open(cx, |dialog, _, _| dialog.title("Reduced").child("body"));
1196 cx.update(|window, cx| window.close_dialog(cx));
1197 cx.update(|window, cx| {
1198 assert!(WindowState::read(window, cx).active_dialogs.is_empty());
1199 assert!(!window.has_active_dialog(cx));
1200 });
1201 }
1202
1203 #[gpui::test]
1204 fn zero_theme_duration_closes_fade_entrances_immediately(cx: &mut TestAppContext) {
1205 for entrance in [DialogEntrance::Fade, DialogEntrance::FadeSlide] {
1206 let cx = window(cx, size(px(1000.), px(800.)));
1207 cx.update(|_, cx| {
1208 cx.set_reduce_motion(false);
1209 crate::Theme::update(cx, |theme| theme.motion.duration_normal = Duration::ZERO);
1210 });
1211 open(cx, move |dialog, _, _| {
1212 dialog.title("Immediate").child("body").entrance(entrance)
1213 });
1214 cx.update(|window, cx| window.close_dialog(cx));
1215 cx.update(|window, cx| {
1216 assert!(WindowState::read(window, cx).active_dialogs.is_empty());
1217 assert!(!window.has_active_dialog(cx));
1218 });
1219 }
1220 }
1221
1222 #[gpui::test]
1225 fn reopening_during_the_exit_keeps_the_new_dialog(cx: &mut TestAppContext) {
1226 let cx = window(cx, size(px(1000.), px(800.)));
1227 cx.update(|_, cx| cx.set_reduce_motion(false));
1228 open(cx, |dialog, _, _| dialog.title("First").child("body"));
1229 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1230 frame(cx);
1231 cx.update(|window, cx| window.close_dialog(cx));
1232 frame(cx);
1233
1234 open(cx, |dialog, _, _| dialog.title("Second").child("body"));
1235 cx.update(|window, cx| {
1236 let state = WindowState::read(window, cx);
1237 assert_eq!(state.active_dialogs.len(), 2);
1238 assert!(state.active_dialogs[0].closing);
1239 assert!(!state.active_dialogs[1].closing);
1240 assert!(
1241 state.active_dialogs[1].focus_handle().is_focused(window),
1242 "the reopened dialog owns focus while the old one leaves"
1243 );
1244 });
1245
1246 cx.background_executor
1247 .advance_clock(*ANIMATION_DURATION + Duration::from_millis(1));
1248 frame(cx);
1249 frame(cx);
1251 cx.update(|window, cx| {
1252 let state = WindowState::read(window, cx);
1253 assert_eq!(state.active_dialogs.len(), 1);
1254 assert!(!state.active_dialogs[0].closing);
1255 assert!(state.active_dialogs[0].focus_handle().is_focused(window));
1256 assert!(window.has_active_dialog(cx));
1257 });
1258 assert_eq!(surface(cx, 0).top(), px(80.));
1259 assert!(cx.debug_bounds("dialog-1").is_none());
1260 }
1261
1262 #[gpui::test]
1265 fn closing_the_top_dialog_leaves_the_one_below_mounted(cx: &mut TestAppContext) {
1266 let cx = window(cx, size(px(1000.), px(800.)));
1267 cx.update(|_, cx| cx.set_reduce_motion(false));
1268 open(cx, |dialog, _, _| dialog.title("First").child("body"));
1269 open(cx, |dialog, _, _| dialog.title("Second").child("body"));
1270
1271 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1272 frame(cx);
1273 cx.update(|window, cx| window.close_dialog(cx));
1274 frame(cx);
1275 cx.update(|window, cx| {
1276 assert_eq!(WindowState::read(window, cx).active_dialogs.len(), 2);
1277 assert!(window.has_active_dialog(cx));
1278 });
1279 assert!(
1280 cx.debug_bounds("dialog-0").is_some(),
1281 "the lower dialog stays"
1282 );
1283 assert!(
1284 cx.debug_bounds("dialog-1").is_some(),
1285 "the closing dialog still paints its exit"
1286 );
1287
1288 cx.background_executor
1289 .advance_clock(*ANIMATION_DURATION + Duration::from_millis(1));
1290 frame(cx);
1291 cx.update(|window, cx| {
1292 assert_eq!(WindowState::read(window, cx).active_dialogs.len(), 1);
1293 assert!(window.has_active_dialog(cx));
1294 });
1295 assert!(cx.debug_bounds("dialog-0").is_some());
1296 assert!(cx.debug_bounds("dialog-1").is_none());
1297 }
1298
1299 #[gpui::test]
1302 fn closing_dialog_does_not_block_input_beneath_it(cx: &mut TestAppContext) {
1303 let (cx, clicks) = window_with_click_count(cx, size(px(1000.), px(800.)));
1304 cx.update(|_, cx| cx.set_reduce_motion(false));
1305 open(cx, |dialog, _, _| {
1306 dialog
1307 .title("Closing")
1308 .child("body")
1309 .margin_top(px(300.))
1310 .overlay_closable(false)
1311 });
1312
1313 let backdrop_point = point(px(100.), px(60.));
1316 cx.simulate_click(backdrop_point, gpui::Modifiers::default());
1317 assert_eq!(clicks.get(), 0, "an open dialog blocked the application");
1318
1319 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1320 frame(cx);
1321 cx.update(|window, cx| window.close_dialog(cx));
1322 frame(cx);
1323 cx.simulate_click(backdrop_point, gpui::Modifiers::default());
1324 assert_eq!(
1325 clicks.get(),
1326 1,
1327 "the closing dialog swallowed a press meant for the application"
1328 );
1329 }
1330
1331 #[gpui::test]
1332 fn zero_theme_duration_displays_fade_entrances_immediately(cx: &mut TestAppContext) {
1333 for entrance in [DialogEntrance::Fade, DialogEntrance::FadeSlide] {
1334 let cx = window(cx, size(px(1000.), px(800.)));
1335 cx.update(|_, cx| {
1336 cx.set_reduce_motion(false);
1337 crate::Theme::update(cx, |theme| theme.motion.duration_normal = Duration::ZERO);
1338 });
1339 open(cx, move |dialog, _, _| {
1340 dialog
1341 .title("Immediate")
1342 .child("body")
1343 .entrance(entrance)
1344 .margin_top(px(300.))
1345 });
1346 assert_eq!(surface(cx, 0).top(), px(300.));
1347 }
1348 }
1349
1350 #[gpui::test]
1351 fn alert_dialog_uses_the_same_entrance(cx: &mut TestAppContext) {
1352 for entrance in [
1353 DialogEntrance::SlideDown,
1354 DialogEntrance::Fade,
1355 DialogEntrance::FadeSlide,
1356 DialogEntrance::None,
1357 ] {
1358 let cx = window(cx, size(px(1000.), px(800.)));
1359 cx.update(|window, cx| {
1360 let alert = crate::dialog::AlertDialog::new(cx)
1361 .title("Alert")
1362 .entrance(entrance);
1363 assert_eq!(alert.build_surface(window, cx).props.entrance, entrance);
1364 window.open_alert_dialog(cx, move |alert, _, _| {
1365 alert.title("Alert").entrance(entrance)
1366 });
1367 });
1368 cx.run_until_parked();
1369 cx.update(|window, cx| window.draw(cx).clear(cx));
1370 cx.update(|window, cx| window.draw(cx).clear(cx));
1371 assert_eq!(surface(cx, 0).top(), px(80.));
1372 }
1373 }
1374
1375 #[gpui::test]
1378 fn closing_fades_the_dialog_surface(cx: &mut TestAppContext) {
1379 let cx = window(cx, size(px(1000.), px(800.)));
1380 cx.update(|_, cx| cx.set_reduce_motion(false));
1381 open(cx, |dialog, _, _| {
1382 dialog
1383 .title("Fading")
1384 .child("body")
1385 .margin_top(px(300.))
1386 .entrance(DialogEntrance::Fade)
1387 });
1388 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1389 frame(cx);
1390 cx.update(|window, cx| window.close_dialog(cx));
1391 frame(cx);
1392 assert_eq!(
1393 surface_alpha(cx),
1394 1.0,
1395 "the exit should start fully visible"
1396 );
1397
1398 cx.background_executor
1399 .advance_clock(Duration::from_millis(90));
1400 frame(cx);
1401 let alpha = surface_alpha(cx);
1402 assert!(
1403 alpha > 0.0 && alpha < 1.0,
1404 "the closing surface should fade, but its background is {alpha}"
1405 );
1406 }
1407
1408 #[gpui::test]
1409 fn closing_during_entrance_reverses_from_the_current_sample(cx: &mut TestAppContext) {
1410 for entrance in [
1411 DialogEntrance::SlideDown,
1412 DialogEntrance::Fade,
1413 DialogEntrance::FadeSlide,
1414 ] {
1415 let cx = window(cx, size(px(1000.), px(800.)));
1416 cx.update(|_, cx| cx.set_reduce_motion(false));
1417 open(cx, move |dialog, _, _| {
1418 dialog
1419 .title("Interrupted")
1420 .child("body")
1421 .margin_top(px(300.))
1422 .entrance(entrance)
1423 });
1424 cx.background_executor
1425 .advance_clock(Duration::from_millis(40));
1426 frame(cx);
1427 let before = surface(cx, 0);
1428 let alpha = surface_alpha(cx);
1429 assert!(alpha > 0. && alpha < 1.);
1430 cx.update(|window, cx| window.close_dialog(cx));
1431 frame(cx);
1432 assert_eq!(surface(cx, 0), before, "{entrance:?} jumped on close");
1433 assert!((surface_alpha(cx) - alpha).abs() < 0.001);
1434 cx.background_executor
1435 .advance_clock(Duration::from_millis(20));
1436 frame(cx);
1437 assert!(surface_alpha(cx) < alpha);
1438 assert!(surface(cx, 0).top() <= before.top());
1439 }
1440 }
1441
1442 #[gpui::test]
1443 fn exiting_content_rejects_clicks_focus_and_text_input(cx: &mut TestAppContext) {
1444 use crate::input::{Input, InputState};
1445 use gpui::Focusable as _;
1446
1447 for deferred in [false, true] {
1448 let cx = window(cx, size(px(1000.), px(800.)));
1449 let input = cx.update(|window, cx| cx.new(|cx| InputState::new(window, cx)));
1450 let clicks = Rc::new(Cell::new(0));
1451 let counter = clicks.clone();
1452 let field = input.clone();
1453 cx.update(|_, cx| cx.set_reduce_motion(false));
1454 open(cx, move |dialog, _, _| {
1455 let counter = counter.clone();
1456 dialog
1457 .title("Form")
1458 .child(
1459 div()
1460 .debug_selector(|| "exit-input".into())
1461 .child(Input::new(&field)),
1462 )
1463 .footer(
1464 div().debug_selector(|| "exit-button".into()).child(
1465 Button::new("custom")
1466 .label("Custom action")
1467 .w_full()
1468 .on_click(move |_, _, _| counter.set(counter.get() + 1)),
1469 ),
1470 )
1471 });
1472 cx.background_executor.advance_clock(*ANIMATION_DURATION);
1473 frame(cx);
1474 let button = cx.debug_bounds("exit-button").unwrap().center();
1475 cx.simulate_click(button, gpui::Modifiers::default());
1476 assert_eq!(clicks.get(), 1);
1477 cx.update(|window, cx| input.focus_handle(cx).focus(window, cx));
1478 cx.simulate_input("before");
1479 assert_eq!(input.read_with(cx, |input, _| input.value()), "before");
1480 cx.update(|window, cx| {
1481 if deferred {
1482 Dialog::defer_close_dialog(window, cx);
1483 } else {
1484 window.close_dialog(cx);
1485 }
1486 });
1487 frame(cx);
1488 let button = cx.debug_bounds("exit-button").unwrap().center();
1489 cx.simulate_click(button, gpui::Modifiers::default());
1490 assert_eq!(clicks.get(), 1, "a closing footer button ran its callback");
1491 let field = cx.debug_bounds("exit-input").unwrap().center();
1492 cx.simulate_click(field, gpui::Modifiers::default());
1493 cx.simulate_input("after");
1494 assert_eq!(input.read_with(cx, |input, _| input.value()), "before");
1495 for key in ["tab", "shift-tab"] {
1496 cx.simulate_keystrokes(key);
1497 frame(cx);
1498 cx.update(|window, cx| assert!(!input.focus_handle(cx).is_focused(window)));
1499 }
1500 cx.update(|window, cx| input.focus_handle(cx).focus(window, cx));
1502 frame(cx);
1503 cx.update(|window, cx| assert!(!input.focus_handle(cx).is_focused(window)));
1504 cx.simulate_input("after");
1505 assert_eq!(input.read_with(cx, |input, _| input.value()), "before");
1506 }
1507 }
1508
1509 fn surface_alpha(cx: &mut VisualTestContext) -> f32 {
1511 let bounds = cx.debug_bounds("dialog-0").unwrap();
1512 let scale = cx.update(|window, _| window.scale_factor());
1513 let left = f32::from(bounds.origin.x) * scale;
1514 let top = f32::from(bounds.origin.y) * scale;
1515 let width = f32::from(bounds.size.width) * scale;
1516 let height = f32::from(bounds.size.height) * scale;
1517 cx.update(|window, _| {
1518 window
1519 .painted_quads()
1520 .into_iter()
1521 .filter(|quad| {
1522 (quad.bounds.origin.x.0 - left).abs() < 1.0
1523 && (quad.bounds.origin.y.0 - top).abs() < 1.0
1524 && (quad.bounds.size.width.0 - width).abs() < 1.0
1525 && (quad.bounds.size.height.0 - height).abs() < 1.0
1526 })
1527 .filter_map(|quad| quad.background.as_solid().map(|color| color.a))
1528 .fold(0.0_f32, f32::max)
1529 })
1530 }
1531}