Skip to main content

gpui_component/dialog/
alert_dialog.rs

1use gpui::{
2    AnyElement, App, ClickEvent, IntoElement, ParentElement, Pixels, RenderOnce, StyleRefinement,
3    Styled, Window, prelude::FluentBuilder as _,
4};
5
6use crate::{
7    StyledExt as _, WindowExt as _,
8    dialog::{
9        Dialog, DialogButtonProps, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
10    },
11    h_flex, v_flex,
12};
13
14/// AlertDialog is a modal dialog that interrupts the user with important content
15/// and expects a response.
16///
17/// It is built on top of the Dialog component with opinionated defaults:
18/// - Footer buttons are center-aligned (vs right-aligned in Dialog)
19/// - Icon is optional (disabled by default, enable with `.show_icon(true)`)
20/// - Simplified API for common alert scenarios
21/// - Uses declarative DialogHeader, DialogTitle, DialogDescription, and DialogFooter components
22/// - Supports both imperative and declarative API styles
23///
24/// # Examples
25///
26/// ## Imperative API (using WindowExt)
27///
28/// ```ignore
29/// use gpui_kit::component::{AlertDialog, alert::AlertVariant};
30///
31/// // Using WindowExt trait
32/// window.open_alert_dialog(cx, |alert, _, _| {
33///     alert
34///         .title("Unsaved Changes")
35///         .description("You have unsaved changes. Are you sure you want to leave?")
36///         .show_cancel(true)
37/// });
38/// ```
39///
40/// ## Declarative API (using trigger and content)
41///
42/// ```ignore
43/// use gpui_kit::component::{AlertDialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter};
44///
45/// AlertDialog::new(cx)
46///     .trigger(Button::new("delete").label("Delete"))
47///     .content(|content, _, cx| {
48///         content
49///             .child(
50///                 DialogHeader::new()
51///                     .items_center()
52///                     .child(DialogTitle::new().child("Delete File"))
53///                     .child(DialogDescription::new().child("Are you sure?"))
54///             )
55///             .child(
56///                 DialogFooter::new()
57///                     .justify_center()
58///                     .child(Button::new("cancel").label("Cancel"))
59///                     .child(Button::new("confirm").label("Delete"))
60///             )
61///     })
62/// ```
63#[derive(IntoElement)]
64pub struct AlertDialog {
65    base: Dialog,
66    trigger: Option<AnyElement>,
67    icon: Option<AnyElement>,
68    title: Option<AnyElement>,
69    description: Option<AnyElement>,
70    button_props: DialogButtonProps,
71    children: Vec<AnyElement>,
72}
73
74impl AlertDialog {
75    /// Create a new AlertDialog.
76    ///
77    /// By default, the dialog is not overlay closable with a OK button.
78    ///
79    pub fn new(cx: &mut App) -> Self {
80        Self {
81            base: Dialog::new(cx)
82                .with_base_alert_dialog(gpui_base::AlertDialog::new(cx))
83                .close_button(false),
84            trigger: None,
85            icon: None,
86            title: None,
87            description: None,
88            button_props: DialogButtonProps::default(),
89            children: Vec::new(),
90        }
91    }
92
93    /// Set to use confirm dialog, with OK and Cancel buttons.
94    ///
95    /// The default of [`AlertDialog`] has OK button.
96    pub fn confirm(mut self) -> Self {
97        self.button_props.show_cancel = true;
98        self
99    }
100
101    /// Sets the trigger element for the alert dialog.
102    ///
103    /// When a trigger is set, the dialog will render as a trigger element that opens the dialog when clicked.
104    ///
105    /// **Note**: When using `.trigger()`, you should also use `.content()` to define the dialog content
106    /// declaratively instead of using `.title()`, `.description()`, etc.
107    ///
108    /// The `title`, `description`, `icon`, and `button_props` will be ignored when used together with `.trigger()`.
109    pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
110        self.trigger = Some(trigger.into_any_element());
111        self
112    }
113
114    /// Sets the content builder for declarative API.
115    ///
116    /// When using this method, you define the dialog content using declarative components like
117    /// `DialogHeader`, `DialogTitle`, `DialogDescription`, and `DialogFooter`.
118    ///
119    /// This method is typically used together with `.trigger()` for a fully declarative API.
120    ///
121    /// # Examples
122    ///
123    /// ```ignore
124    /// AlertDialog::new(cx)
125    ///     .trigger(Button::new("delete").label("Delete"))
126    ///     .content(|content, _, cx| {
127    ///         content
128    ///             .child(DialogHeader::new().child(DialogTitle::new().child("Confirm")))
129    ///             .child(DialogFooter::new().child(Button::new("ok").label("OK")))
130    ///     })
131    /// ```
132    pub fn content<F>(mut self, builder: F) -> Self
133    where
134        F: Fn(crate::dialog::DialogContent, &mut Window, &mut App) -> crate::dialog::DialogContent
135            + 'static,
136    {
137        self.base = self.base.content(builder);
138        self
139    }
140
141    /// Sets the footer builder for declarative API.
142    ///
143    /// This is used to define the footer content using declarative components like `DialogFooter`.
144    ///
145    /// If not set, a default footer with OK and optional Cancel button will be used.
146    pub fn footer(mut self, footer: impl IntoElement) -> Self {
147        self.base = self.base.footer(footer);
148        self
149    }
150
151    #[track_caller]
152    fn debug_assert_no_trigger(&self) {
153        debug_assert!(
154            self.trigger.is_none() && self.base.content_builder.is_none(),
155            "Cannot set this property when trigger is used. Use content() to define dialog content instead."
156        );
157    }
158
159    /// Sets the icon of the alert dialog, default is None.
160    #[track_caller]
161    pub fn icon(mut self, icon: impl IntoElement) -> Self {
162        self.debug_assert_no_trigger();
163        self.icon = Some(icon.into_any_element());
164        self
165    }
166
167    /// Sets the title of the alert dialog.
168    #[track_caller]
169    pub fn title(mut self, title: impl IntoElement) -> Self {
170        self.debug_assert_no_trigger();
171        self.title = Some(title.into_any_element());
172        self
173    }
174
175    /// Sets the description of the alert dialog.
176    #[track_caller]
177    pub fn description(mut self, description: impl IntoElement) -> Self {
178        self.debug_assert_no_trigger();
179        self.description = Some(description.into_any_element());
180        self
181    }
182
183    /// Set the button props of the alert dialog.
184    ///
185    /// Use this to configure button text, variants, and visibility.
186    ///
187    /// # Examples
188    ///
189    /// ```ignore
190    /// alert.button_props(
191    ///     DialogButtonProps::default()
192    ///         .ok_text("Delete")
193    ///         .ok_variant(ButtonVariant::Danger)
194    ///         .cancel_text("Keep")
195    ///         .show_cancel(true)
196    /// )
197    /// ```
198    #[track_caller]
199    pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
200        self.debug_assert_no_trigger();
201        self.button_props = button_props;
202        self
203    }
204
205    /// Sets the width of the alert dialog, defaults to 420px.
206    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
207        self.base = self.base.width(width);
208        self
209    }
210
211    /// Show cancel button. Default is false.
212    pub fn show_cancel(mut self, show_cancel: bool) -> Self {
213        self.button_props = self.button_props.show_cancel(show_cancel);
214        self
215    }
216
217    /// Alert dialogs never close from a backdrop press.
218    #[deprecated(note = "AlertDialog backdrop dismissal is disabled by design")]
219    pub fn overlay_closable(self, _: bool) -> Self {
220        self
221    }
222
223    /// Set the close button of the alert dialog, defaults to `false`.
224    pub fn close_button(mut self, close_button: bool) -> Self {
225        self.base = self.base.close_button(close_button);
226        self
227    }
228
229    /// Set whether to support keyboard esc to close the dialog, defaults to `true`.
230    pub fn keyboard(mut self, keyboard: bool) -> Self {
231        self.base = self.base.keyboard(keyboard);
232        self
233    }
234
235    /// Sets the callback for when the alert dialog is closed.
236    ///
237    /// Called after [`Self::on_action`] or [`Self::on_cancel`] callback.
238    pub fn on_close(
239        mut self,
240        on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
241    ) -> Self {
242        self.base = self.base.on_close(on_close);
243        self
244    }
245
246    /// Sets the callback for when the OK/action button is clicked.
247    ///
248    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
249    pub fn on_ok(
250        mut self,
251        on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
252    ) -> Self {
253        self.button_props = self.button_props.on_ok(on_ok);
254        self
255    }
256
257    /// Sets the callback for when the alert dialog has been canceled.
258    ///
259    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
260    pub fn on_cancel(
261        mut self,
262        on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
263    ) -> Self {
264        self.button_props = self.button_props.on_cancel(on_cancel);
265        self
266    }
267
268    /// Build the styled dialog surface around the Base alert-dialog host.
269    pub(crate) fn build_surface(self, window: &mut Window, cx: &mut App) -> Dialog {
270        let button_props = self.button_props.clone();
271        let has_title = self.icon.is_some() || self.title.is_some();
272        let has_header = has_title || self.description.is_some();
273        let has_footer = self.base.footer.is_some();
274
275        self.base
276            .button_props(button_props.clone())
277            .when(has_header, |this| {
278                this.header(
279                    DialogHeader::new().child(
280                        h_flex()
281                            .gap_2()
282                            .items_start()
283                            .when_some(self.icon, |row, icon| row.child(icon))
284                            .child(
285                                v_flex()
286                                    .flex_1()
287                                    .min_w_0()
288                                    .gap_1()
289                                    .when_some(self.title, |this, title| {
290                                        this.child(DialogTitle::new().child(title))
291                                    })
292                                    .when_some(self.description, |this, desc| {
293                                        this.child(DialogDescription::new().child(desc))
294                                    }),
295                            ),
296                    ),
297                )
298            })
299            .children(self.children)
300            .when(!has_footer, |this| {
301                // Default footer for AlertDialog if user doesn't provide one, with OK and optional Cancel button
302                this.footer(
303                    DialogFooter::new()
304                        .when(button_props.show_cancel, |this| {
305                            this.child(button_props.render_cancel(window, cx))
306                        })
307                        .child(button_props.render_ok(window, cx)),
308                )
309            })
310    }
311}
312
313impl Styled for AlertDialog {
314    fn style(&mut self) -> &mut StyleRefinement {
315        &mut self.base.style
316    }
317}
318
319impl ParentElement for AlertDialog {
320    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
321        self.children.extend(elements);
322    }
323}
324
325impl AlertDialog {
326    fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
327        let content_builder = self.base.content_builder.clone();
328        let style = self.base.style.clone();
329        let props = self.base.props.clone();
330        let mut button_props = self.button_props.clone();
331        button_props.on_close = self.base.button_props.on_close.clone();
332
333        gpui_base::AlertDialogTrigger::new(trigger)
334            .on_open(move |window, cx| {
335                let content_builder = content_builder.clone();
336                let style = style.clone();
337                let props = props.clone();
338                let button_props = button_props.clone();
339                window.open_dialog(cx, move |dialog, _, cx| {
340                    dialog
341                        .with_base_alert_dialog(gpui_base::AlertDialog::new(cx))
342                        .refine_style(&style)
343                        .button_props(button_props.clone())
344                        .with_props(props.clone())
345                        .when_some(content_builder.clone(), |this, content_builder| {
346                            this.content(move |content, window, cx| {
347                                content_builder(content, window, cx)
348                            })
349                        })
350                });
351            })
352            .into_any_element()
353    }
354}
355
356impl RenderOnce for AlertDialog {
357    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
358        if let Some(trigger) = self.trigger.take() {
359            // If a trigger is provided, render the trigger element that opens the dialog
360            self.render_trigger(trigger, window, cx)
361        } else {
362            // Otherwise, render the dialog content directly
363            self.build_surface(window, cx).into_any_element()
364        }
365    }
366}