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        dialog_part!($(#[$meta])* $name, $id, false);
189    };
190    ($(#[$meta:meta])* $name:ident, $id:literal, $occlude:literal) => {
191        $(#[$meta])*
192        #[derive(IntoElement)]
193        pub struct $name {
194            style: StyleRefinement,
195            children: SmallVec<[AnyElement; 2]>,
196        }
197
198        impl $name {
199            pub fn new() -> Self {
200                Self {
201                    style: StyleRefinement::default(),
202                    children: SmallVec::new(),
203                }
204            }
205        }
206
207        impl Default for $name {
208            fn default() -> Self {
209                Self::new()
210            }
211        }
212
213        impl Styled for $name {
214            fn style(&mut self) -> &mut StyleRefinement {
215                &mut self.style
216            }
217        }
218
219        impl ParentElement for $name {
220            fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
221                self.children.extend(elements);
222            }
223        }
224
225        impl RenderOnce for $name {
226            fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
227                div()
228                    .id($id)
229                    // The popup sits over the backdrop, whose press dismisses
230                    // the dialog, so it keeps presses on itself from falling through.
231                    .when($occlude, |this| this.occlude())
232                    .children(self.children)
233                    .refine_style(&self.style)
234            }
235        }
236    };
237}
238
239dialog_part!(
240    /// Unstyled backdrop part rendered behind a dialog popup.
241    DialogBackdrop,
242    "dialog-backdrop"
243);
244
245dialog_part!(
246    /// Unstyled popup part containing dialog content.
247    DialogPopup,
248    "dialog-popup",
249    true
250);
251
252/// Unstyled title slot for a dialog surface.
253#[derive(IntoElement)]
254pub struct DialogTitle {
255    base: gpui::Div,
256    style: StyleRefinement,
257    children: SmallVec<[AnyElement; 2]>,
258}
259
260impl DialogTitle {
261    pub fn new() -> Self {
262        Self {
263            base: div(),
264            style: StyleRefinement::default(),
265            children: SmallVec::new(),
266        }
267    }
268}
269
270impl Default for DialogTitle {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275impl Styled for DialogTitle {
276    fn style(&mut self) -> &mut StyleRefinement {
277        &mut self.style
278    }
279}
280impl ParentElement for DialogTitle {
281    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
282        self.children.extend(elements);
283    }
284}
285impl RenderOnce for DialogTitle {
286    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
287        self.base
288            .id("dialog-title")
289            .children(self.children)
290            .refine_style(&self.style)
291    }
292}
293
294/// Unstyled descriptive-content slot for a dialog surface.
295#[derive(IntoElement)]
296pub struct DialogDescription {
297    base: gpui::Div,
298    style: StyleRefinement,
299    children: SmallVec<[AnyElement; 2]>,
300}
301
302impl DialogDescription {
303    pub fn new() -> Self {
304        Self {
305            base: div(),
306            style: StyleRefinement::default(),
307            children: SmallVec::new(),
308        }
309    }
310}
311
312impl Default for DialogDescription {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317impl Styled for DialogDescription {
318    fn style(&mut self) -> &mut StyleRefinement {
319        &mut self.style
320    }
321}
322impl ParentElement for DialogDescription {
323    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
324        self.children.extend(elements);
325    }
326}
327impl RenderOnce for DialogDescription {
328    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
329        self.base
330            .id("dialog-description")
331            .children(self.children)
332            .refine_style(&self.style)
333    }
334}
335
336/// Wrapper that dispatches the dialog cancel action when activated.
337#[derive(IntoElement)]
338pub struct DialogClose {
339    style: StyleRefinement,
340    children: SmallVec<[AnyElement; 1]>,
341    trigger: Option<AnyElement>,
342    /// The focus node activation dispatches from, filled at render and read
343    /// when clicked, so the trigger built earlier reaches it too.
344    anchor: Rc<RefCell<Option<FocusHandle>>>,
345}
346
347impl DialogClose {
348    pub fn new() -> Self {
349        Self {
350            style: StyleRefinement::default(),
351            children: SmallVec::new(),
352            trigger: None,
353            anchor: Rc::default(),
354        }
355    }
356
357    /// Styles a button with the accessible name "Close" and cancel activation.
358    ///
359    /// The supplied button already supports pointer, keyboard, and accessibility
360    /// activation. The builder only needs to supply presentation.
361    /// The wrapper does not also handle clicks when a trigger is supplied.
362    pub fn trigger<E: IntoElement>(mut self, build: impl FnOnce(crate::Button) -> E) -> Self {
363        let anchor = self.anchor.clone();
364        let button = crate::Button::new("close")
365            .accessibility_label("Close")
366            .on_click(move |_, window, cx| Self::activate(&anchor, window, cx));
367        self.trigger = Some(build(button).into_any_element());
368        self
369    }
370
371    /// Dispatches [`Cancel`] from the control's own focus node, so it reaches
372    /// the dialog the control sits in whatever holds focus at that moment: a
373    /// surface that keeps taking focus back (a native web view, an
374    /// always-on-top window) would otherwise leave the control inert.
375    fn activate(anchor: &RefCell<Option<FocusHandle>>, window: &mut Window, cx: &mut App) {
376        // Clone out before dispatching so the dialog's handlers never run
377        // while the cell is borrowed.
378        let anchor = anchor.borrow().clone();
379        match anchor {
380            Some(anchor) => anchor.dispatch_action(&Cancel, window, cx),
381            None => window.dispatch_action(Box::new(Cancel), cx),
382        }
383    }
384}
385impl Default for DialogClose {
386    fn default() -> Self {
387        Self::new()
388    }
389}
390impl ParentElement for DialogClose {
391    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
392        self.children.extend(elements);
393    }
394}
395impl Styled for DialogClose {
396    fn style(&mut self) -> &mut StyleRefinement {
397        &mut self.style
398    }
399}
400impl RenderOnce for DialogClose {
401    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
402        let anchor = window
403            .use_keyed_state("dialog-close-anchor", cx, |_, cx| cx.focus_handle())
404            .read(cx)
405            .clone();
406        *self.anchor.borrow_mut() = Some(anchor.clone());
407        let cell = self.anchor;
408        div()
409            .id("dialog-close")
410            // A zero-size, out-of-flow node that tracks the anchor: it is never
411            // hovered, so it takes no focus on its own and does not enter the
412            // Tab order, but it sits inside this dialog's dispatch path.
413            .child(div().absolute().size_0().track_focus(&anchor))
414            .when(self.trigger.is_none(), |this| {
415                this.on_click(move |_, window, cx| Self::activate(&cell, window, cx))
416            })
417            .children(self.trigger)
418            .children(self.children)
419            .refine_style(&self.style)
420    }
421}
422
423impl Dialog {
424    pub fn new(cx: &mut App) -> Self {
425        Self {
426            style: StyleRefinement::default(),
427            focus: cx.focus_handle(),
428            role: Role::Dialog,
429            layer: 0,
430            keyboard: true,
431            overlay_closable: true,
432            topmost: true,
433            dismiss_below_y: px(0.),
434            backdrop: None,
435            popup: None,
436            children: SmallVec::new(),
437            on_ok: Rc::new(|_, _, _| true),
438            on_cancel: Rc::new(|_, _, _| true),
439            on_close: Rc::new(|_, _, _| {}),
440            request_close: Rc::new(|_, _, _| {}),
441            handle: None,
442            open: true,
443            on_open_change: None,
444        }
445    }
446    pub fn open(mut self, open: bool) -> Self {
447        self.open = open;
448        self
449    }
450    pub fn handle(mut self, handle: DialogHandle) -> Self {
451        if let Some(callback) = self.on_open_change.as_ref() {
452            *handle.on_open_change.borrow_mut() = Some(callback.clone());
453        }
454        self.handle = Some(handle);
455        self
456    }
457    pub fn on_open_change(
458        mut self,
459        handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
460    ) -> Self {
461        let handler: OpenChange = Rc::new(handler);
462        if let Some(handle) = self.handle.as_ref() {
463            *handle.on_open_change.borrow_mut() = Some(handler.clone());
464        }
465        self.on_open_change = Some(handler);
466        self
467    }
468
469    pub fn backdrop(mut self, element: impl IntoElement) -> Self {
470        self.backdrop = Some(element.into_any_element());
471        self
472    }
473    pub fn popup(mut self, element: impl IntoElement) -> Self {
474        self.popup = Some(element.into_any_element());
475        self
476    }
477    pub fn close_on_escape(mut self, value: bool) -> Self {
478        self.keyboard = value;
479        self
480    }
481    pub fn close_on_backdrop_press(mut self, value: bool) -> Self {
482        self.overlay_closable = value;
483        self
484    }
485    pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
486        self.dismiss_below_y = value;
487        self
488    }
489    pub(crate) fn role(mut self, role: Role) -> Self {
490        self.role = role;
491        self
492    }
493    #[doc(hidden)]
494    pub fn layer(mut self, index: usize, topmost: bool) -> Self {
495        self.layer = index;
496        self.topmost = topmost;
497        self
498    }
499    #[doc(hidden)]
500    pub fn focus_handle(mut self, value: FocusHandle) -> Self {
501        self.focus = value;
502        self
503    }
504    #[doc(hidden)]
505    pub fn request_close(
506        mut self,
507        handler: impl Fn(bool, &mut Window, &mut App) + 'static,
508    ) -> Self {
509        self.request_close = Rc::new(handler);
510        self
511    }
512}
513
514impl Styled for Dialog {
515    fn style(&mut self) -> &mut StyleRefinement {
516        &mut self.style
517    }
518}
519impl ParentElement for Dialog {
520    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
521        self.children.extend(elements);
522    }
523}
524
525impl RenderOnce for Dialog {
526    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
527        let open = self
528            .handle
529            .as_ref()
530            .map_or(self.open, DialogHandle::is_open);
531        if !open {
532            return div().into_any_element();
533        }
534        let request_close = self.request_close;
535        let cancel = self.on_cancel.clone();
536        let confirm = self.on_ok.clone();
537        let closed = self.on_close.clone();
538        let overlay_closable = self.overlay_closable && self.topmost;
539        let dismiss_below_y = self.dismiss_below_y;
540        let escape_handle = self.handle.clone();
541        let confirm_handle = self.handle.clone();
542        let backdrop_handle = self.handle.clone();
543        let escape_change = self.on_open_change.clone();
544        let confirm_change = self.on_open_change.clone();
545        let backdrop_change = self.on_open_change.clone();
546        let viewport = window.viewport_size();
547
548        deferred(
549            anchored().position(point(px(0.), px(0.))).child(
550                div()
551                    .id(("dialog-host", self.layer))
552                    .test_support()
553                    .absolute()
554                    .top_0()
555                    .left_0()
556                    .w(viewport.width)
557                    .h(viewport.height)
558                    // A popup in normal flow lands centered; `refine_style`
559                    // below lets the caller lay the host out differently.
560                    .flex()
561                    .items_center()
562                    .justify_center()
563                    .role(self.role)
564                    .track_focus(&self.focus)
565                    .focus_trap(format!("dialog-{}", self.layer), &self.focus)
566                    .when(self.keyboard, |this| this.key_context(CONTEXT))
567                    .map(|this| {
568                        let request_cancel = request_close.clone();
569                        let request_confirm = request_close.clone();
570                        let closed_cancel = closed.clone();
571                        this.on_action(move |_: &Cancel, window, cx| {
572                            let event = ClickEvent::default();
573                            if cancel(&event, window, cx) {
574                                request_open_change(
575                                    &escape_handle,
576                                    &escape_change,
577                                    false,
578                                    DialogChangeReason::Cancel,
579                                    window,
580                                    cx,
581                                );
582                                request_cancel(false, window, cx);
583                                closed_cancel(&event, window, cx);
584                            }
585                        })
586                        .on_action(move |_: &Confirm, window, cx| {
587                            let event = ClickEvent::default();
588                            if confirm(&event, window, cx) {
589                                request_open_change(
590                                    &confirm_handle,
591                                    &confirm_change,
592                                    false,
593                                    DialogChangeReason::Confirm,
594                                    window,
595                                    cx,
596                                );
597                                request_confirm(true, window, cx);
598                                closed(&event, window, cx);
599                            }
600                        })
601                    })
602                    .when_some(self.backdrop, |this, backdrop| {
603                        let cancel = self.on_cancel.clone();
604                        let closed = self.on_close.clone();
605                        let request_close = request_close.clone();
606                        this.child(
607                            div()
608                                // The backdrop covers the host, so a caller's
609                                // `absolute()` surface has a box to fill.
610                                .absolute()
611                                .inset_0()
612                                .on_any_mouse_down(move |event, window, cx| {
613                                    if event.position.y < dismiss_below_y {
614                                        return;
615                                    }
616                                    let button = event.button;
617                                    cx.stop_propagation();
618                                    let event = ClickEvent::default();
619                                    if button == MouseButton::Left
620                                        && overlay_closable
621                                        && cancel(&event, window, cx)
622                                    {
623                                        request_open_change(
624                                            &backdrop_handle,
625                                            &backdrop_change,
626                                            false,
627                                            DialogChangeReason::BackdropPress,
628                                            window,
629                                            cx,
630                                        );
631                                        request_close(false, window, cx);
632                                        closed(&event, window, cx);
633                                    }
634                                })
635                                .child(backdrop),
636                        )
637                    })
638                    .children(self.popup)
639                    .children(self.children)
640                    .refine_style(&self.style),
641            ),
642        )
643        .with_priority(10 + self.layer)
644        .into_any_element()
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use gpui::{Context, Render, point};
652    use std::{cell::RefCell, rc::Rc};
653
654    #[gpui::test]
655    fn close_trigger_supplies_accessible_button(cx: &mut gpui::TestAppContext) {
656        use gpui::{Element as _, accesskit, canvas};
657        use std::sync::{Arc, Mutex};
658
659        struct Probe(Arc<Mutex<Option<accesskit::Node>>>);
660        impl Render for Probe {
661            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
662                let captured = self.0.clone();
663                canvas(
664                    move |_, window, cx| {
665                        DialogClose::new().trigger(|button| {
666                            let element = button.render(window, cx).into_element();
667                            let mut node = accesskit::Node::new(element.a11y_role().unwrap());
668                            element.write_a11y_info(&mut node);
669                            *captured.lock().unwrap() = Some(node);
670                            element
671                        });
672                    },
673                    |_, _, _, _| {},
674                )
675            }
676        }
677
678        let captured = Arc::new(Mutex::new(None));
679        let result = captured.clone();
680        let (_, cx) = cx.add_window_view(move |_, _| Probe(captured));
681        cx.update(|window, cx| window.draw(cx).clear(cx));
682        let node = result.lock().unwrap().take().unwrap();
683        assert_eq!(node.role(), Role::Button);
684        assert_eq!(node.label(), Some("Close"));
685        assert!(node.supports_action(accesskit::Action::Click));
686    }
687
688    #[gpui::test]
689    fn close_trigger_activates_once_and_respects_cancel_veto(cx: &mut gpui::TestAppContext) {
690        use gpui::{KeyDownEvent, KeyUpEvent, Keystroke};
691
692        struct Harness {
693            focus: FocusHandle,
694            button_focus: FocusHandle,
695            handle: DialogHandle,
696            attempts: Rc<Cell<usize>>,
697        }
698        impl Render for Harness {
699            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
700                let attempts = self.attempts.clone();
701                let button_focus = self.button_focus.clone();
702                Dialog::new(cx)
703                    .handle(self.handle.clone())
704                    .focus_handle(self.focus.clone())
705                    .on_cancel(move |_, _, _| {
706                        attempts.set(attempts.get() + 1);
707                        attempts.get() > 1
708                    })
709                    .popup(
710                        DialogClose::new().trigger(move |button| {
711                            button.size(px(100.)).track_focus(&button_focus)
712                        }),
713                    )
714            }
715        }
716
717        cx.update(crate::init);
718        let handle = DialogHandle::new(true);
719        let attempts = Rc::new(Cell::new(0));
720        let (view, cx) = cx.add_window_view({
721            let handle = handle.clone();
722            let attempts = attempts.clone();
723            move |_, cx| Harness {
724                focus: cx.focus_handle(),
725                button_focus: cx.focus_handle(),
726                handle,
727                attempts,
728            }
729        });
730        let viewport = cx.update(|window, cx| {
731            view.read(cx).focus.clone().focus(window, cx);
732            window.draw(cx).clear(cx);
733            window.viewport_size()
734        });
735        let popup_center = point(viewport.width / 2., viewport.height / 2.);
736        cx.simulate_click(popup_center, Default::default());
737        cx.run_until_parked();
738        assert_eq!(attempts.get(), 1);
739        assert!(handle.is_open(), "on_cancel can veto pointer dismissal");
740
741        cx.update(|window, cx| {
742            view.read(cx).button_focus.clone().focus(window, cx);
743            window.draw(cx).clear(cx);
744        });
745        let keystroke = Keystroke::parse("space").unwrap();
746        cx.simulate_event(KeyDownEvent {
747            keystroke: keystroke.clone(),
748            is_held: false,
749            prefer_character_input: false,
750        });
751        cx.simulate_event(KeyUpEvent { keystroke });
752        cx.run_until_parked();
753        assert_eq!(attempts.get(), 2);
754        assert!(!handle.is_open(), "Space uses the same cancel decision");
755    }
756
757    struct TriggerHarness {
758        handle: DialogHandle,
759    }
760    impl Render for TriggerHarness {
761        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
762            DialogTrigger::new(div().size(px(100.))).handle(self.handle.clone())
763        }
764    }
765
766    #[gpui::test]
767    fn trigger_opens_shared_handle_and_reports_reason(cx: &mut gpui::TestAppContext) {
768        let handle = DialogHandle::new(false);
769        let changes = Rc::new(RefCell::new(Vec::new()));
770        *handle.on_open_change.borrow_mut() = Some({
771            let changes = changes.clone();
772            Rc::new(move |open, reason, _, _| changes.borrow_mut().push((open, reason)))
773        });
774        let (_, cx) = cx.add_window_view({
775            let handle = handle.clone();
776            move |_, _| TriggerHarness { handle }
777        });
778        cx.update(|window, cx| window.draw(cx).clear(cx));
779        cx.simulate_click(point(px(20.), px(20.)), Default::default());
780
781        assert!(handle.is_open());
782        assert_eq!(
783            &*changes.borrow(),
784            &[(true, DialogChangeReason::TriggerPress)]
785        );
786    }
787
788    /// The backdrop is the dimming surface every caller hands over as an
789    /// `absolute()` element, so it has to have the host's box to resolve
790    /// against — a collapsed wrapper leaves it zero-sized and invisible.
791    #[gpui::test]
792    fn the_backdrop_fills_the_host(cx: &mut gpui::TestAppContext) {
793        use gpui::{Bounds, canvas};
794        use std::cell::Cell;
795
796        struct Harness {
797            focus: FocusHandle,
798            bounds: Rc<Cell<Bounds<Pixels>>>,
799        }
800        impl Render for Harness {
801            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
802                let bounds = self.bounds.clone();
803                Dialog::new(cx)
804                    .open(true)
805                    .focus_handle(self.focus.clone())
806                    .backdrop(
807                        canvas(
808                            move |bounds_of_backdrop, _, _| bounds.set(bounds_of_backdrop),
809                            |_, _, _, _| {},
810                        )
811                        .absolute()
812                        .size_full(),
813                    )
814                    .popup(div().size(px(100.)))
815            }
816        }
817
818        cx.update(crate::init);
819        let bounds = Rc::new(Cell::new(Bounds::default()));
820        let (_, cx) = cx.add_window_view({
821            let bounds = bounds.clone();
822            move |_, cx| Harness {
823                focus: cx.focus_handle(),
824                bounds,
825            }
826        });
827        let viewport = cx.update(|window, cx| {
828            let viewport = window.viewport_size();
829            window.draw(cx).clear(cx);
830            viewport
831        });
832
833        assert_eq!(
834            bounds.get().size,
835            viewport,
836            "a zero-sized backdrop paints no overlay behind the dialog"
837        );
838    }
839
840    /// The backdrop sits under the popup, so a press on the popup must not
841    /// reach it and dismiss the dialog.
842    #[gpui::test]
843    fn pressing_the_popup_does_not_dismiss(cx: &mut gpui::TestAppContext) {
844        struct Harness(DialogHandle);
845        impl Render for Harness {
846            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
847                Dialog::new(cx)
848                    .handle(self.0.clone())
849                    .backdrop(div().absolute().size_full())
850                    .popup(DialogPopup::new().size(px(100.)))
851            }
852        }
853
854        cx.update(crate::init);
855        let handle = DialogHandle::new(true);
856        let (_, cx) = cx.add_window_view({
857            let handle = handle.clone();
858            move |_, _| Harness(handle)
859        });
860        let viewport = cx.update(|window, cx| {
861            window.draw(cx).clear(cx);
862            window.viewport_size()
863        });
864
865        cx.simulate_click(
866            point(viewport.width / 2., viewport.height / 2.),
867            Default::default(),
868        );
869        assert!(
870            handle.is_open(),
871            "a press on the popup keeps the dialog open"
872        );
873
874        cx.simulate_click(point(px(5.), px(5.)), Default::default());
875        assert!(!handle.is_open(), "a press on the backdrop still dismisses");
876    }
877
878    #[gpui::test]
879    fn the_popup_is_centered_by_default(cx: &mut gpui::TestAppContext) {
880        use gpui::{Bounds, canvas};
881        use std::cell::Cell;
882
883        struct Harness(Rc<Cell<Bounds<Pixels>>>);
884        impl Render for Harness {
885            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
886                let bounds = self.0.clone();
887                Dialog::new(cx).popup(
888                    canvas(move |popup, _, _| bounds.set(popup), |_, _, _, _| {}).size(px(100.)),
889                )
890            }
891        }
892
893        cx.update(crate::init);
894        let bounds = Rc::new(Cell::new(Bounds::default()));
895        let (_, cx) = cx.add_window_view({
896            let bounds = bounds.clone();
897            move |_, _| Harness(bounds)
898        });
899        let viewport = cx.update(|window, cx| {
900            let viewport = window.viewport_size();
901            window.draw(cx).clear(cx);
902            viewport
903        });
904
905        assert_eq!(
906            bounds.get().center(),
907            point(viewport.width / 2., viewport.height / 2.)
908        );
909    }
910}