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}
336
337impl DialogClose {
338    pub fn new() -> Self {
339        Self {
340            style: StyleRefinement::default(),
341            children: SmallVec::new(),
342            trigger: None,
343        }
344    }
345
346    /// Styles a button with the accessible name "Close" and cancel activation.
347    ///
348    /// The supplied button already supports pointer, keyboard, and accessibility
349    /// activation. The builder only needs to supply presentation.
350    /// The wrapper does not also handle clicks when a trigger is supplied.
351    pub fn trigger<E: IntoElement>(mut self, build: impl FnOnce(crate::Button) -> E) -> Self {
352        let button = crate::Button::new("close")
353            .accessibility_label("Close")
354            .on_click(Self::activate);
355        self.trigger = Some(build(button).into_any_element());
356        self
357    }
358
359    fn activate(_: &ClickEvent, window: &mut Window, cx: &mut App) {
360        window.dispatch_action(Box::new(Cancel), cx);
361    }
362}
363impl Default for DialogClose {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368impl ParentElement for DialogClose {
369    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
370        self.children.extend(elements);
371    }
372}
373impl Styled for DialogClose {
374    fn style(&mut self) -> &mut StyleRefinement {
375        &mut self.style
376    }
377}
378impl RenderOnce for DialogClose {
379    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
380        div()
381            .id("dialog-close")
382            .when(self.trigger.is_none(), |this| this.on_click(Self::activate))
383            .children(self.trigger)
384            .children(self.children)
385            .refine_style(&self.style)
386    }
387}
388
389impl Dialog {
390    pub fn new(cx: &mut App) -> Self {
391        Self {
392            style: StyleRefinement::default(),
393            focus: cx.focus_handle(),
394            role: Role::Dialog,
395            layer: 0,
396            keyboard: true,
397            overlay_closable: true,
398            topmost: true,
399            dismiss_below_y: px(0.),
400            backdrop: None,
401            popup: None,
402            children: SmallVec::new(),
403            on_ok: Rc::new(|_, _, _| true),
404            on_cancel: Rc::new(|_, _, _| true),
405            on_close: Rc::new(|_, _, _| {}),
406            request_close: Rc::new(|_, _, _| {}),
407            handle: None,
408            open: true,
409            on_open_change: None,
410        }
411    }
412    pub fn open(mut self, open: bool) -> Self {
413        self.open = open;
414        self
415    }
416    pub fn handle(mut self, handle: DialogHandle) -> Self {
417        if let Some(callback) = self.on_open_change.as_ref() {
418            *handle.on_open_change.borrow_mut() = Some(callback.clone());
419        }
420        self.handle = Some(handle);
421        self
422    }
423    pub fn on_open_change(
424        mut self,
425        handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
426    ) -> Self {
427        let handler: OpenChange = Rc::new(handler);
428        if let Some(handle) = self.handle.as_ref() {
429            *handle.on_open_change.borrow_mut() = Some(handler.clone());
430        }
431        self.on_open_change = Some(handler);
432        self
433    }
434
435    pub fn backdrop(mut self, element: impl IntoElement) -> Self {
436        self.backdrop = Some(element.into_any_element());
437        self
438    }
439    pub fn popup(mut self, element: impl IntoElement) -> Self {
440        self.popup = Some(element.into_any_element());
441        self
442    }
443    pub fn close_on_escape(mut self, value: bool) -> Self {
444        self.keyboard = value;
445        self
446    }
447    pub fn close_on_backdrop_press(mut self, value: bool) -> Self {
448        self.overlay_closable = value;
449        self
450    }
451    pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
452        self.dismiss_below_y = value;
453        self
454    }
455    pub(crate) fn role(mut self, role: Role) -> Self {
456        self.role = role;
457        self
458    }
459    #[doc(hidden)]
460    pub fn layer(mut self, index: usize, topmost: bool) -> Self {
461        self.layer = index;
462        self.topmost = topmost;
463        self
464    }
465    #[doc(hidden)]
466    pub fn focus_handle(mut self, value: FocusHandle) -> Self {
467        self.focus = value;
468        self
469    }
470    #[doc(hidden)]
471    pub fn request_close(
472        mut self,
473        handler: impl Fn(bool, &mut Window, &mut App) + 'static,
474    ) -> Self {
475        self.request_close = Rc::new(handler);
476        self
477    }
478}
479
480impl Styled for Dialog {
481    fn style(&mut self) -> &mut StyleRefinement {
482        &mut self.style
483    }
484}
485impl ParentElement for Dialog {
486    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
487        self.children.extend(elements);
488    }
489}
490
491impl RenderOnce for Dialog {
492    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
493        let open = self
494            .handle
495            .as_ref()
496            .map_or(self.open, DialogHandle::is_open);
497        if !open {
498            return div().into_any_element();
499        }
500        let request_close = self.request_close;
501        let cancel = self.on_cancel.clone();
502        let confirm = self.on_ok.clone();
503        let closed = self.on_close.clone();
504        let overlay_closable = self.overlay_closable && self.topmost;
505        let dismiss_below_y = self.dismiss_below_y;
506        let escape_handle = self.handle.clone();
507        let confirm_handle = self.handle.clone();
508        let backdrop_handle = self.handle.clone();
509        let escape_change = self.on_open_change.clone();
510        let confirm_change = self.on_open_change.clone();
511        let backdrop_change = self.on_open_change.clone();
512        let viewport = window.viewport_size();
513
514        deferred(
515            anchored().position(point(px(0.), px(0.))).child(
516                div()
517                    .id(("dialog-host", self.layer))
518                    .test_support()
519                    .absolute()
520                    .top_0()
521                    .left_0()
522                    .w(viewport.width)
523                    .h(viewport.height)
524                    .role(self.role)
525                    .track_focus(&self.focus)
526                    .focus_trap(format!("dialog-{}", self.layer), &self.focus)
527                    .when(self.keyboard, |this| this.key_context(CONTEXT))
528                    .map(|this| {
529                        let request_cancel = request_close.clone();
530                        let request_confirm = request_close.clone();
531                        let closed_cancel = closed.clone();
532                        this.on_action(move |_: &Cancel, window, cx| {
533                            let event = ClickEvent::default();
534                            if cancel(&event, window, cx) {
535                                request_open_change(
536                                    &escape_handle,
537                                    &escape_change,
538                                    false,
539                                    DialogChangeReason::Cancel,
540                                    window,
541                                    cx,
542                                );
543                                request_cancel(false, window, cx);
544                                closed_cancel(&event, window, cx);
545                            }
546                        })
547                        .on_action(move |_: &Confirm, window, cx| {
548                            let event = ClickEvent::default();
549                            if confirm(&event, window, cx) {
550                                request_open_change(
551                                    &confirm_handle,
552                                    &confirm_change,
553                                    false,
554                                    DialogChangeReason::Confirm,
555                                    window,
556                                    cx,
557                                );
558                                request_confirm(true, window, cx);
559                                closed(&event, window, cx);
560                            }
561                        })
562                    })
563                    .when_some(self.backdrop, |this, backdrop| {
564                        let cancel = self.on_cancel.clone();
565                        let closed = self.on_close.clone();
566                        let request_close = request_close.clone();
567                        this.child(
568                            div()
569                                // The backdrop covers the host, so a caller's
570                                // `absolute()` surface has a box to fill.
571                                .absolute()
572                                .inset_0()
573                                .on_any_mouse_down(move |event, window, cx| {
574                                    if event.position.y < dismiss_below_y {
575                                        return;
576                                    }
577                                    let button = event.button;
578                                    cx.stop_propagation();
579                                    let event = ClickEvent::default();
580                                    if button == MouseButton::Left
581                                        && overlay_closable
582                                        && cancel(&event, window, cx)
583                                    {
584                                        request_open_change(
585                                            &backdrop_handle,
586                                            &backdrop_change,
587                                            false,
588                                            DialogChangeReason::BackdropPress,
589                                            window,
590                                            cx,
591                                        );
592                                        request_close(false, window, cx);
593                                        closed(&event, window, cx);
594                                    }
595                                })
596                                .child(backdrop),
597                        )
598                    })
599                    .children(self.popup)
600                    .children(self.children)
601                    .refine_style(&self.style),
602            ),
603        )
604        .with_priority(10 + self.layer)
605        .into_any_element()
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    use gpui::{Context, Render, point};
613    use std::{cell::RefCell, rc::Rc};
614
615    #[gpui::test]
616    fn close_trigger_supplies_accessible_button(cx: &mut gpui::TestAppContext) {
617        use gpui::{Element as _, accesskit, canvas};
618        use std::sync::{Arc, Mutex};
619
620        struct Probe(Arc<Mutex<Option<accesskit::Node>>>);
621        impl Render for Probe {
622            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
623                let captured = self.0.clone();
624                canvas(
625                    move |_, window, cx| {
626                        DialogClose::new().trigger(|button| {
627                            let element = button.render(window, cx).into_element();
628                            let mut node = accesskit::Node::new(element.a11y_role().unwrap());
629                            element.write_a11y_info(&mut node);
630                            *captured.lock().unwrap() = Some(node);
631                            element
632                        });
633                    },
634                    |_, _, _, _| {},
635                )
636            }
637        }
638
639        let captured = Arc::new(Mutex::new(None));
640        let result = captured.clone();
641        let (_, cx) = cx.add_window_view(move |_, _| Probe(captured));
642        cx.update(|window, cx| window.draw(cx).clear(cx));
643        let node = result.lock().unwrap().take().unwrap();
644        assert_eq!(node.role(), Role::Button);
645        assert_eq!(node.label(), Some("Close"));
646        assert!(node.supports_action(accesskit::Action::Click));
647    }
648
649    #[gpui::test]
650    fn close_trigger_activates_once_and_respects_cancel_veto(cx: &mut gpui::TestAppContext) {
651        use gpui::{KeyDownEvent, KeyUpEvent, Keystroke};
652
653        struct Harness {
654            focus: FocusHandle,
655            button_focus: FocusHandle,
656            handle: DialogHandle,
657            attempts: Rc<Cell<usize>>,
658        }
659        impl Render for Harness {
660            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
661                let attempts = self.attempts.clone();
662                let button_focus = self.button_focus.clone();
663                Dialog::new(cx)
664                    .handle(self.handle.clone())
665                    .focus_handle(self.focus.clone())
666                    .on_cancel(move |_, _, _| {
667                        attempts.set(attempts.get() + 1);
668                        attempts.get() > 1
669                    })
670                    .popup(
671                        DialogClose::new().trigger(move |button| {
672                            button.size(px(100.)).track_focus(&button_focus)
673                        }),
674                    )
675            }
676        }
677
678        cx.update(crate::init);
679        let handle = DialogHandle::new(true);
680        let attempts = Rc::new(Cell::new(0));
681        let (view, cx) = cx.add_window_view({
682            let handle = handle.clone();
683            let attempts = attempts.clone();
684            move |_, cx| Harness {
685                focus: cx.focus_handle(),
686                button_focus: cx.focus_handle(),
687                handle,
688                attempts,
689            }
690        });
691        cx.update(|window, cx| {
692            view.read(cx).focus.clone().focus(window, cx);
693            window.draw(cx).clear(cx);
694        });
695        cx.simulate_click(point(px(20.), px(20.)), Default::default());
696        cx.run_until_parked();
697        assert_eq!(attempts.get(), 1);
698        assert!(handle.is_open(), "on_cancel can veto pointer dismissal");
699
700        cx.update(|window, cx| {
701            view.read(cx).button_focus.clone().focus(window, cx);
702            window.draw(cx).clear(cx);
703        });
704        let keystroke = Keystroke::parse("space").unwrap();
705        cx.simulate_event(KeyDownEvent {
706            keystroke: keystroke.clone(),
707            is_held: false,
708            prefer_character_input: false,
709        });
710        cx.simulate_event(KeyUpEvent { keystroke });
711        cx.run_until_parked();
712        assert_eq!(attempts.get(), 2);
713        assert!(!handle.is_open(), "Space uses the same cancel decision");
714    }
715
716    struct TriggerHarness {
717        handle: DialogHandle,
718    }
719    impl Render for TriggerHarness {
720        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
721            DialogTrigger::new(div().size(px(100.))).handle(self.handle.clone())
722        }
723    }
724
725    #[gpui::test]
726    fn trigger_opens_shared_handle_and_reports_reason(cx: &mut gpui::TestAppContext) {
727        let handle = DialogHandle::new(false);
728        let changes = Rc::new(RefCell::new(Vec::new()));
729        *handle.on_open_change.borrow_mut() = Some({
730            let changes = changes.clone();
731            Rc::new(move |open, reason, _, _| changes.borrow_mut().push((open, reason)))
732        });
733        let (_, cx) = cx.add_window_view({
734            let handle = handle.clone();
735            move |_, _| TriggerHarness { handle }
736        });
737        cx.update(|window, cx| window.draw(cx).clear(cx));
738        cx.simulate_click(point(px(20.), px(20.)), Default::default());
739
740        assert!(handle.is_open());
741        assert_eq!(
742            &*changes.borrow(),
743            &[(true, DialogChangeReason::TriggerPress)]
744        );
745    }
746
747    /// The backdrop is the dimming surface every caller hands over as an
748    /// `absolute()` element, so it has to have the host's box to resolve
749    /// against — a collapsed wrapper leaves it zero-sized and invisible.
750    #[gpui::test]
751    fn the_backdrop_fills_the_host(cx: &mut gpui::TestAppContext) {
752        use gpui::{Bounds, canvas};
753        use std::cell::Cell;
754
755        struct Harness {
756            focus: FocusHandle,
757            bounds: Rc<Cell<Bounds<Pixels>>>,
758        }
759        impl Render for Harness {
760            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
761                let bounds = self.bounds.clone();
762                Dialog::new(cx)
763                    .open(true)
764                    .focus_handle(self.focus.clone())
765                    .backdrop(
766                        canvas(
767                            move |bounds_of_backdrop, _, _| bounds.set(bounds_of_backdrop),
768                            |_, _, _, _| {},
769                        )
770                        .absolute()
771                        .size_full(),
772                    )
773                    .popup(div().size(px(100.)))
774            }
775        }
776
777        cx.update(crate::init);
778        let bounds = Rc::new(Cell::new(Bounds::default()));
779        let (_, cx) = cx.add_window_view({
780            let bounds = bounds.clone();
781            move |_, cx| Harness {
782                focus: cx.focus_handle(),
783                bounds,
784            }
785        });
786        let viewport = cx.update(|window, cx| {
787            let viewport = window.viewport_size();
788            window.draw(cx).clear(cx);
789            viewport
790        });
791
792        assert_eq!(
793            bounds.get().size,
794            viewport,
795            "a zero-sized backdrop paints no overlay behind the dialog"
796        );
797    }
798}