1use std::{
2 cell::{Cell, RefCell},
3 rc::Rc,
4};
5
6use gpui::{
7 AnyElement, App, ClickEvent, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding,
8 MouseButton, ParentElement, Pixels, RenderOnce, Role, StatefulInteractiveElement as _,
9 StyleRefinement, Styled, Window, anchored, deferred, div, point, prelude::FluentBuilder as _,
10 px,
11};
12use smallvec::SmallVec;
13
14use crate::actions::{Cancel, Confirm};
15use crate::{FocusTrapElement as _, StyledExt as _};
16
17const CONTEXT: &str = "Dialog";
18type Decision = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
19type Closed = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
20type CloseRequest = Rc<dyn Fn(bool, &mut Window, &mut App)>;
21type OpenRequest = Rc<dyn Fn(&mut Window, &mut App)>;
22type OpenChange = Rc<dyn Fn(bool, DialogChangeReason, &mut Window, &mut App)>;
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum DialogChangeReason {
26 TriggerPress,
27 BackdropPress,
28 Cancel,
29 Confirm,
30 Imperative,
31}
32
33#[derive(Clone)]
34pub struct DialogHandle {
35 open: Rc<Cell<bool>>,
36 on_open_change: Rc<RefCell<Option<OpenChange>>>,
37}
38
39impl DialogHandle {
40 pub fn new(open: bool) -> Self {
41 Self {
42 open: Rc::new(Cell::new(open)),
43 on_open_change: Rc::new(RefCell::new(None)),
44 }
45 }
46 pub fn is_open(&self) -> bool {
47 self.open.get()
48 }
49 pub fn open(&self, window: &mut Window, cx: &mut App) {
50 self.set_open(true, DialogChangeReason::Imperative, window, cx);
51 }
52 pub fn close(&self, window: &mut Window, cx: &mut App) {
53 self.set_open(false, DialogChangeReason::Imperative, window, cx);
54 }
55 pub(crate) fn set_open(
56 &self,
57 open: bool,
58 reason: DialogChangeReason,
59 window: &mut Window,
60 cx: &mut App,
61 ) {
62 if self.open.replace(open) == open {
63 return;
64 }
65 let callback = self.on_open_change.borrow().clone();
66 if let Some(callback) = callback {
67 callback(open, reason, window, cx);
68 }
69 window.refresh();
70 }
71}
72
73fn request_open_change(
74 handle: &Option<DialogHandle>,
75 callback: &Option<OpenChange>,
76 open: bool,
77 reason: DialogChangeReason,
78 window: &mut Window,
79 cx: &mut App,
80) {
81 if let Some(handle) = handle {
82 handle.set_open(open, reason, window, cx);
83 } else if let Some(callback) = callback {
84 callback(open, reason, window, cx);
85 }
86}
87
88pub fn init(cx: &mut App) {
89 cx.bind_keys([
90 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
91 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
92 ]);
93}
94
95impl Dialog {
96 pub fn on_ok(
97 mut self,
98 handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
99 ) -> Self {
100 self.on_ok = Rc::new(handler);
101 self
102 }
103
104 pub fn on_cancel(
105 mut self,
106 handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
107 ) -> Self {
108 self.on_cancel = Rc::new(handler);
109 self
110 }
111
112 pub fn on_close(
113 mut self,
114 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
115 ) -> Self {
116 self.on_close = Rc::new(handler);
117 self
118 }
119}
120
121#[derive(IntoElement)]
123pub struct Dialog {
124 style: StyleRefinement,
125 focus: FocusHandle,
126 role: Role,
127 layer: usize,
128 keyboard: bool,
129 overlay_closable: bool,
130 topmost: bool,
131 dismiss_below_y: Pixels,
132 backdrop: Option<AnyElement>,
133 popup: Option<AnyElement>,
134 children: SmallVec<[AnyElement; 2]>,
135 on_ok: Decision,
136 on_cancel: Decision,
137 on_close: Closed,
138 request_close: CloseRequest,
139 handle: Option<DialogHandle>,
140 open: bool,
141 on_open_change: Option<OpenChange>,
142}
143
144#[derive(IntoElement)]
146pub struct DialogTrigger {
147 trigger: AnyElement,
148 open: OpenRequest,
149 handle: Option<DialogHandle>,
150}
151
152impl DialogTrigger {
153 pub fn new(trigger: impl IntoElement) -> Self {
154 Self {
155 trigger: trigger.into_any_element(),
156 open: Rc::new(|_, _| {}),
157 handle: None,
158 }
159 }
160 pub fn handle(mut self, handle: DialogHandle) -> Self {
161 self.handle = Some(handle);
162 self
163 }
164
165 pub fn on_open(mut self, open: impl Fn(&mut Window, &mut App) + 'static) -> Self {
166 self.open = Rc::new(open);
167 self
168 }
169}
170
171impl RenderOnce for DialogTrigger {
172 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
173 div()
174 .on_mouse_down(MouseButton::Left, move |_, window, cx| {
175 if let Some(handle) = self.handle.as_ref() {
176 handle.set_open(true, DialogChangeReason::TriggerPress, window, cx);
177 }
178 (self.open)(window, cx);
179 cx.stop_propagation();
180 })
181 .child(self.trigger)
182 }
183}
184
185macro_rules! dialog_part {
186 ($(#[$meta:meta])* $name:ident, $id:literal) => {
187 $(#[$meta])*
188 #[derive(IntoElement)]
189 pub struct $name {
190 style: StyleRefinement,
191 children: SmallVec<[AnyElement; 2]>,
192 }
193
194 impl $name {
195 pub fn new() -> Self {
196 Self {
197 style: StyleRefinement::default(),
198 children: SmallVec::new(),
199 }
200 }
201 }
202
203 impl Default for $name {
204 fn default() -> Self {
205 Self::new()
206 }
207 }
208
209 impl Styled for $name {
210 fn style(&mut self) -> &mut StyleRefinement {
211 &mut self.style
212 }
213 }
214
215 impl ParentElement for $name {
216 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
217 self.children.extend(elements);
218 }
219 }
220
221 impl RenderOnce for $name {
222 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
223 div()
224 .id($id)
225 .children(self.children)
226 .refine_style(&self.style)
227 }
228 }
229 };
230}
231
232dialog_part!(
233 DialogBackdrop,
235 "dialog-backdrop"
236);
237
238dialog_part!(
239 DialogPopup,
241 "dialog-popup"
242);
243
244#[derive(IntoElement)]
246pub struct DialogTitle {
247 base: gpui::Div,
248 style: StyleRefinement,
249 children: SmallVec<[AnyElement; 2]>,
250}
251
252impl DialogTitle {
253 pub fn new() -> Self {
254 Self {
255 base: div(),
256 style: StyleRefinement::default(),
257 children: SmallVec::new(),
258 }
259 }
260}
261
262impl Default for DialogTitle {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267impl Styled for DialogTitle {
268 fn style(&mut self) -> &mut StyleRefinement {
269 &mut self.style
270 }
271}
272impl ParentElement for DialogTitle {
273 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
274 self.children.extend(elements);
275 }
276}
277impl RenderOnce for DialogTitle {
278 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
279 self.base
280 .id("dialog-title")
281 .children(self.children)
282 .refine_style(&self.style)
283 }
284}
285
286#[derive(IntoElement)]
288pub struct DialogDescription {
289 base: gpui::Div,
290 style: StyleRefinement,
291 children: SmallVec<[AnyElement; 2]>,
292}
293
294impl DialogDescription {
295 pub fn new() -> Self {
296 Self {
297 base: div(),
298 style: StyleRefinement::default(),
299 children: SmallVec::new(),
300 }
301 }
302}
303
304impl Default for DialogDescription {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309impl Styled for DialogDescription {
310 fn style(&mut self) -> &mut StyleRefinement {
311 &mut self.style
312 }
313}
314impl ParentElement for DialogDescription {
315 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
316 self.children.extend(elements);
317 }
318}
319impl RenderOnce for DialogDescription {
320 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
321 self.base
322 .id("dialog-description")
323 .children(self.children)
324 .refine_style(&self.style)
325 }
326}
327
328#[derive(IntoElement)]
330pub struct DialogClose {
331 style: StyleRefinement,
332 children: SmallVec<[AnyElement; 1]>,
333}
334
335impl DialogClose {
336 pub fn new() -> Self {
337 Self {
338 style: StyleRefinement::default(),
339 children: SmallVec::new(),
340 }
341 }
342}
343impl Default for DialogClose {
344 fn default() -> Self {
345 Self::new()
346 }
347}
348impl ParentElement for DialogClose {
349 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
350 self.children.extend(elements);
351 }
352}
353impl Styled for DialogClose {
354 fn style(&mut self) -> &mut StyleRefinement {
355 &mut self.style
356 }
357}
358impl RenderOnce for DialogClose {
359 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
360 div()
361 .id("dialog-close")
362 .on_click(|_, window, cx| window.dispatch_action(Box::new(Cancel), cx))
363 .children(self.children)
364 .refine_style(&self.style)
365 }
366}
367
368impl Dialog {
369 pub fn new(cx: &mut App) -> Self {
370 Self {
371 style: StyleRefinement::default(),
372 focus: cx.focus_handle(),
373 role: Role::Dialog,
374 layer: 0,
375 keyboard: true,
376 overlay_closable: true,
377 topmost: true,
378 dismiss_below_y: px(0.),
379 backdrop: None,
380 popup: None,
381 children: SmallVec::new(),
382 on_ok: Rc::new(|_, _, _| true),
383 on_cancel: Rc::new(|_, _, _| true),
384 on_close: Rc::new(|_, _, _| {}),
385 request_close: Rc::new(|_, _, _| {}),
386 handle: None,
387 open: true,
388 on_open_change: None,
389 }
390 }
391 pub fn open(mut self, open: bool) -> Self {
392 self.open = open;
393 self
394 }
395 pub fn handle(mut self, handle: DialogHandle) -> Self {
396 if let Some(callback) = self.on_open_change.as_ref() {
397 *handle.on_open_change.borrow_mut() = Some(callback.clone());
398 }
399 self.handle = Some(handle);
400 self
401 }
402 pub fn on_open_change(
403 mut self,
404 handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
405 ) -> Self {
406 let handler: OpenChange = Rc::new(handler);
407 if let Some(handle) = self.handle.as_ref() {
408 *handle.on_open_change.borrow_mut() = Some(handler.clone());
409 }
410 self.on_open_change = Some(handler);
411 self
412 }
413
414 pub fn backdrop(mut self, element: impl IntoElement) -> Self {
415 self.backdrop = Some(element.into_any_element());
416 self
417 }
418 pub fn popup(mut self, element: impl IntoElement) -> Self {
419 self.popup = Some(element.into_any_element());
420 self
421 }
422 pub fn close_on_escape(mut self, value: bool) -> Self {
423 self.keyboard = value;
424 self
425 }
426 pub fn close_on_backdrop_press(mut self, value: bool) -> Self {
427 self.overlay_closable = value;
428 self
429 }
430 pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
431 self.dismiss_below_y = value;
432 self
433 }
434 pub(crate) fn role(mut self, role: Role) -> Self {
435 self.role = role;
436 self
437 }
438 #[doc(hidden)]
439 pub fn layer(mut self, index: usize, topmost: bool) -> Self {
440 self.layer = index;
441 self.topmost = topmost;
442 self
443 }
444 #[doc(hidden)]
445 pub fn focus_handle(mut self, value: FocusHandle) -> Self {
446 self.focus = value;
447 self
448 }
449 #[doc(hidden)]
450 pub fn request_close(
451 mut self,
452 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
453 ) -> Self {
454 self.request_close = Rc::new(handler);
455 self
456 }
457}
458
459impl Styled for Dialog {
460 fn style(&mut self) -> &mut StyleRefinement {
461 &mut self.style
462 }
463}
464impl ParentElement for Dialog {
465 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
466 self.children.extend(elements);
467 }
468}
469
470impl RenderOnce for Dialog {
471 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
472 let open = self
473 .handle
474 .as_ref()
475 .map_or(self.open, DialogHandle::is_open);
476 if !open {
477 return div().into_any_element();
478 }
479 let request_close = self.request_close;
480 let cancel = self.on_cancel.clone();
481 let confirm = self.on_ok.clone();
482 let closed = self.on_close.clone();
483 let overlay_closable = self.overlay_closable && self.topmost;
484 let dismiss_below_y = self.dismiss_below_y;
485 let escape_handle = self.handle.clone();
486 let confirm_handle = self.handle.clone();
487 let backdrop_handle = self.handle.clone();
488 let escape_change = self.on_open_change.clone();
489 let confirm_change = self.on_open_change.clone();
490 let backdrop_change = self.on_open_change.clone();
491 let viewport = window.viewport_size();
492
493 deferred(
494 anchored().position(point(px(0.), px(0.))).child(
495 div()
496 .id(("dialog-host", self.layer))
497 .absolute()
498 .top_0()
499 .left_0()
500 .w(viewport.width)
501 .h(viewport.height)
502 .role(self.role)
503 .track_focus(&self.focus)
504 .focus_trap(format!("dialog-{}", self.layer), &self.focus)
505 .when(self.keyboard, |this| this.key_context(CONTEXT))
506 .map(|this| {
507 let request_cancel = request_close.clone();
508 let request_confirm = request_close.clone();
509 let closed_cancel = closed.clone();
510 this.on_action(move |_: &Cancel, window, cx| {
511 let event = ClickEvent::default();
512 if cancel(&event, window, cx) {
513 request_open_change(
514 &escape_handle,
515 &escape_change,
516 false,
517 DialogChangeReason::Cancel,
518 window,
519 cx,
520 );
521 request_cancel(false, window, cx);
522 closed_cancel(&event, window, cx);
523 }
524 })
525 .on_action(move |_: &Confirm, window, cx| {
526 let event = ClickEvent::default();
527 if confirm(&event, window, cx) {
528 request_open_change(
529 &confirm_handle,
530 &confirm_change,
531 false,
532 DialogChangeReason::Confirm,
533 window,
534 cx,
535 );
536 request_confirm(true, window, cx);
537 closed(&event, window, cx);
538 }
539 })
540 })
541 .when_some(self.backdrop, |this, backdrop| {
542 let cancel = self.on_cancel.clone();
543 let closed = self.on_close.clone();
544 let request_close = request_close.clone();
545 this.child(
546 div()
547 .absolute()
550 .inset_0()
551 .on_any_mouse_down(move |event, window, cx| {
552 if event.position.y < dismiss_below_y {
553 return;
554 }
555 let button = event.button;
556 cx.stop_propagation();
557 let event = ClickEvent::default();
558 if button == MouseButton::Left
559 && overlay_closable
560 && cancel(&event, window, cx)
561 {
562 request_open_change(
563 &backdrop_handle,
564 &backdrop_change,
565 false,
566 DialogChangeReason::BackdropPress,
567 window,
568 cx,
569 );
570 request_close(false, window, cx);
571 closed(&event, window, cx);
572 }
573 })
574 .child(backdrop),
575 )
576 })
577 .children(self.popup)
578 .children(self.children)
579 .refine_style(&self.style),
580 ),
581 )
582 .with_priority(10 + self.layer)
583 .into_any_element()
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use gpui::{Context, Render, point};
591 use std::{cell::RefCell, rc::Rc};
592
593 struct TriggerHarness {
594 handle: DialogHandle,
595 }
596 impl Render for TriggerHarness {
597 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
598 DialogTrigger::new(div().size(px(100.))).handle(self.handle.clone())
599 }
600 }
601
602 #[gpui::test]
603 fn trigger_opens_shared_handle_and_reports_reason(cx: &mut gpui::TestAppContext) {
604 let handle = DialogHandle::new(false);
605 let changes = Rc::new(RefCell::new(Vec::new()));
606 *handle.on_open_change.borrow_mut() = Some({
607 let changes = changes.clone();
608 Rc::new(move |open, reason, _, _| changes.borrow_mut().push((open, reason)))
609 });
610 let (_, cx) = cx.add_window_view({
611 let handle = handle.clone();
612 move |_, _| TriggerHarness { handle }
613 });
614 cx.update(|window, cx| window.draw(cx).clear(cx));
615 cx.simulate_click(point(px(20.), px(20.)), Default::default());
616
617 assert!(handle.is_open());
618 assert_eq!(
619 &*changes.borrow(),
620 &[(true, DialogChangeReason::TriggerPress)]
621 );
622 }
623
624 #[gpui::test]
628 fn the_backdrop_fills_the_host(cx: &mut gpui::TestAppContext) {
629 use gpui::{Bounds, canvas};
630 use std::cell::Cell;
631
632 struct Harness {
633 focus: FocusHandle,
634 bounds: Rc<Cell<Bounds<Pixels>>>,
635 }
636 impl Render for Harness {
637 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
638 let bounds = self.bounds.clone();
639 Dialog::new(cx)
640 .open(true)
641 .focus_handle(self.focus.clone())
642 .backdrop(
643 canvas(
644 move |bounds_of_backdrop, _, _| bounds.set(bounds_of_backdrop),
645 |_, _, _, _| {},
646 )
647 .absolute()
648 .size_full(),
649 )
650 .popup(div().size(px(100.)))
651 }
652 }
653
654 cx.update(crate::init);
655 let bounds = Rc::new(Cell::new(Bounds::default()));
656 let (_, cx) = cx.add_window_view({
657 let bounds = bounds.clone();
658 move |_, cx| Harness {
659 focus: cx.focus_handle(),
660 bounds,
661 }
662 });
663 let viewport = cx.update(|window, cx| {
664 let viewport = window.viewport_size();
665 window.draw(cx).clear(cx);
666 viewport
667 });
668
669 assert_eq!(
670 bounds.get().size,
671 viewport,
672 "a zero-sized backdrop paints no overlay behind the dialog"
673 );
674 }
675}