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, SharedString,
10 StatefulInteractiveElement as _, StyleRefinement, Styled, Window, anchored, deferred, div,
11 point, prelude::FluentBuilder as _, 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 accessibility_label: Option<SharedString>,
129 layer: usize,
130 keyboard: bool,
131 overlay_closable: bool,
132 topmost: bool,
133 dismiss_below_y: Pixels,
134 backdrop: Option<AnyElement>,
135 popup: Option<AnyElement>,
136 children: SmallVec<[AnyElement; 2]>,
137 on_ok: Decision,
138 on_cancel: Decision,
139 on_close: Closed,
140 request_close: CloseRequest,
141 handle: Option<DialogHandle>,
142 open: bool,
143 on_open_change: Option<OpenChange>,
144}
145
146#[derive(IntoElement)]
148pub struct DialogTrigger {
149 trigger: AnyElement,
150 open: OpenRequest,
151 handle: Option<DialogHandle>,
152}
153
154impl DialogTrigger {
155 pub fn new(trigger: impl IntoElement) -> Self {
156 Self {
157 trigger: trigger.into_any_element(),
158 open: Rc::new(|_, _| {}),
159 handle: None,
160 }
161 }
162 pub fn handle(mut self, handle: DialogHandle) -> Self {
163 self.handle = Some(handle);
164 self
165 }
166
167 pub fn on_open(mut self, open: impl Fn(&mut Window, &mut App) + 'static) -> Self {
168 self.open = Rc::new(open);
169 self
170 }
171}
172
173impl RenderOnce for DialogTrigger {
174 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
175 div()
176 .on_mouse_down(MouseButton::Left, move |_, window, cx| {
177 if let Some(handle) = self.handle.as_ref() {
178 handle.set_open(true, DialogChangeReason::TriggerPress, window, cx);
179 }
180 (self.open)(window, cx);
181 cx.stop_propagation();
182 })
183 .child(self.trigger)
184 }
185}
186
187macro_rules! dialog_part {
188 ($(#[$meta:meta])* $name:ident, $id:literal) => {
189 dialog_part!($(#[$meta])* $name, $id, false);
190 };
191 ($(#[$meta:meta])* $name:ident, $id:literal, $occlude:literal) => {
192 $(#[$meta])*
193 #[derive(IntoElement)]
194 pub struct $name {
195 style: StyleRefinement,
196 children: SmallVec<[AnyElement; 2]>,
197 }
198
199 impl $name {
200 pub fn new() -> Self {
201 Self {
202 style: StyleRefinement::default(),
203 children: SmallVec::new(),
204 }
205 }
206 }
207
208 impl Default for $name {
209 fn default() -> Self {
210 Self::new()
211 }
212 }
213
214 impl Styled for $name {
215 fn style(&mut self) -> &mut StyleRefinement {
216 &mut self.style
217 }
218 }
219
220 impl ParentElement for $name {
221 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
222 self.children.extend(elements);
223 }
224 }
225
226 impl RenderOnce for $name {
227 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
228 div()
229 .id($id)
230 .when($occlude, |this| this.occlude())
233 .children(self.children)
234 .refine_style(&self.style)
235 }
236 }
237 };
238}
239
240dialog_part!(
241 DialogBackdrop,
243 "dialog-backdrop"
244);
245
246dialog_part!(
247 DialogPopup,
249 "dialog-popup",
250 true
251);
252
253#[derive(IntoElement)]
255pub struct DialogTitle {
256 base: gpui::Div,
257 style: StyleRefinement,
258 children: SmallVec<[AnyElement; 2]>,
259}
260
261impl DialogTitle {
262 pub fn new() -> Self {
263 Self {
264 base: div(),
265 style: StyleRefinement::default(),
266 children: SmallVec::new(),
267 }
268 }
269}
270
271impl Default for DialogTitle {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276impl Styled for DialogTitle {
277 fn style(&mut self) -> &mut StyleRefinement {
278 &mut self.style
279 }
280}
281impl ParentElement for DialogTitle {
282 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
283 self.children.extend(elements);
284 }
285}
286impl RenderOnce for DialogTitle {
287 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
288 self.base
289 .id("dialog-title")
290 .children(self.children)
291 .refine_style(&self.style)
292 }
293}
294
295#[derive(IntoElement)]
297pub struct DialogDescription {
298 base: gpui::Div,
299 style: StyleRefinement,
300 children: SmallVec<[AnyElement; 2]>,
301}
302
303impl DialogDescription {
304 pub fn new() -> Self {
305 Self {
306 base: div(),
307 style: StyleRefinement::default(),
308 children: SmallVec::new(),
309 }
310 }
311}
312
313impl Default for DialogDescription {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318impl Styled for DialogDescription {
319 fn style(&mut self) -> &mut StyleRefinement {
320 &mut self.style
321 }
322}
323impl ParentElement for DialogDescription {
324 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
325 self.children.extend(elements);
326 }
327}
328impl RenderOnce for DialogDescription {
329 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
330 self.base
331 .id("dialog-description")
332 .children(self.children)
333 .refine_style(&self.style)
334 }
335}
336
337#[derive(IntoElement)]
339pub struct DialogClose {
340 style: StyleRefinement,
341 children: SmallVec<[AnyElement; 1]>,
342 trigger: Option<AnyElement>,
343 anchor: Rc<RefCell<Option<FocusHandle>>>,
346}
347
348impl DialogClose {
349 pub fn new() -> Self {
350 Self {
351 style: StyleRefinement::default(),
352 children: SmallVec::new(),
353 trigger: None,
354 anchor: Rc::default(),
355 }
356 }
357
358 pub fn trigger<E: IntoElement>(mut self, build: impl FnOnce(crate::Button) -> E) -> Self {
364 let anchor = self.anchor.clone();
365 let button = crate::Button::new("close")
366 .accessibility_label("Close")
367 .on_click(move |_, window, cx| Self::activate(&anchor, window, cx));
368 self.trigger = Some(build(button).into_any_element());
369 self
370 }
371
372 fn activate(anchor: &RefCell<Option<FocusHandle>>, window: &mut Window, cx: &mut App) {
377 let anchor = anchor.borrow().clone();
380 match anchor {
381 Some(anchor) => anchor.dispatch_action(&Cancel, window, cx),
382 None => window.dispatch_action(Box::new(Cancel), cx),
383 }
384 }
385}
386impl Default for DialogClose {
387 fn default() -> Self {
388 Self::new()
389 }
390}
391impl ParentElement for DialogClose {
392 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
393 self.children.extend(elements);
394 }
395}
396impl Styled for DialogClose {
397 fn style(&mut self) -> &mut StyleRefinement {
398 &mut self.style
399 }
400}
401impl RenderOnce for DialogClose {
402 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
403 let anchor = window
404 .use_keyed_state("dialog-close-anchor", cx, |_, cx| cx.focus_handle())
405 .read(cx)
406 .clone();
407 *self.anchor.borrow_mut() = Some(anchor.clone());
408 let cell = self.anchor;
409 div()
410 .id("dialog-close")
411 .child(div().absolute().size_0().track_focus(&anchor))
415 .when(self.trigger.is_none(), |this| {
416 this.on_click(move |_, window, cx| Self::activate(&cell, window, cx))
417 })
418 .children(self.trigger)
419 .children(self.children)
420 .refine_style(&self.style)
421 }
422}
423
424impl Dialog {
425 pub fn new(cx: &mut App) -> Self {
426 Self {
427 style: StyleRefinement::default(),
428 focus: cx.focus_handle(),
429 role: Role::Dialog,
430 accessibility_label: None,
431 layer: 0,
432 keyboard: true,
433 overlay_closable: true,
434 topmost: true,
435 dismiss_below_y: px(0.),
436 backdrop: None,
437 popup: None,
438 children: SmallVec::new(),
439 on_ok: Rc::new(|_, _, _| true),
440 on_cancel: Rc::new(|_, _, _| true),
441 on_close: Rc::new(|_, _, _| {}),
442 request_close: Rc::new(|_, _, _| {}),
443 handle: None,
444 open: true,
445 on_open_change: None,
446 }
447 }
448 pub fn open(mut self, open: bool) -> Self {
449 self.open = open;
450 self
451 }
452 pub fn handle(mut self, handle: DialogHandle) -> Self {
453 if let Some(callback) = self.on_open_change.as_ref() {
454 *handle.on_open_change.borrow_mut() = Some(callback.clone());
455 }
456 self.handle = Some(handle);
457 self
458 }
459 pub fn on_open_change(
460 mut self,
461 handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
462 ) -> Self {
463 let handler: OpenChange = Rc::new(handler);
464 if let Some(handle) = self.handle.as_ref() {
465 *handle.on_open_change.borrow_mut() = Some(handler.clone());
466 }
467 self.on_open_change = Some(handler);
468 self
469 }
470
471 pub fn backdrop(mut self, element: impl IntoElement) -> Self {
472 self.backdrop = Some(element.into_any_element());
473 self
474 }
475 pub fn popup(mut self, element: impl IntoElement) -> Self {
476 self.popup = Some(element.into_any_element());
477 self
478 }
479 pub fn close_on_escape(mut self, value: bool) -> Self {
480 self.keyboard = value;
481 self
482 }
483 pub fn close_on_backdrop_press(mut self, value: bool) -> Self {
484 self.overlay_closable = value;
485 self
486 }
487 pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
488 self.dismiss_below_y = value;
489 self
490 }
491 pub(crate) fn role(mut self, role: Role) -> Self {
492 self.role = role;
493 self
494 }
495 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
497 self.accessibility_label = Some(label.into());
498 self
499 }
500 #[doc(hidden)]
501 pub fn layer(mut self, index: usize, topmost: bool) -> Self {
502 self.layer = index;
503 self.topmost = topmost;
504 self
505 }
506 #[doc(hidden)]
507 pub fn focus_handle(mut self, value: FocusHandle) -> Self {
508 self.focus = value;
509 self
510 }
511 #[doc(hidden)]
512 pub fn request_close(
513 mut self,
514 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
515 ) -> Self {
516 self.request_close = Rc::new(handler);
517 self
518 }
519}
520
521impl Styled for Dialog {
522 fn style(&mut self) -> &mut StyleRefinement {
523 &mut self.style
524 }
525}
526impl ParentElement for Dialog {
527 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
528 self.children.extend(elements);
529 }
530}
531
532impl RenderOnce for Dialog {
533 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
534 let open = self
535 .handle
536 .as_ref()
537 .map_or(self.open, DialogHandle::is_open);
538 if !open {
539 return div().into_any_element();
540 }
541 let request_close = self.request_close;
542 let cancel = self.on_cancel.clone();
543 let confirm = self.on_ok.clone();
544 let closed = self.on_close.clone();
545 let overlay_closable = self.overlay_closable && self.topmost;
546 let dismiss_below_y = self.dismiss_below_y;
547 let escape_handle = self.handle.clone();
548 let confirm_handle = self.handle.clone();
549 let backdrop_handle = self.handle.clone();
550 let escape_change = self.on_open_change.clone();
551 let confirm_change = self.on_open_change.clone();
552 let backdrop_change = self.on_open_change.clone();
553 let viewport = window.viewport_size();
554
555 deferred(
556 anchored().position(point(px(0.), px(0.))).child(
557 div()
558 .id(("dialog-host", self.layer))
559 .test_support()
560 .absolute()
561 .top_0()
562 .left_0()
563 .w(viewport.width)
564 .h(viewport.height)
565 .flex()
568 .items_center()
569 .justify_center()
570 .role(self.role)
571 .when_some(self.accessibility_label.clone(), |this, label| {
572 this.aria_label(label)
573 })
574 .track_focus(&self.focus)
575 .focus_trap(format!("dialog-{}", self.layer), &self.focus)
576 .when(self.keyboard, |this| this.key_context(CONTEXT))
577 .map(|this| {
578 let request_cancel = request_close.clone();
579 let request_confirm = request_close.clone();
580 let closed_cancel = closed.clone();
581 this.on_action(move |_: &Cancel, window, cx| {
582 let event = ClickEvent::default();
583 if cancel(&event, window, cx) {
584 request_open_change(
585 &escape_handle,
586 &escape_change,
587 false,
588 DialogChangeReason::Cancel,
589 window,
590 cx,
591 );
592 request_cancel(false, window, cx);
593 closed_cancel(&event, window, cx);
594 }
595 })
596 .on_action(move |_: &Confirm, window, cx| {
597 let event = ClickEvent::default();
598 if confirm(&event, window, cx) {
599 request_open_change(
600 &confirm_handle,
601 &confirm_change,
602 false,
603 DialogChangeReason::Confirm,
604 window,
605 cx,
606 );
607 request_confirm(true, window, cx);
608 closed(&event, window, cx);
609 }
610 })
611 })
612 .when_some(self.backdrop, |this, backdrop| {
613 let cancel = self.on_cancel.clone();
614 let closed = self.on_close.clone();
615 let request_close = request_close.clone();
616 this.child(
617 div()
618 .absolute()
621 .inset_0()
622 .on_any_mouse_down(move |event, window, cx| {
623 if event.position.y < dismiss_below_y {
624 return;
625 }
626 let button = event.button;
627 cx.stop_propagation();
628 let event = ClickEvent::default();
629 if button == MouseButton::Left
630 && overlay_closable
631 && cancel(&event, window, cx)
632 {
633 request_open_change(
634 &backdrop_handle,
635 &backdrop_change,
636 false,
637 DialogChangeReason::BackdropPress,
638 window,
639 cx,
640 );
641 request_close(false, window, cx);
642 closed(&event, window, cx);
643 }
644 })
645 .child(backdrop),
646 )
647 })
648 .children(self.popup)
649 .children(self.children)
650 .refine_style(&self.style),
651 ),
652 )
653 .with_priority(10 + self.layer)
654 .into_any_element()
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use gpui::{Context, Render, point};
662 use std::{cell::RefCell, rc::Rc};
663
664 #[gpui::test]
665 fn a_dialog_takes_an_accessible_name(cx: &mut gpui::TestAppContext) {
666 struct Named;
667 impl Render for Named {
668 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
669 div().child(
670 Dialog::new(cx)
671 .accessibility_label("Discard draft?")
672 .popup(div().size(px(10.))),
673 )
674 }
675 }
676 cx.update(crate::init);
677 let (_, cx) = cx.add_window_view(|_, _| Named);
678 cx.update(|window, cx| window.draw(cx).clear(cx));
679 let dialog = cx.update(|window, _| {
680 crate::test_support::snapshots(window)
681 .into_iter()
682 .find(|node| node.role() == Some(Role::Dialog))
683 .expect("a dialog node")
684 });
685 assert_eq!(dialog.label(), Some("Discard draft?"));
686 }
687
688 #[gpui::test]
689 fn close_trigger_supplies_accessible_button(cx: &mut gpui::TestAppContext) {
690 use gpui::{Element as _, accesskit, canvas};
691 use std::sync::{Arc, Mutex};
692
693 struct Probe(Arc<Mutex<Option<accesskit::Node>>>);
694 impl Render for Probe {
695 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
696 let captured = self.0.clone();
697 canvas(
698 move |_, window, cx| {
699 DialogClose::new().trigger(|button| {
700 let element = button.render(window, cx).into_element();
701 let mut node = accesskit::Node::new(element.a11y_role().unwrap());
702 element.write_a11y_info(&mut node);
703 *captured.lock().unwrap() = Some(node);
704 element
705 });
706 },
707 |_, _, _, _| {},
708 )
709 }
710 }
711
712 let captured = Arc::new(Mutex::new(None));
713 let result = captured.clone();
714 let (_, cx) = cx.add_window_view(move |_, _| Probe(captured));
715 cx.update(|window, cx| window.draw(cx).clear(cx));
716 let node = result.lock().unwrap().take().unwrap();
717 assert_eq!(node.role(), Role::Button);
718 assert_eq!(node.label(), Some("Close"));
719 assert!(node.supports_action(accesskit::Action::Click));
720 }
721
722 #[gpui::test]
723 fn close_trigger_activates_once_and_respects_cancel_veto(cx: &mut gpui::TestAppContext) {
724 use gpui::{KeyDownEvent, KeyUpEvent, Keystroke};
725
726 struct Harness {
727 focus: FocusHandle,
728 button_focus: FocusHandle,
729 handle: DialogHandle,
730 attempts: Rc<Cell<usize>>,
731 }
732 impl Render for Harness {
733 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
734 let attempts = self.attempts.clone();
735 let button_focus = self.button_focus.clone();
736 Dialog::new(cx)
737 .handle(self.handle.clone())
738 .focus_handle(self.focus.clone())
739 .on_cancel(move |_, _, _| {
740 attempts.set(attempts.get() + 1);
741 attempts.get() > 1
742 })
743 .popup(
744 DialogClose::new().trigger(move |button| {
745 button.size(px(100.)).track_focus(&button_focus)
746 }),
747 )
748 }
749 }
750
751 cx.update(crate::init);
752 let handle = DialogHandle::new(true);
753 let attempts = Rc::new(Cell::new(0));
754 let (view, cx) = cx.add_window_view({
755 let handle = handle.clone();
756 let attempts = attempts.clone();
757 move |_, cx| Harness {
758 focus: cx.focus_handle(),
759 button_focus: cx.focus_handle(),
760 handle,
761 attempts,
762 }
763 });
764 let viewport = cx.update(|window, cx| {
765 view.read(cx).focus.clone().focus(window, cx);
766 window.draw(cx).clear(cx);
767 window.viewport_size()
768 });
769 let popup_center = point(viewport.width / 2., viewport.height / 2.);
770 cx.simulate_click(popup_center, Default::default());
771 cx.run_until_parked();
772 assert_eq!(attempts.get(), 1);
773 assert!(handle.is_open(), "on_cancel can veto pointer dismissal");
774
775 cx.update(|window, cx| {
776 view.read(cx).button_focus.clone().focus(window, cx);
777 window.draw(cx).clear(cx);
778 });
779 let keystroke = Keystroke::parse("space").unwrap();
780 cx.simulate_event(KeyDownEvent {
781 keystroke: keystroke.clone(),
782 is_held: false,
783 prefer_character_input: false,
784 });
785 cx.simulate_event(KeyUpEvent { keystroke });
786 cx.run_until_parked();
787 assert_eq!(attempts.get(), 2);
788 assert!(!handle.is_open(), "Space uses the same cancel decision");
789 }
790
791 struct TriggerHarness {
792 handle: DialogHandle,
793 }
794 impl Render for TriggerHarness {
795 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
796 DialogTrigger::new(div().size(px(100.))).handle(self.handle.clone())
797 }
798 }
799
800 #[gpui::test]
801 fn trigger_opens_shared_handle_and_reports_reason(cx: &mut gpui::TestAppContext) {
802 let handle = DialogHandle::new(false);
803 let changes = Rc::new(RefCell::new(Vec::new()));
804 *handle.on_open_change.borrow_mut() = Some({
805 let changes = changes.clone();
806 Rc::new(move |open, reason, _, _| changes.borrow_mut().push((open, reason)))
807 });
808 let (_, cx) = cx.add_window_view({
809 let handle = handle.clone();
810 move |_, _| TriggerHarness { handle }
811 });
812 cx.update(|window, cx| window.draw(cx).clear(cx));
813 cx.simulate_click(point(px(20.), px(20.)), Default::default());
814
815 assert!(handle.is_open());
816 assert_eq!(
817 &*changes.borrow(),
818 &[(true, DialogChangeReason::TriggerPress)]
819 );
820 }
821
822 #[gpui::test]
826 fn the_backdrop_fills_the_host(cx: &mut gpui::TestAppContext) {
827 use gpui::{Bounds, canvas};
828 use std::cell::Cell;
829
830 struct Harness {
831 focus: FocusHandle,
832 bounds: Rc<Cell<Bounds<Pixels>>>,
833 }
834 impl Render for Harness {
835 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
836 let bounds = self.bounds.clone();
837 Dialog::new(cx)
838 .open(true)
839 .focus_handle(self.focus.clone())
840 .backdrop(
841 canvas(
842 move |bounds_of_backdrop, _, _| bounds.set(bounds_of_backdrop),
843 |_, _, _, _| {},
844 )
845 .absolute()
846 .size_full(),
847 )
848 .popup(div().size(px(100.)))
849 }
850 }
851
852 cx.update(crate::init);
853 let bounds = Rc::new(Cell::new(Bounds::default()));
854 let (_, cx) = cx.add_window_view({
855 let bounds = bounds.clone();
856 move |_, cx| Harness {
857 focus: cx.focus_handle(),
858 bounds,
859 }
860 });
861 let viewport = cx.update(|window, cx| {
862 let viewport = window.viewport_size();
863 window.draw(cx).clear(cx);
864 viewport
865 });
866
867 assert_eq!(
868 bounds.get().size,
869 viewport,
870 "a zero-sized backdrop paints no overlay behind the dialog"
871 );
872 }
873
874 #[gpui::test]
877 fn pressing_the_popup_does_not_dismiss(cx: &mut gpui::TestAppContext) {
878 struct Harness(DialogHandle);
879 impl Render for Harness {
880 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
881 Dialog::new(cx)
882 .handle(self.0.clone())
883 .backdrop(div().absolute().size_full())
884 .popup(DialogPopup::new().size(px(100.)))
885 }
886 }
887
888 cx.update(crate::init);
889 let handle = DialogHandle::new(true);
890 let (_, cx) = cx.add_window_view({
891 let handle = handle.clone();
892 move |_, _| Harness(handle)
893 });
894 let viewport = cx.update(|window, cx| {
895 window.draw(cx).clear(cx);
896 window.viewport_size()
897 });
898
899 cx.simulate_click(
900 point(viewport.width / 2., viewport.height / 2.),
901 Default::default(),
902 );
903 assert!(
904 handle.is_open(),
905 "a press on the popup keeps the dialog open"
906 );
907
908 cx.simulate_click(point(px(5.), px(5.)), Default::default());
909 assert!(!handle.is_open(), "a press on the backdrop still dismisses");
910 }
911
912 #[gpui::test]
913 fn the_popup_is_centered_by_default(cx: &mut gpui::TestAppContext) {
914 use gpui::{Bounds, canvas};
915 use std::cell::Cell;
916
917 struct Harness(Rc<Cell<Bounds<Pixels>>>);
918 impl Render for Harness {
919 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
920 let bounds = self.0.clone();
921 Dialog::new(cx).popup(
922 canvas(move |popup, _, _| bounds.set(popup), |_, _, _, _| {}).size(px(100.)),
923 )
924 }
925 }
926
927 cx.update(crate::init);
928 let bounds = Rc::new(Cell::new(Bounds::default()));
929 let (_, cx) = cx.add_window_view({
930 let bounds = bounds.clone();
931 move |_, _| Harness(bounds)
932 });
933 let viewport = cx.update(|window, cx| {
934 let viewport = window.viewport_size();
935 window.draw(cx).clear(cx);
936 viewport
937 });
938
939 assert_eq!(
940 bounds.get().center(),
941 point(viewport.width / 2., viewport.height / 2.)
942 );
943 }
944}