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#[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#[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 .when($occlude, |this| this.occlude())
232 .children(self.children)
233 .refine_style(&self.style)
234 }
235 }
236 };
237}
238
239dialog_part!(
240 DialogBackdrop,
242 "dialog-backdrop"
243);
244
245dialog_part!(
246 DialogPopup,
248 "dialog-popup",
249 true
250);
251
252#[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#[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#[derive(IntoElement)]
338pub struct DialogClose {
339 style: StyleRefinement,
340 children: SmallVec<[AnyElement; 1]>,
341 trigger: Option<AnyElement>,
342 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 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 fn activate(anchor: &RefCell<Option<FocusHandle>>, window: &mut Window, cx: &mut App) {
376 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 .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 .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 .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 #[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 #[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}