use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
StyleRefinement, Styled, Window, prelude::FluentBuilder as _,
};
use crate::{
StyledExt as _, WindowExt as _,
button::ButtonVariant,
dialog::{
Dialog, DialogButtonProps, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
},
h_flex, v_flex,
};
#[derive(IntoElement)]
pub struct AlertDialog {
base: Dialog,
trigger: Option<AnyElement>,
icon: Option<AnyElement>,
title: Option<AnyElement>,
description: Option<AnyElement>,
children: Vec<AnyElement>,
}
impl AlertDialog {
pub fn new(cx: &mut App) -> Self {
Self {
base: Dialog::new(cx)
.with_base_alert_dialog(gpui_base::AlertDialog::new(cx))
.close_button(false),
trigger: None,
icon: None,
title: None,
description: None,
children: Vec::new(),
}
}
pub fn confirm(mut self) -> Self {
self.base.button_props.show_cancel = Some(true);
self
}
pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
self.trigger = Some(trigger.into_any_element());
self
}
pub fn content<F>(mut self, builder: F) -> Self
where
F: Fn(crate::dialog::DialogContent, &mut Window, &mut App) -> crate::dialog::DialogContent
+ 'static,
{
self.base = self.base.content(builder);
self
}
pub fn footer(mut self, footer: impl IntoElement) -> Self {
self.base = self.base.footer(footer);
self
}
#[track_caller]
fn debug_assert_no_trigger(&self) {
debug_assert!(
self.trigger.is_none() && self.base.content_builder.is_none(),
"Cannot set this property when trigger is used. Use content() to define dialog content instead."
);
}
#[track_caller]
pub fn icon(mut self, icon: impl IntoElement) -> Self {
self.debug_assert_no_trigger();
self.icon = Some(icon.into_any_element());
self
}
#[track_caller]
pub fn title(mut self, title: impl IntoElement) -> Self {
self.debug_assert_no_trigger();
self.title = Some(title.into_any_element());
self
}
#[track_caller]
pub fn description(mut self, description: impl IntoElement) -> Self {
self.debug_assert_no_trigger();
self.description = Some(description.into_any_element());
self
}
#[track_caller]
pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
self.debug_assert_no_trigger();
self.base = self.base.button_props(button_props);
self
}
pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
self.base.button_props.ok_text = Some(ok_text.into());
self
}
pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
self.base.button_props.ok_variant = Some(ok_variant);
self
}
pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
self.base.button_props.cancel_text = Some(cancel_text.into());
self
}
pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
self.base.button_props.cancel_variant = Some(cancel_variant);
self
}
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.base = self.base.width(width);
self
}
pub fn show_cancel(mut self, show_cancel: bool) -> Self {
self.base.button_props.show_cancel = Some(show_cancel);
self
}
#[deprecated(note = "AlertDialog backdrop dismissal is disabled by design")]
pub fn overlay_closable(self, _: bool) -> Self {
self
}
pub fn close_button(mut self, close_button: bool) -> Self {
self.base = self.base.close_button(close_button);
self
}
pub fn keyboard(mut self, keyboard: bool) -> Self {
self.base = self.base.keyboard(keyboard);
self
}
pub fn on_close(
mut self,
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.base = self.base.on_close(on_close);
self
}
pub fn on_ok(
mut self,
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
) -> Self {
self.base = self.base.on_ok(on_ok);
self
}
pub fn on_cancel(
mut self,
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
) -> Self {
self.base = self.base.on_cancel(on_cancel);
self
}
pub(crate) fn build_surface(self, window: &mut Window, cx: &mut App) -> Dialog {
let button_props = self.base.button_props.clone();
let has_title = self.icon.is_some() || self.title.is_some();
let has_header = has_title || self.description.is_some();
let has_footer = self.base.footer.is_some();
self.base
.when(has_header, |this| {
this.header(
DialogHeader::new().child(
h_flex()
.gap_2()
.items_start()
.when_some(self.icon, |row, icon| row.child(icon))
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.when_some(self.title, |this, title| {
this.child(DialogTitle::new().child(title))
})
.when_some(self.description, |this, desc| {
this.child(DialogDescription::new().child(desc))
}),
),
),
)
})
.children(self.children)
.when(!has_footer, |this| {
this.footer(
DialogFooter::new()
.when(button_props.is_cancel_shown(), |this| {
this.child(button_props.render_cancel(window, cx))
})
.child(button_props.render_ok(window, cx)),
)
})
}
}
impl Styled for AlertDialog {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.base.style
}
}
impl ParentElement for AlertDialog {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl AlertDialog {
fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
let content_builder = self.base.content_builder.clone();
let style = self.base.style.clone();
let props = self.base.props.clone();
let button_props = self.base.button_props.clone();
gpui_base::AlertDialogTrigger::new(trigger)
.on_open(move |window, cx| {
let content_builder = content_builder.clone();
let style = style.clone();
let props = props.clone();
let button_props = button_props.clone();
window.open_dialog(cx, move |dialog, _, cx| {
dialog
.with_base_alert_dialog(gpui_base::AlertDialog::new(cx))
.refine_style(&style)
.button_props(button_props.clone())
.with_props(props.clone())
.when_some(content_builder.clone(), |this, content_builder| {
this.content(move |content, window, cx| {
content_builder(content, window, cx)
})
})
});
})
.into_any_element()
}
}
impl RenderOnce for AlertDialog {
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
if let Some(trigger) = self.trigger.take() {
self.render_trigger(trigger, window, cx)
} else {
self.build_surface(window, cx).into_any_element()
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc};
use gpui::{ClickEvent, TestAppContext, px, size};
use super::*;
use crate::dialog::dialog::tests::window;
#[gpui::test]
fn button_props_after_confirm_keeps_the_cancel_button(cx: &mut TestAppContext) {
let cx = window(cx, size(px(800.), px(600.)));
cx.update(|_, cx| {
let alert = AlertDialog::new(cx)
.confirm()
.button_props(DialogButtonProps::default().ok_text("Delete"));
assert!(alert.base.button_props.is_cancel_shown());
assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete"));
});
}
#[gpui::test]
fn confirm_after_button_props_keeps_the_ok_text(cx: &mut TestAppContext) {
let cx = window(cx, size(px(800.), px(600.)));
cx.update(|_, cx| {
let alert = AlertDialog::new(cx)
.button_props(DialogButtonProps::default().ok_text("Delete"))
.confirm();
assert!(alert.base.button_props.is_cancel_shown());
assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete"));
});
}
#[gpui::test]
fn button_props_after_on_ok_keeps_the_callback(cx: &mut TestAppContext) {
let cx = window(cx, size(px(800.), px(600.)));
let confirmed = Rc::new(Cell::new(0));
let counter = confirmed.clone();
cx.update(|window, cx| {
let alert = AlertDialog::new(cx)
.on_ok(move |_, _, _| {
counter.set(counter.get() + 1);
true
})
.button_props(DialogButtonProps::default().ok_text("Delete"));
assert!(alert.base.button_props.ok_handler()(
&ClickEvent::default(),
window,
cx
));
});
assert_eq!(confirmed.get(), 1);
}
#[gpui::test]
fn the_direct_builders_match_button_props(cx: &mut TestAppContext) {
let cx = window(cx, size(px(800.), px(600.)));
cx.update(|_, cx| {
let direct = AlertDialog::new(cx)
.confirm()
.ok_text("Delete")
.ok_variant(ButtonVariant::Danger)
.cancel_text("Keep")
.cancel_variant(ButtonVariant::Ghost);
let bundled = AlertDialog::new(cx).button_props(
DialogButtonProps::default()
.show_cancel(true)
.ok_text("Delete")
.ok_variant(ButtonVariant::Danger)
.cancel_text("Keep")
.cancel_variant(ButtonVariant::Ghost),
);
for props in [&direct.base.button_props, &bundled.base.button_props] {
assert!(props.is_cancel_shown());
assert_eq!(props.ok_text.as_deref(), Some("Delete"));
assert_eq!(props.ok_variant, Some(ButtonVariant::Danger));
assert_eq!(props.cancel_text.as_deref(), Some("Keep"));
assert_eq!(props.cancel_variant, Some(ButtonVariant::Ghost));
}
});
}
}