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 $(#[$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 DialogBackdrop,
236 "dialog-backdrop"
237);
238
239dialog_part!(
240 DialogPopup,
242 "dialog-popup"
243);
244
245#[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#[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#[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 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 .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 #[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}