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}