Skip to main content

gpui_base/
dialog.rs

1use crate::TestSupportExt as _;
2use std::{
3    cell::{Cell, RefCell},
4    rc::Rc,
5};
6
7use gpui::{
8    AnyElement, App, ClickEvent, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding,
9    MouseButton, ParentElement, Pixels, RenderOnce, Role, StatefulInteractiveElement as _,
10    StyleRefinement, Styled, Window, anchored, deferred, div, point, prelude::FluentBuilder as _,
11    px,
12};
13use smallvec::SmallVec;
14
15use crate::actions::{Cancel, Confirm};
16use crate::{FocusTrapElement as _, StyledExt as _};
17
18const CONTEXT: &str = "Dialog";
19type Decision = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
20type Closed = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
21type CloseRequest = Rc<dyn Fn(bool, &mut Window, &mut App)>;
22type OpenRequest = Rc<dyn Fn(&mut Window, &mut App)>;
23type OpenChange = Rc<dyn Fn(bool, DialogChangeReason, &mut Window, &mut App)>;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum DialogChangeReason {
27    TriggerPress,
28    BackdropPress,
29    Cancel,
30    Confirm,
31    Imperative,
32}
33
34#[derive(Clone)]
35pub struct DialogHandle {
36    open: Rc<Cell<bool>>,
37    on_open_change: Rc<RefCell<Option<OpenChange>>>,
38}
39
40impl DialogHandle {
41    pub fn new(open: bool) -> Self {
42        Self {
43            open: Rc::new(Cell::new(open)),
44            on_open_change: Rc::new(RefCell::new(None)),
45        }
46    }
47    pub fn is_open(&self) -> bool {
48        self.open.get()
49    }
50    pub fn open(&self, window: &mut Window, cx: &mut App) {
51        self.set_open(true, DialogChangeReason::Imperative, window, cx);
52    }
53    pub fn close(&self, window: &mut Window, cx: &mut App) {
54        self.set_open(false, DialogChangeReason::Imperative, window, cx);
55    }
56    pub(crate) fn set_open(
57        &self,
58        open: bool,
59        reason: DialogChangeReason,
60        window: &mut Window,
61        cx: &mut App,
62    ) {
63        if self.open.replace(open) == open {
64            return;
65        }
66        let callback = self.on_open_change.borrow().clone();
67        if let Some(callback) = callback {
68            callback(open, reason, window, cx);
69        }
70        window.refresh();
71    }
72}
73
74fn request_open_change(
75    handle: &Option<DialogHandle>,
76    callback: &Option<OpenChange>,
77    open: bool,
78    reason: DialogChangeReason,
79    window: &mut Window,
80    cx: &mut App,
81) {
82    if let Some(handle) = handle {
83        handle.set_open(open, reason, window, cx);
84    } else if let Some(callback) = callback {
85        callback(open, reason, window, cx);
86    }
87}
88
89pub fn init(cx: &mut App) {
90    cx.bind_keys([
91        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
92        KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
93    ]);
94}
95
96impl Dialog {
97    pub fn on_ok(
98        mut self,
99        handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
100    ) -> Self {
101        self.on_ok = Rc::new(handler);
102        self
103    }
104
105    pub fn on_cancel(
106        mut self,
107        handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
108    ) -> Self {
109        self.on_cancel = Rc::new(handler);
110        self
111    }
112
113    pub fn on_close(
114        mut self,
115        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
116    ) -> Self {
117        self.on_close = Rc::new(handler);
118        self
119    }
120}
121
122/// Unstyled modal host owning focus, keyboard actions, dismissal, and callback ordering.
123#[derive(IntoElement)]
124pub struct Dialog {
125    style: StyleRefinement,
126    focus: FocusHandle,
127    role: Role,
128    layer: usize,
129    keyboard: bool,
130    overlay_closable: bool,
131    topmost: bool,
132    dismiss_below_y: Pixels,
133    backdrop: Option<AnyElement>,
134    popup: Option<AnyElement>,
135    children: SmallVec<[AnyElement; 2]>,
136    on_ok: Decision,
137    on_cancel: Decision,
138    on_close: Closed,
139    request_close: CloseRequest,
140    handle: Option<DialogHandle>,
141    open: bool,
142    on_open_change: Option<OpenChange>,
143}
144
145/// Unstyled trigger that owns pointer activation for opening a dialog.
146#[derive(IntoElement)]
147pub struct DialogTrigger {
148    trigger: AnyElement,
149    open: OpenRequest,
150    handle: Option<DialogHandle>,
151}
152
153impl DialogTrigger {
154    pub fn new(trigger: impl IntoElement) -> Self {
155        Self {
156            trigger: trigger.into_any_element(),
157            open: Rc::new(|_, _| {}),
158            handle: None,
159        }
160    }
161    pub fn handle(mut self, handle: DialogHandle) -> Self {
162        self.handle = Some(handle);
163        self
164    }
165
166    pub fn on_open(mut self, open: impl Fn(&mut Window, &mut App) + 'static) -> Self {
167        self.open = Rc::new(open);
168        self
169    }
170}
171
172impl RenderOnce for DialogTrigger {
173    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
174        div()
175            .on_mouse_down(MouseButton::Left, move |_, window, cx| {
176                if let Some(handle) = self.handle.as_ref() {
177                    handle.set_open(true, DialogChangeReason::TriggerPress, window, cx);
178                }
179                (self.open)(window, cx);
180                cx.stop_propagation();
181            })
182            .child(self.trigger)
183    }
184}
185
186macro_rules! dialog_part {
187    ($(#[$meta:meta])* $name:ident, $id:literal) => {
188        $(#[$meta])*
189        #[derive(IntoElement)]
190        pub struct $name {
191            style: StyleRefinement,
192            children: SmallVec<[AnyElement; 2]>,
193        }
194
195        impl $name {
196            pub fn new() -> Self {
197                Self {
198                    style: StyleRefinement::default(),
199                    children: SmallVec::new(),
200                }
201            }
202        }
203
204        impl Default for $name {
205            fn default() -> Self {
206                Self::new()
207            }
208        }
209
210        impl Styled for $name {
211            fn style(&mut self) -> &mut StyleRefinement {
212                &mut self.style
213            }
214        }
215
216        impl ParentElement for $name {
217            fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
218                self.children.extend(elements);
219            }
220        }
221
222        impl RenderOnce for $name {
223            fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
224                div()
225                    .id($id)
226                    .children(self.children)
227                    .refine_style(&self.style)
228            }
229        }
230    };
231}
232
233dialog_part!(
234    /// Unstyled backdrop part rendered behind a dialog popup.
235    DialogBackdrop,
236    "dialog-backdrop"
237);
238
239dialog_part!(
240    /// Unstyled popup part containing dialog content.
241    DialogPopup,
242    "dialog-popup"
243);
244
245/// Unstyled title slot for a dialog surface.
246#[derive(IntoElement)]
247pub struct DialogTitle {
248    base: gpui::Div,
249    style: StyleRefinement,
250    children: SmallVec<[AnyElement; 2]>,
251}
252
253impl DialogTitle {
254    pub fn new() -> Self {
255        Self {
256            base: div(),
257            style: StyleRefinement::default(),
258            children: SmallVec::new(),
259        }
260    }
261}
262
263impl Default for DialogTitle {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268impl Styled for DialogTitle {
269    fn style(&mut self) -> &mut StyleRefinement {
270        &mut self.style
271    }
272}
273impl ParentElement for DialogTitle {
274    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
275        self.children.extend(elements);
276    }
277}
278impl RenderOnce for DialogTitle {
279    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
280        self.base
281            .id("dialog-title")
282            .children(self.children)
283            .refine_style(&self.style)
284    }
285}
286
287/// Unstyled descriptive-content slot for a dialog surface.
288#[derive(IntoElement)]
289pub struct DialogDescription {
290    base: gpui::Div,
291    style: StyleRefinement,
292    children: SmallVec<[AnyElement; 2]>,
293}
294
295impl DialogDescription {
296    pub fn new() -> Self {
297        Self {
298            base: div(),
299            style: StyleRefinement::default(),
300            children: SmallVec::new(),
301        }
302    }
303}
304
305impl Default for DialogDescription {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310impl Styled for DialogDescription {
311    fn style(&mut self) -> &mut StyleRefinement {
312        &mut self.style
313    }
314}
315impl ParentElement for DialogDescription {
316    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
317        self.children.extend(elements);
318    }
319}
320impl RenderOnce for DialogDescription {
321    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
322        self.base
323            .id("dialog-description")
324            .children(self.children)
325            .refine_style(&self.style)
326    }
327}
328
329/// Wrapper that dispatches the dialog cancel action when activated.
330#[derive(IntoElement)]
331pub struct DialogClose {
332    style: StyleRefinement,
333    children: SmallVec<[AnyElement; 1]>,
334    trigger: Option<AnyElement>,
335    /// The focus node activation dispatches from, filled at render and read
336    /// when clicked, so the trigger built earlier reaches it too.
337    anchor: Rc<RefCell<Option<FocusHandle>>>,
338}
339
340impl DialogClose {
341    pub fn new() -> Self {
342        Self {
343            style: StyleRefinement::default(),
344            children: SmallVec::new(),
345            trigger: None,
346            anchor: Rc::default(),
347        }
348    }
349
350    /// Styles a button with the accessible name "Close" and cancel activation.
351    ///
352    /// The supplied button already supports pointer, keyboard, and accessibility
353    /// activation. The builder only needs to supply presentation.
354    /// The wrapper does not also handle clicks when a trigger is supplied.
355    pub fn trigger<E: IntoElement>(mut self, build: impl FnOnce(crate::Button) -> E) -> Self {
356        let anchor = self.anchor.clone();
357        let button = crate::Button::new("close")
358            .accessibility_label("Close")
359            .on_click(move |_, window, cx| Self::activate(&anchor, window, cx));
360        self.trigger = Some(build(button).into_any_element());
361        self
362    }
363
364    /// Dispatches [`Cancel`] from the control's own focus node, so it reaches
365    /// the dialog the control sits in whatever holds focus at that moment: a
366    /// surface that keeps taking focus back (a native web view, an
367    /// always-on-top window) would otherwise leave the control inert.
368    fn activate(anchor: &RefCell<Option<FocusHandle>>, window: &mut Window, cx: &mut App) {
369        // Clone out before dispatching so the dialog's handlers never run
370        // while the cell is borrowed.
371        let anchor = anchor.borrow().clone();
372        match anchor {
373            Some(anchor) => anchor.dispatch_action(&Cancel, window, cx),
374            None => window.dispatch_action(Box::new(Cancel), cx),
375        }
376    }
377}
378impl Default for DialogClose {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383impl ParentElement for DialogClose {
384    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
385        self.children.extend(elements);
386    }
387}
388impl Styled for DialogClose {
389    fn style(&mut self) -> &mut StyleRefinement {
390        &mut self.style
391    }
392}
393impl RenderOnce for DialogClose {
394    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
395        let anchor = window
396            .use_keyed_state("dialog-close-anchor", cx, |_, cx| cx.focus_handle())
397            .read(cx)
398            .clone();
399        *self.anchor.borrow_mut() = Some(anchor.clone());
400        let cell = self.anchor;
401        div()
402            .id("dialog-close")
403            // A zero-size, out-of-flow node that tracks the anchor: it is never
404            // hovered, so it takes no focus on its own and does not enter the
405            // Tab order, but it sits inside this dialog's dispatch path.
406            .child(div().absolute().size_0().track_focus(&anchor))
407            .when(self.trigger.is_none(), |this| {
408                this.on_click(move |_, window, cx| Self::activate(&cell, window, cx))
409            })
410            .children(self.trigger)
411            .children(self.children)
412            .refine_style(&self.style)
413    }
414}
415
416impl Dialog {
417    pub fn new(cx: &mut App) -> Self {
418        Self {
419            style: StyleRefinement::default(),
420            focus: cx.focus_handle(),
421            role: Role::Dialog,
422            layer: 0,
423            keyboard: true,
424            overlay_closable: true,
425            topmost: true,
426            dismiss_below_y: px(0.),
427            backdrop: None,
428            popup: None,
429            children: SmallVec::new(),
430            on_ok: Rc::new(|_, _, _| true),
431            on_cancel: Rc::new(|_, _, _| true),
432            on_close: Rc::new(|_, _, _| {}),
433            request_close: Rc::new(|_, _, _| {}),
434            handle: None,
435            open: true,
436            on_open_change: None,
437        }
438    }
439    pub fn open(mut self, open: bool) -> Self {
440        self.open = open;
441        self
442    }
443    pub fn handle(mut self, handle: DialogHandle) -> Self {
444        if let Some(callback) = self.on_open_change.as_ref() {
445            *handle.on_open_change.borrow_mut() = Some(callback.clone());
446        }
447        self.handle = Some(handle);
448        self
449    }
450    pub fn on_open_change(
451        mut self,
452        handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
453    ) -> Self {
454        let handler: OpenChange = Rc::new(handler);
455        if let Some(handle) = self.handle.as_ref() {
456            *handle.on_open_change.borrow_mut() = Some(handler.clone());
457        }
458        self.on_open_change = Some(handler);
459        self
460    }
461
462    pub fn backdrop(mut self, element: impl IntoElement) -> Self {
463        self.backdrop = Some(element.into_any_element());
464        self
465    }
466    pub fn popup(mut self, element: impl IntoElement) -> Self {
467        self.popup = Some(element.into_any_element());
468        self
469    }
470    pub fn close_on_escape(mut self, value: bool) -> Self {
471        self.keyboard = value;
472        self
473    }
474    pub fn close_on_backdrop_press(mut self, value: bool) -> Self {
475        self.overlay_closable = value;
476        self
477    }
478    pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
479        self.dismiss_below_y = value;
480        self
481    }
482    pub(crate) fn role(mut self, role: Role) -> Self {
483        self.role = role;
484        self
485    }
486    #[doc(hidden)]
487    pub fn layer(mut self, index: usize, topmost: bool) -> Self {
488        self.layer = index;
489        self.topmost = topmost;
490        self
491    }
492    #[doc(hidden)]
493    pub fn focus_handle(mut self, value: FocusHandle) -> Self {
494        self.focus = value;
495        self
496    }
497    #[doc(hidden)]
498    pub fn request_close(
499        mut self,
500        handler: impl Fn(bool, &mut Window, &mut App) + 'static,
501    ) -> Self {
502        self.request_close = Rc::new(handler);
503        self
504    }
505}
506
507impl Styled for Dialog {
508    fn style(&mut self) -> &mut StyleRefinement {
509        &mut self.style
510    }
511}
512impl ParentElement for Dialog {
513    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
514        self.children.extend(elements);
515    }
516}
517
518impl RenderOnce for Dialog {
519    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
520        let open = self
521            .handle
522            .as_ref()
523            .map_or(self.open, DialogHandle::is_open);
524        if !open {
525            return div().into_any_element();
526        }
527        let request_close = self.request_close;
528        let cancel = self.on_cancel.clone();
529        let confirm = self.on_ok.clone();
530        let closed = self.on_close.clone();
531        let overlay_closable = self.overlay_closable && self.topmost;
532        let dismiss_below_y = self.dismiss_below_y;
533        let escape_handle = self.handle.clone();
534        let confirm_handle = self.handle.clone();
535        let backdrop_handle = self.handle.clone();
536        let escape_change = self.on_open_change.clone();
537        let confirm_change = self.on_open_change.clone();
538        let backdrop_change = self.on_open_change.clone();
539        let viewport = window.viewport_size();
540
541        deferred(
542            anchored().position(point(px(0.), px(0.))).child(
543                div()
544                    .id(("dialog-host", self.layer))
545                    .test_support()
546                    .absolute()
547                    .top_0()
548                    .left_0()
549                    .w(viewport.width)
550                    .h(viewport.height)
551                    .role(self.role)
552                    .track_focus(&self.focus)
553                    .focus_trap(format!("dialog-{}", self.layer), &self.focus)
554                    .when(self.keyboard, |this| this.key_context(CONTEXT))
555                    .map(|this| {
556                        let request_cancel = request_close.clone();
557                        let request_confirm = request_close.clone();
558                        let closed_cancel = closed.clone();
559                        this.on_action(move |_: &Cancel, window, cx| {
560                            let event = ClickEvent::default();
561                            if cancel(&event, window, cx) {
562                                request_open_change(
563                                    &escape_handle,
564                                    &escape_change,
565                                    false,
566                                    DialogChangeReason::Cancel,
567                                    window,
568                                    cx,
569                                );
570                                request_cancel(false, window, cx);
571                                closed_cancel(&event, window, cx);
572                            }
573                        })
574                        .on_action(move |_: &Confirm, window, cx| {
575                            let event = ClickEvent::default();
576                            if confirm(&event, window, cx) {
577                                request_open_change(
578                                    &confirm_handle,
579                                    &confirm_change,
580                                    false,
581                                    DialogChangeReason::Confirm,
582                                    window,
583                                    cx,
584                                );
585                                request_confirm(true, window, cx);
586                                closed(&event, window, cx);
587                            }
588                        })
589                    })
590                    .when_some(self.backdrop, |this, backdrop| {
591                        let cancel = self.on_cancel.clone();
592                        let closed = self.on_close.clone();
593                        let request_close = request_close.clone();
594                        this.child(
595                            div()
596                                // The backdrop covers the host, so a caller's
597                                // `absolute()` surface has a box to fill.
598                                .absolute()
599                                .inset_0()
600                                .on_any_mouse_down(move |event, window, cx| {
601                                    if event.position.y < dismiss_below_y {
602                                        return;
603                                    }
604                                    let button = event.button;
605                                    cx.stop_propagation();
606                                    let event = ClickEvent::default();
607                                    if button == MouseButton::Left
608                                        && overlay_closable
609                                        && cancel(&event, window, cx)
610                                    {
611                                        request_open_change(
612                                            &backdrop_handle,
613                                            &backdrop_change,
614                                            false,
615                                            DialogChangeReason::BackdropPress,
616                                            window,
617                                            cx,
618                                        );
619                                        request_close(false, window, cx);
620                                        closed(&event, window, cx);
621                                    }
622                                })
623                                .child(backdrop),
624                        )
625                    })
626                    .children(self.popup)
627                    .children(self.children)
628                    .refine_style(&self.style),
629            ),
630        )
631        .with_priority(10 + self.layer)
632        .into_any_element()
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use gpui::{Context, Render, point};
640    use std::{cell::RefCell, rc::Rc};
641
642    #[gpui::test]
643    fn close_trigger_supplies_accessible_button(cx: &mut gpui::TestAppContext) {
644        use gpui::{Element as _, accesskit, canvas};
645        use std::sync::{Arc, Mutex};
646
647        struct Probe(Arc<Mutex<Option<accesskit::Node>>>);
648        impl Render for Probe {
649            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
650                let captured = self.0.clone();
651                canvas(
652                    move |_, window, cx| {
653                        DialogClose::new().trigger(|button| {
654                            let element = button.render(window, cx).into_element();
655                            let mut node = accesskit::Node::new(element.a11y_role().unwrap());
656                            element.write_a11y_info(&mut node);
657                            *captured.lock().unwrap() = Some(node);
658                            element
659                        });
660                    },
661                    |_, _, _, _| {},
662                )
663            }
664        }
665
666        let captured = Arc::new(Mutex::new(None));
667        let result = captured.clone();
668        let (_, cx) = cx.add_window_view(move |_, _| Probe(captured));
669        cx.update(|window, cx| window.draw(cx).clear(cx));
670        let node = result.lock().unwrap().take().unwrap();
671        assert_eq!(node.role(), Role::Button);
672        assert_eq!(node.label(), Some("Close"));
673        assert!(node.supports_action(accesskit::Action::Click));
674    }
675
676    #[gpui::test]
677    fn close_trigger_activates_once_and_respects_cancel_veto(cx: &mut gpui::TestAppContext) {
678        use gpui::{KeyDownEvent, KeyUpEvent, Keystroke};
679
680        struct Harness {
681            focus: FocusHandle,
682            button_focus: FocusHandle,
683            handle: DialogHandle,
684            attempts: Rc<Cell<usize>>,
685        }
686        impl Render for Harness {
687            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
688                let attempts = self.attempts.clone();
689                let button_focus = self.button_focus.clone();
690                Dialog::new(cx)
691                    .handle(self.handle.clone())
692                    .focus_handle(self.focus.clone())
693                    .on_cancel(move |_, _, _| {
694                        attempts.set(attempts.get() + 1);
695                        attempts.get() > 1
696                    })
697                    .popup(
698                        DialogClose::new().trigger(move |button| {
699                            button.size(px(100.)).track_focus(&button_focus)
700                        }),
701                    )
702            }
703        }
704
705        cx.update(crate::init);
706        let handle = DialogHandle::new(true);
707        let attempts = Rc::new(Cell::new(0));
708        let (view, cx) = cx.add_window_view({
709            let handle = handle.clone();
710            let attempts = attempts.clone();
711            move |_, cx| Harness {
712                focus: cx.focus_handle(),
713                button_focus: cx.focus_handle(),
714                handle,
715                attempts,
716            }
717        });
718        cx.update(|window, cx| {
719            view.read(cx).focus.clone().focus(window, cx);
720            window.draw(cx).clear(cx);
721        });
722        cx.simulate_click(point(px(20.), px(20.)), Default::default());
723        cx.run_until_parked();
724        assert_eq!(attempts.get(), 1);
725        assert!(handle.is_open(), "on_cancel can veto pointer dismissal");
726
727        cx.update(|window, cx| {
728            view.read(cx).button_focus.clone().focus(window, cx);
729            window.draw(cx).clear(cx);
730        });
731        let keystroke = Keystroke::parse("space").unwrap();
732        cx.simulate_event(KeyDownEvent {
733            keystroke: keystroke.clone(),
734            is_held: false,
735            prefer_character_input: false,
736        });
737        cx.simulate_event(KeyUpEvent { keystroke });
738        cx.run_until_parked();
739        assert_eq!(attempts.get(), 2);
740        assert!(!handle.is_open(), "Space uses the same cancel decision");
741    }
742
743    struct TriggerHarness {
744        handle: DialogHandle,
745    }
746    impl Render for TriggerHarness {
747        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
748            DialogTrigger::new(div().size(px(100.))).handle(self.handle.clone())
749        }
750    }
751
752    #[gpui::test]
753    fn trigger_opens_shared_handle_and_reports_reason(cx: &mut gpui::TestAppContext) {
754        let handle = DialogHandle::new(false);
755        let changes = Rc::new(RefCell::new(Vec::new()));
756        *handle.on_open_change.borrow_mut() = Some({
757            let changes = changes.clone();
758            Rc::new(move |open, reason, _, _| changes.borrow_mut().push((open, reason)))
759        });
760        let (_, cx) = cx.add_window_view({
761            let handle = handle.clone();
762            move |_, _| TriggerHarness { handle }
763        });
764        cx.update(|window, cx| window.draw(cx).clear(cx));
765        cx.simulate_click(point(px(20.), px(20.)), Default::default());
766
767        assert!(handle.is_open());
768        assert_eq!(
769            &*changes.borrow(),
770            &[(true, DialogChangeReason::TriggerPress)]
771        );
772    }
773
774    /// The backdrop is the dimming surface every caller hands over as an
775    /// `absolute()` element, so it has to have the host's box to resolve
776    /// against — a collapsed wrapper leaves it zero-sized and invisible.
777    #[gpui::test]
778    fn the_backdrop_fills_the_host(cx: &mut gpui::TestAppContext) {
779        use gpui::{Bounds, canvas};
780        use std::cell::Cell;
781
782        struct Harness {
783            focus: FocusHandle,
784            bounds: Rc<Cell<Bounds<Pixels>>>,
785        }
786        impl Render for Harness {
787            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
788                let bounds = self.bounds.clone();
789                Dialog::new(cx)
790                    .open(true)
791                    .focus_handle(self.focus.clone())
792                    .backdrop(
793                        canvas(
794                            move |bounds_of_backdrop, _, _| bounds.set(bounds_of_backdrop),
795                            |_, _, _, _| {},
796                        )
797                        .absolute()
798                        .size_full(),
799                    )
800                    .popup(div().size(px(100.)))
801            }
802        }
803
804        cx.update(crate::init);
805        let bounds = Rc::new(Cell::new(Bounds::default()));
806        let (_, cx) = cx.add_window_view({
807            let bounds = bounds.clone();
808            move |_, cx| Harness {
809                focus: cx.focus_handle(),
810                bounds,
811            }
812        });
813        let viewport = cx.update(|window, cx| {
814            let viewport = window.viewport_size();
815            window.draw(cx).clear(cx);
816            viewport
817        });
818
819        assert_eq!(
820            bounds.get().size,
821            viewport,
822            "a zero-sized backdrop paints no overlay behind the dialog"
823        );
824    }
825}