Skip to main content

gpui_base/
alert_dialog.rs

1use gpui::prelude::FluentBuilder as _;
2use gpui::{
3    App, ClickEvent, FocusHandle, InteractiveElement as _, IntoElement, MouseButton, ParentElement,
4    Pixels, RenderOnce, Role, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
5    div,
6};
7use smallvec::SmallVec;
8
9use crate::StyledExt as _;
10use crate::{Dialog, DialogChangeReason, DialogHandle};
11
12macro_rules! alert_part {
13    ($name:ident, $id:literal) => {
14        alert_part!($name, $id, false);
15    };
16    ($name:ident, $id:literal, $occlude:literal) => {
17        #[derive(IntoElement)]
18        pub struct $name {
19            style: StyleRefinement,
20            children: SmallVec<[gpui::AnyElement; 2]>,
21        }
22        impl $name {
23            pub fn new() -> Self {
24                Self {
25                    style: StyleRefinement::default(),
26                    children: SmallVec::new(),
27                }
28            }
29        }
30        impl Default for $name {
31            fn default() -> Self {
32                Self::new()
33            }
34        }
35        impl ParentElement for $name {
36            fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
37                self.children.extend(elements);
38            }
39        }
40        impl Styled for $name {
41            fn style(&mut self) -> &mut StyleRefinement {
42                &mut self.style
43            }
44        }
45        impl RenderOnce for $name {
46            fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
47                div()
48                    .id($id)
49                    // Presses on the popup stay off the backdrop behind it.
50                    .when($occlude, |this| this.occlude())
51                    .children(self.children)
52                    .refine_style(&self.style)
53            }
54        }
55    };
56}
57
58alert_part!(AlertDialogBackdrop, "alert-dialog-backdrop");
59alert_part!(AlertDialogPopup, "alert-dialog-popup", true);
60alert_part!(AlertDialogTitle, "alert-dialog-title");
61alert_part!(AlertDialogDescription, "alert-dialog-description");
62
63#[derive(IntoElement)]
64pub struct AlertDialogTrigger {
65    trigger: gpui::AnyElement,
66    open: std::rc::Rc<dyn Fn(&mut Window, &mut App)>,
67    handle: Option<DialogHandle>,
68}
69impl AlertDialogTrigger {
70    pub fn new(trigger: impl IntoElement) -> Self {
71        Self {
72            trigger: trigger.into_any_element(),
73            open: std::rc::Rc::new(|_, _| {}),
74            handle: None,
75        }
76    }
77    pub fn on_open(mut self, open: impl Fn(&mut Window, &mut App) + 'static) -> Self {
78        self.open = std::rc::Rc::new(open);
79        self
80    }
81    pub fn handle(mut self, handle: DialogHandle) -> Self {
82        self.handle = Some(handle);
83        self
84    }
85}
86impl RenderOnce for AlertDialogTrigger {
87    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
88        div()
89            .on_mouse_down(MouseButton::Left, move |_, window, cx| {
90                if let Some(handle) = self.handle.as_ref() {
91                    handle.set_open(true, DialogChangeReason::TriggerPress, window, cx);
92                }
93                (self.open)(window, cx);
94                cx.stop_propagation();
95            })
96            .child(self.trigger)
97    }
98}
99
100macro_rules! alert_close_part {
101    ($name:ident, $id:literal) => {
102        #[derive(IntoElement)]
103        pub struct $name {
104            style: StyleRefinement,
105            children: SmallVec<[gpui::AnyElement; 1]>,
106        }
107        impl $name {
108            pub fn new() -> Self {
109                Self {
110                    style: StyleRefinement::default(),
111                    children: SmallVec::new(),
112                }
113            }
114        }
115        impl Default for $name {
116            fn default() -> Self {
117                Self::new()
118            }
119        }
120        impl ParentElement for $name {
121            fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
122                self.children.extend(elements);
123            }
124        }
125        impl Styled for $name {
126            fn style(&mut self) -> &mut StyleRefinement {
127                &mut self.style
128            }
129        }
130        impl RenderOnce for $name {
131            fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
132                div()
133                    .id($id)
134                    .on_click(|_, window, cx| {
135                        window.dispatch_action(Box::new(crate::actions::Cancel), cx)
136                    })
137                    .children(self.children)
138                    .refine_style(&self.style)
139            }
140        }
141    };
142}
143alert_close_part!(AlertDialogClose, "alert-dialog-close");
144alert_close_part!(AlertDialogCancel, "alert-dialog-cancel");
145
146/// Wrapper that dispatches the alert dialog's confirm action.
147#[derive(IntoElement)]
148pub struct AlertDialogAction {
149    style: StyleRefinement,
150    children: SmallVec<[gpui::AnyElement; 1]>,
151}
152
153impl AlertDialogAction {
154    pub fn new() -> Self {
155        Self {
156            style: StyleRefinement::default(),
157            children: SmallVec::new(),
158        }
159    }
160}
161
162impl Default for AlertDialogAction {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl ParentElement for AlertDialogAction {
169    fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
170        self.children.extend(elements);
171    }
172}
173
174impl Styled for AlertDialogAction {
175    fn style(&mut self) -> &mut StyleRefinement {
176        &mut self.style
177    }
178}
179
180impl RenderOnce for AlertDialogAction {
181    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
182        div()
183            .id("alert-dialog-action")
184            .on_click(|_, window, cx| {
185                window.dispatch_action(Box::new(crate::actions::Confirm { secondary: false }), cx)
186            })
187            .children(self.children)
188            .refine_style(&self.style)
189    }
190}
191
192/// Alert-dialog specialization of the Base modal host.
193pub struct AlertDialog(Dialog);
194
195impl AlertDialog {
196    pub fn new(cx: &mut App) -> Self {
197        Self(
198            Dialog::new(cx)
199                .role(Role::AlertDialog)
200                .close_on_backdrop_press(false),
201        )
202    }
203    pub fn open(mut self, open: bool) -> Self {
204        self.0 = self.0.open(open);
205        self
206    }
207    pub fn handle(mut self, handle: DialogHandle) -> Self {
208        self.0 = self.0.handle(handle);
209        self
210    }
211    pub fn on_open_change(
212        mut self,
213        handler: impl Fn(bool, DialogChangeReason, &mut Window, &mut App) + 'static,
214    ) -> Self {
215        self.0 = self.0.on_open_change(handler);
216        self
217    }
218    pub fn backdrop(mut self, element: impl IntoElement) -> Self {
219        self.0 = self.0.backdrop(element);
220        self
221    }
222    pub fn popup(mut self, element: impl IntoElement) -> Self {
223        self.0 = self.0.popup(element);
224        self
225    }
226    pub fn close_on_escape(mut self, value: bool) -> Self {
227        self.0 = self.0.close_on_escape(value);
228        self
229    }
230    pub fn dismiss_below_y(mut self, value: Pixels) -> Self {
231        self.0 = self.0.dismiss_below_y(value);
232        self
233    }
234    pub fn on_ok(
235        mut self,
236        handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
237    ) -> Self {
238        self.0 = self.0.on_ok(handler);
239        self
240    }
241    pub fn on_cancel(
242        mut self,
243        handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
244    ) -> Self {
245        self.0 = self.0.on_cancel(handler);
246        self
247    }
248    pub fn on_close(
249        mut self,
250        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
251    ) -> Self {
252        self.0 = self.0.on_close(handler);
253        self
254    }
255    #[doc(hidden)]
256    pub fn layer(mut self, index: usize, topmost: bool) -> Self {
257        self.0 = self.0.layer(index, topmost);
258        self
259    }
260    #[doc(hidden)]
261    pub fn focus_handle(mut self, value: FocusHandle) -> Self {
262        self.0 = self.0.focus_handle(value);
263        self
264    }
265    #[doc(hidden)]
266    pub fn request_close(
267        mut self,
268        handler: impl Fn(bool, &mut Window, &mut App) + 'static,
269    ) -> Self {
270        self.0 = self.0.request_close(handler);
271        self
272    }
273}
274
275impl Styled for AlertDialog {
276    fn style(&mut self) -> &mut StyleRefinement {
277        self.0.style()
278    }
279}
280impl ParentElement for AlertDialog {
281    fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
282        self.0.extend(elements);
283    }
284}
285impl IntoElement for AlertDialog {
286    type Element = <Dialog as IntoElement>::Element;
287    fn into_element(self) -> Self::Element {
288        self.0.into_element()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use gpui::{Context, Render, div, point, px};
296    use std::{cell::Cell, rc::Rc};
297
298    struct Harness {
299        close_requested: Rc<Cell<bool>>,
300    }
301
302    impl Render for Harness {
303        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
304            let close_requested = self.close_requested.clone();
305            AlertDialog::new(cx)
306                .request_close(move |_, _, _| close_requested.set(true))
307                .backdrop(div().size(px(200.)))
308        }
309    }
310
311    #[gpui::test]
312    fn backdrop_is_not_closable_by_default(cx: &mut gpui::TestAppContext) {
313        cx.update(crate::init);
314        let close_requested = Rc::new(Cell::new(false));
315        let (_, cx) = cx.add_window_view({
316            let close_requested = close_requested.clone();
317            move |_, _| Harness { close_requested }
318        });
319        cx.update(|window, cx| window.draw(cx).clear(cx));
320
321        cx.simulate_click(point(px(20.), px(20.)), Default::default());
322
323        assert!(!close_requested.get());
324    }
325
326    /// The host centers its popup by default; styling the alert dialog
327    /// replaces that layout just as it does for a plain dialog.
328    #[gpui::test]
329    fn host_style_positions_the_popup(cx: &mut gpui::TestAppContext) {
330        use gpui::{Bounds, canvas};
331
332        struct TopLeft(Rc<Cell<Bounds<Pixels>>>);
333        impl Render for TopLeft {
334            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
335                let bounds = self.0.clone();
336                AlertDialog::new(cx).items_start().justify_start().popup(
337                    canvas(move |popup, _, _| bounds.set(popup), |_, _, _, _| {}).size(px(100.)),
338                )
339            }
340        }
341
342        cx.update(crate::init);
343        let bounds = Rc::new(Cell::new(Bounds::default()));
344        let (_, cx) = cx.add_window_view({
345            let bounds = bounds.clone();
346            move |_, _| TopLeft(bounds)
347        });
348        cx.update(|window, cx| window.draw(cx).clear(cx));
349
350        assert_eq!(bounds.get().origin, point(px(0.), px(0.)));
351    }
352}