use std::sync::Arc;
use blinc_animation::{AnimationPreset, MultiKeyframeAnimation};
use blinc_core::Color;
use blinc_layout::motion::motion_derived;
use blinc_layout::overlay_state::get_overlay_manager;
use blinc_layout::prelude::*;
use blinc_layout::widgets::overlay::{OverlayHandle, OverlayManagerExt};
use blinc_layout::InstanceKey;
use blinc_theme::{ColorToken, RadiusToken, SpacingToken, ThemeState};
use super::button::{button, ButtonVariant};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DialogSize {
Small,
#[default]
Medium,
Large,
Full,
}
impl DialogSize {
pub fn max_width(&self) -> f32 {
match self {
DialogSize::Small => 400.0,
DialogSize::Medium => 500.0,
DialogSize::Large => 600.0,
DialogSize::Full => 800.0,
}
}
}
pub struct DialogBuilder {
title: Option<String>,
description: Option<String>,
content: Option<Arc<dyn Fn() -> Div + Send + Sync>>,
footer: Option<Arc<dyn Fn() -> Div + Send + Sync>>,
size: DialogSize,
confirm_text: String,
cancel_text: String,
on_confirm: Option<Arc<dyn Fn() + Send + Sync>>,
on_cancel: Option<Arc<dyn Fn() + Send + Sync>>,
confirm_destructive: bool,
show_cancel: bool,
enter_animation: Option<MultiKeyframeAnimation>,
exit_animation: Option<MultiKeyframeAnimation>,
classes: Vec<String>,
user_id: Option<String>,
key: InstanceKey,
}
impl DialogBuilder {
#[track_caller]
pub fn new() -> Self {
Self {
title: None,
description: None,
content: None,
footer: None,
size: DialogSize::Medium,
confirm_text: "Confirm".to_string(),
cancel_text: "Cancel".to_string(),
on_confirm: None,
on_cancel: None,
confirm_destructive: false,
show_cancel: true,
enter_animation: None,
exit_animation: None,
classes: Vec::new(),
user_id: None,
key: InstanceKey::new("dialog"),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn content<F>(mut self, content: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Arc::new(content));
self
}
pub fn footer<F>(mut self, footer: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.footer = Some(Arc::new(footer));
self
}
pub fn size(mut self, size: DialogSize) -> Self {
self.size = size;
self
}
pub fn confirm_text(mut self, text: impl Into<String>) -> Self {
self.confirm_text = text.into();
self
}
pub fn cancel_text(mut self, text: impl Into<String>) -> Self {
self.cancel_text = text.into();
self
}
pub fn confirm_destructive(mut self, destructive: bool) -> Self {
self.confirm_destructive = destructive;
self
}
pub fn hide_cancel(mut self) -> Self {
self.show_cancel = false;
self
}
pub fn on_confirm<F>(mut self, callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_confirm = Some(Arc::new(callback));
self
}
pub fn on_cancel<F>(mut self, callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_cancel = Some(Arc::new(callback));
self
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.classes.push(name.into());
self
}
pub fn id(mut self, id: &str) -> Self {
self.user_id = Some(id.to_string());
self
}
pub fn enter_animation(mut self, animation: MultiKeyframeAnimation) -> Self {
self.enter_animation = Some(animation);
self
}
pub fn exit_animation(mut self, animation: MultiKeyframeAnimation) -> Self {
self.exit_animation = Some(animation);
self
}
pub fn show(self) -> OverlayHandle {
let theme = ThemeState::get();
let bg = theme.color(ColorToken::Surface);
let border = theme.color(ColorToken::Border);
let text_primary = theme.color(ColorToken::TextPrimary);
let text_secondary = theme.color(ColorToken::TextSecondary);
let radius = theme.radius(RadiusToken::Lg);
let spacing = theme.spacing_value(SpacingToken::Space4);
let title = self.title;
let description = self.description;
let content = self.content;
let footer = self.footer;
let max_width = self.size.max_width();
let confirm_text = self.confirm_text;
let cancel_text = self.cancel_text;
let on_confirm = self.on_confirm;
let on_cancel = self.on_cancel;
let confirm_destructive = self.confirm_destructive;
let show_cancel = self.show_cancel;
let classes = self.classes;
let user_id = self.user_id;
let enter_animation = self
.enter_animation
.unwrap_or_else(|| AnimationPreset::grow_in(200));
let exit_animation = self
.exit_animation
.unwrap_or_else(|| AnimationPreset::grow_out(150));
let mgr = get_overlay_manager();
let motion_key_str = format!("dialog_{}", self.key.get());
let motion_key_with_child = format!("{}:child:0", motion_key_str);
mgr.modal()
.dismiss_on_escape(true)
.motion_key(&motion_key_with_child)
.content(move || {
let mut content_div = build_dialog_content(
&title,
&description,
&content,
&footer,
max_width,
bg,
border,
text_primary,
text_secondary,
radius,
spacing,
&confirm_text,
&cancel_text,
&on_confirm,
&on_cancel,
confirm_destructive,
show_cancel,
&enter_animation,
&exit_animation,
&motion_key_str,
);
for c in &classes {
content_div = content_div.class(c);
}
if let Some(ref id) = user_id {
content_div = content_div.id(id);
}
content_div
})
.show()
}
}
impl Default for DialogBuilder {
fn default() -> Self {
Self::new()
}
}
#[track_caller]
pub fn dialog() -> DialogBuilder {
DialogBuilder::new()
}
pub struct AlertDialogBuilder {
inner: DialogBuilder,
}
impl AlertDialogBuilder {
pub fn new() -> Self {
Self {
inner: DialogBuilder::new().hide_cancel(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.inner = self.inner.title(title);
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.inner = self.inner.description(description);
self
}
pub fn confirm_text(mut self, text: impl Into<String>) -> Self {
self.inner = self.inner.confirm_text(text);
self
}
pub fn on_confirm<F>(mut self, callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.inner = self.inner.on_confirm(callback);
self
}
pub fn size(mut self, size: DialogSize) -> Self {
self.inner = self.inner.size(size);
self
}
pub fn show(self) -> OverlayHandle {
self.inner.show()
}
}
impl Default for AlertDialogBuilder {
fn default() -> Self {
Self::new()
}
}
#[track_caller]
pub fn alert_dialog() -> AlertDialogBuilder {
AlertDialogBuilder::new()
}
#[allow(clippy::too_many_arguments)]
fn build_dialog_content(
title: &Option<String>,
description: &Option<String>,
content: &Option<Arc<dyn Fn() -> Div + Send + Sync>>,
footer: &Option<Arc<dyn Fn() -> Div + Send + Sync>>,
max_width: f32,
bg: Color,
border: Color,
text_primary: Color,
text_secondary: Color,
radius: f32,
_spacing: f32,
confirm_text: &str,
_cancel_text: &str,
on_confirm: &Option<Arc<dyn Fn() + Send + Sync>>,
on_cancel: &Option<Arc<dyn Fn() + Send + Sync>>,
confirm_destructive: bool,
show_cancel: bool,
enter_animation: &MultiKeyframeAnimation,
exit_animation: &MultiKeyframeAnimation,
motion_key: &str,
) -> Div {
let theme = ThemeState::get();
let mut inner_content = div().w_full().flex_col();
if title.is_some() || description.is_some() {
let mut header = div().w_full().flex_col().gap_2();
if let Some(ref title_text) = title {
header = header.child(h3(title_text).color(text_primary));
}
if let Some(ref desc_text) = description {
header = header.child(
text(desc_text)
.size(theme.typography().text_sm)
.color(text_secondary),
);
}
inner_content = inner_content.child(header);
}
if let Some(ref content_fn) = content {
inner_content = inner_content.child(
div()
.w_full()
.mt(theme.spacing().space_2)
.child(content_fn()),
); }
let footer_content = if let Some(ref footer_fn) = footer {
footer_fn()
} else {
let mut footer_div = div().w_full().flex_row().gap_2().justify_end();
if show_cancel {
let on_cancel = on_cancel.clone();
footer_div =
footer_div.child(button("Cancel").variant(ButtonVariant::Outline).on_click(
move |_| {
if let Some(ref cb) = on_cancel {
cb();
}
get_overlay_manager().close_top();
},
));
}
let on_confirm = on_confirm.clone();
let confirm_text = confirm_text.to_string();
footer_div = footer_div.child(
button(confirm_text)
.variant(if confirm_destructive {
ButtonVariant::Destructive
} else {
ButtonVariant::Primary
})
.on_click(move |_| {
if let Some(ref cb) = on_confirm {
cb();
}
get_overlay_manager().close_top();
}),
);
footer_div
};
inner_content = inner_content.child(
div()
.w_full()
.mt(theme.spacing().space_2)
.child(footer_content),
);
let inner_motion_key = format!("{}_inner", motion_key);
let animated_inner = motion_derived(&inner_motion_key)
.enter_animation(AnimationPreset::fade_in(150))
.exit_animation(AnimationPreset::fade_out(100))
.child(inner_content);
let dialog = div()
.class("cn-dialog")
.min_w(300.0)
.max_w(max_width)
.bg(bg)
.border(1.0, border)
.rounded(radius)
.shadow_xl()
.flex_col()
.child(animated_inner);
div().child(
motion_derived(motion_key)
.enter_animation(enter_animation.clone())
.exit_animation(exit_animation.clone())
.child(dialog),
)
}
#[doc(hidden)]
pub type Dialog = DialogBuilder;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dialog_builder() {
let builder = dialog()
.title("Test")
.description("Description")
.confirm_text("OK");
assert_eq!(builder.title, Some("Test".to_string()));
assert_eq!(builder.description, Some("Description".to_string()));
assert_eq!(builder.confirm_text, "OK");
}
#[test]
fn test_alert_dialog_builder() {
let builder = alert_dialog().title("Alert").confirm_text("Got it");
assert_eq!(builder.inner.title, Some("Alert".to_string()));
assert_eq!(builder.inner.confirm_text, "Got it");
assert!(!builder.inner.show_cancel);
}
#[test]
fn test_dialog_sizes() {
assert_eq!(DialogSize::Small.max_width(), 400.0);
assert_eq!(DialogSize::Medium.max_width(), 500.0);
assert_eq!(DialogSize::Large.max_width(), 600.0);
assert_eq!(DialogSize::Full.max_width(), 800.0);
}
}