1use gpui::{
2 AnyElement, App, ClickEvent, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
3 StyleRefinement, Styled, Window, prelude::FluentBuilder as _,
4};
5
6use crate::{
7 StyledExt as _, WindowExt as _,
8 button::ButtonVariant,
9 dialog::{
10 Dialog, DialogButtonProps, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
11 },
12 h_flex, v_flex,
13};
14
15#[derive(IntoElement)]
65pub struct AlertDialog {
66 base: Dialog,
67 trigger: Option<AnyElement>,
68 icon: Option<AnyElement>,
69 title: Option<AnyElement>,
70 description: Option<AnyElement>,
71 children: Vec<AnyElement>,
72}
73
74impl AlertDialog {
75 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 children: Vec::new(),
89 }
90 }
91
92 pub fn confirm(mut self) -> Self {
96 self.base.button_props.show_cancel = Some(true);
97 self
98 }
99
100 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
109 self.trigger = Some(trigger.into_any_element());
110 self
111 }
112
113 pub fn content<F>(mut self, builder: F) -> Self
132 where
133 F: Fn(crate::dialog::DialogContent, &mut Window, &mut App) -> crate::dialog::DialogContent
134 + 'static,
135 {
136 self.base = self.base.content(builder);
137 self
138 }
139
140 pub fn footer(mut self, footer: impl IntoElement) -> Self {
146 self.base = self.base.footer(footer);
147 self
148 }
149
150 #[track_caller]
151 fn debug_assert_no_trigger(&self) {
152 debug_assert!(
153 self.trigger.is_none() && self.base.content_builder.is_none(),
154 "Cannot set this property when trigger is used. Use content() to define dialog content instead."
155 );
156 }
157
158 #[track_caller]
160 pub fn icon(mut self, icon: impl IntoElement) -> Self {
161 self.debug_assert_no_trigger();
162 self.icon = Some(icon.into_any_element());
163 self
164 }
165
166 #[track_caller]
168 pub fn title(mut self, title: impl IntoElement) -> Self {
169 self.debug_assert_no_trigger();
170 self.title = Some(title.into_any_element());
171 self
172 }
173
174 #[track_caller]
176 pub fn description(mut self, description: impl IntoElement) -> Self {
177 self.debug_assert_no_trigger();
178 self.description = Some(description.into_any_element());
179 self
180 }
181
182 #[track_caller]
202 pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
203 self.debug_assert_no_trigger();
204 self.base = self.base.button_props(button_props);
205 self
206 }
207
208 pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
210 self.base.button_props.ok_text = Some(ok_text.into());
211 self
212 }
213
214 pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
216 self.base.button_props.ok_variant = Some(ok_variant);
217 self
218 }
219
220 pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
222 self.base.button_props.cancel_text = Some(cancel_text.into());
223 self
224 }
225
226 pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
228 self.base.button_props.cancel_variant = Some(cancel_variant);
229 self
230 }
231
232 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
234 self.base = self.base.width(width);
235 self
236 }
237
238 pub fn show_cancel(mut self, show_cancel: bool) -> Self {
240 self.base.button_props.show_cancel = Some(show_cancel);
241 self
242 }
243
244 #[deprecated(note = "AlertDialog backdrop dismissal is disabled by design")]
246 pub fn overlay_closable(self, _: bool) -> Self {
247 self
248 }
249
250 pub fn close_button(mut self, close_button: bool) -> Self {
252 self.base = self.base.close_button(close_button);
253 self
254 }
255
256 pub fn keyboard(mut self, keyboard: bool) -> Self {
258 self.base = self.base.keyboard(keyboard);
259 self
260 }
261
262 pub fn on_close(
266 mut self,
267 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
268 ) -> Self {
269 self.base = self.base.on_close(on_close);
270 self
271 }
272
273 pub fn on_ok(
277 mut self,
278 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
279 ) -> Self {
280 self.base = self.base.on_ok(on_ok);
281 self
282 }
283
284 pub fn on_cancel(
288 mut self,
289 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
290 ) -> Self {
291 self.base = self.base.on_cancel(on_cancel);
292 self
293 }
294
295 pub(crate) fn build_surface(self, window: &mut Window, cx: &mut App) -> Dialog {
297 let button_props = self.base.button_props.clone();
298 let has_title = self.icon.is_some() || self.title.is_some();
299 let has_header = has_title || self.description.is_some();
300 let has_footer = self.base.footer.is_some();
301
302 self.base
303 .when(has_header, |this| {
304 this.header(
305 DialogHeader::new().child(
306 h_flex()
307 .gap_2()
308 .items_start()
309 .when_some(self.icon, |row, icon| row.child(icon))
310 .child(
311 v_flex()
312 .flex_1()
313 .min_w_0()
314 .gap_1()
315 .when_some(self.title, |this, title| {
316 this.child(DialogTitle::new().child(title))
317 })
318 .when_some(self.description, |this, desc| {
319 this.child(DialogDescription::new().child(desc))
320 }),
321 ),
322 ),
323 )
324 })
325 .children(self.children)
326 .when(!has_footer, |this| {
327 this.footer(
329 DialogFooter::new()
330 .when(button_props.is_cancel_shown(), |this| {
331 this.child(button_props.render_cancel(window, cx))
332 })
333 .child(button_props.render_ok(window, cx)),
334 )
335 })
336 }
337}
338
339impl Styled for AlertDialog {
340 fn style(&mut self) -> &mut StyleRefinement {
341 &mut self.base.style
342 }
343}
344
345impl ParentElement for AlertDialog {
346 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
347 self.children.extend(elements);
348 }
349}
350
351impl AlertDialog {
352 fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
353 let content_builder = self.base.content_builder.clone();
354 let style = self.base.style.clone();
355 let props = self.base.props.clone();
356 let button_props = self.base.button_props.clone();
357
358 gpui_base::AlertDialogTrigger::new(trigger)
359 .on_open(move |window, cx| {
360 let content_builder = content_builder.clone();
361 let style = style.clone();
362 let props = props.clone();
363 let button_props = button_props.clone();
364 window.open_dialog(cx, move |dialog, _, cx| {
365 dialog
366 .with_base_alert_dialog(gpui_base::AlertDialog::new(cx))
367 .refine_style(&style)
368 .button_props(button_props.clone())
369 .with_props(props.clone())
370 .when_some(content_builder.clone(), |this, content_builder| {
371 this.content(move |content, window, cx| {
372 content_builder(content, window, cx)
373 })
374 })
375 });
376 })
377 .into_any_element()
378 }
379}
380
381impl RenderOnce for AlertDialog {
382 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
383 if let Some(trigger) = self.trigger.take() {
384 self.render_trigger(trigger, window, cx)
386 } else {
387 self.build_surface(window, cx).into_any_element()
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use std::{cell::Cell, rc::Rc};
396
397 use gpui::{ClickEvent, TestAppContext, px, size};
398
399 use super::*;
400 use crate::dialog::dialog::tests::window;
401
402 #[gpui::test]
405 fn button_props_after_confirm_keeps_the_cancel_button(cx: &mut TestAppContext) {
406 let cx = window(cx, size(px(800.), px(600.)));
407 cx.update(|_, cx| {
408 let alert = AlertDialog::new(cx)
409 .confirm()
410 .button_props(DialogButtonProps::default().ok_text("Delete"));
411
412 assert!(alert.base.button_props.is_cancel_shown());
413 assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete"));
414 });
415 }
416
417 #[gpui::test]
419 fn confirm_after_button_props_keeps_the_ok_text(cx: &mut TestAppContext) {
420 let cx = window(cx, size(px(800.), px(600.)));
421 cx.update(|_, cx| {
422 let alert = AlertDialog::new(cx)
423 .button_props(DialogButtonProps::default().ok_text("Delete"))
424 .confirm();
425
426 assert!(alert.base.button_props.is_cancel_shown());
427 assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete"));
428 });
429 }
430
431 #[gpui::test]
434 fn button_props_after_on_ok_keeps_the_callback(cx: &mut TestAppContext) {
435 let cx = window(cx, size(px(800.), px(600.)));
436 let confirmed = Rc::new(Cell::new(0));
437 let counter = confirmed.clone();
438 cx.update(|window, cx| {
439 let alert = AlertDialog::new(cx)
440 .on_ok(move |_, _, _| {
441 counter.set(counter.get() + 1);
442 true
443 })
444 .button_props(DialogButtonProps::default().ok_text("Delete"));
445
446 assert!(alert.base.button_props.ok_handler()(
447 &ClickEvent::default(),
448 window,
449 cx
450 ));
451 });
452
453 assert_eq!(confirmed.get(), 1);
454 }
455
456 #[gpui::test]
458 fn the_direct_builders_match_button_props(cx: &mut TestAppContext) {
459 let cx = window(cx, size(px(800.), px(600.)));
460 cx.update(|_, cx| {
461 let direct = AlertDialog::new(cx)
462 .confirm()
463 .ok_text("Delete")
464 .ok_variant(ButtonVariant::Danger)
465 .cancel_text("Keep")
466 .cancel_variant(ButtonVariant::Ghost);
467 let bundled = AlertDialog::new(cx).button_props(
468 DialogButtonProps::default()
469 .show_cancel(true)
470 .ok_text("Delete")
471 .ok_variant(ButtonVariant::Danger)
472 .cancel_text("Keep")
473 .cancel_variant(ButtonVariant::Ghost),
474 );
475
476 for props in [&direct.base.button_props, &bundled.base.button_props] {
477 assert!(props.is_cancel_shown());
478 assert_eq!(props.ok_text.as_deref(), Some("Delete"));
479 assert_eq!(props.ok_variant, Some(ButtonVariant::Danger));
480 assert_eq!(props.cancel_text.as_deref(), Some("Keep"));
481 assert_eq!(props.cancel_variant, Some(ButtonVariant::Ghost));
482 }
483 });
484 }
485}