use std::sync::Arc;
use blinc_core::Color;
use blinc_layout::overlay_state::toast_tray;
use blinc_layout::prelude::*;
use blinc_layout::style;
use blinc_layout::widgets::overlay::Corner;
use blinc_layout::widgets::toast_tray::{ToastBuilder as TrayBuilder, ToastHandle};
use blinc_theme::{ColorToken, RadiusToken, ThemeState};
use super::button::{ButtonSize, ButtonVariant, button};
use crate::reactive_props::clone_reactive;
use blinc_layout::binding::{IntoReactive, Reactive};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ToastVariant {
#[default]
Default,
Success,
Warning,
Destructive,
}
impl ToastVariant {
fn attr(&self) -> &'static str {
match self {
ToastVariant::Default => "default",
ToastVariant::Success => "success",
ToastVariant::Warning => "warning",
ToastVariant::Destructive => "destructive",
}
}
fn icon_svg(&self) -> Option<&'static str> {
match self {
ToastVariant::Default => None,
ToastVariant::Success => Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>"#,
),
ToastVariant::Warning => Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>"#,
),
ToastVariant::Destructive => Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" x2="9" y1="9" y2="15"/><line x1="9" x2="15" y1="9" y2="15"/></svg>"#,
),
}
}
}
pub type ToastBodyFn = Arc<dyn Fn() -> Div + Send + Sync>;
pub struct ToastBuilder {
title: Option<String>,
description: Option<Reactive<String>>,
variant: ToastVariant,
duration_ms: u32,
corner: Corner,
action_label: Option<String>,
action_callback: Option<Arc<dyn Fn() + Send + Sync>>,
show_close: bool,
body: Option<ToastBodyFn>,
custom_content: Option<ToastBodyFn>,
}
impl ToastBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: Some(title.into()),
description: None,
variant: ToastVariant::Default,
duration_ms: 5000, corner: Corner::BottomRight,
action_label: None,
action_callback: None,
show_close: true,
body: None,
custom_content: None,
}
}
pub fn custom<F>(content: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
Self {
title: None,
description: None,
variant: ToastVariant::Default,
duration_ms: 5000,
corner: Corner::BottomRight,
action_label: None,
action_callback: None,
show_close: false,
body: None,
custom_content: Some(Arc::new(content)),
}
}
pub fn description(mut self, description: impl IntoReactive<String>) -> Self {
self.description = Some(description.into_reactive());
self
}
pub fn body<F>(mut self, content: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.body = Some(Arc::new(content));
self
}
pub fn variant(mut self, variant: ToastVariant) -> Self {
self.variant = variant;
self
}
pub fn duration_ms(mut self, ms: u32) -> Self {
self.duration_ms = ms;
self
}
pub fn persistent(mut self) -> Self {
self.duration_ms = 0; self
}
pub fn corner(mut self, corner: Corner) -> Self {
self.corner = corner;
self
}
pub fn action<F>(mut self, label: impl Into<String>, callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.action_label = Some(label.into());
self.action_callback = Some(Arc::new(callback));
self
}
pub fn show_close(mut self, show: bool) -> Self {
self.show_close = show;
self
}
#[track_caller]
pub fn show(self) -> ToastHandle {
let title = self.title;
let description = self.description;
let variant = self.variant;
let icon_svg = variant.icon_svg().map(|s| s.to_string());
let action_label = self.action_label;
let action_callback = self.action_callback;
let show_close = self.show_close;
let corner = self.corner;
let duration_ms = self.duration_ms;
let body = self.body;
let custom_content = self.custom_content;
let next_id = toast_tray()
.lock()
.ok()
.map(|t| t.peek_next_handle_id())
.unwrap_or(0);
let toast_handle = ToastHandle::from_raw(next_id);
let motion_class = match corner {
Corner::TopLeft | Corner::BottomLeft => "cn-toast-motion-left",
Corner::TopRight | Corner::BottomRight => "cn-toast-motion-right",
};
let mut builder = TrayBuilder::new()
.corner(corner)
.motion_class(motion_class)
.dismiss_on_click(false) .content(move || {
build_toast_content(
&title,
&description,
&icon_svg,
variant,
&action_label,
&action_callback,
show_close,
&body,
&custom_content,
toast_handle,
)
});
if duration_ms > 0 {
builder = builder.auto_after_ms(duration_ms as u64);
} else {
builder = builder.auto_after_ms(u64::MAX / 2);
}
builder.show()
}
}
#[allow(clippy::too_many_arguments)]
fn build_toast_content(
title: &Option<String>,
description: &Option<Reactive<String>>,
icon_svg: &Option<String>,
variant: ToastVariant,
action_label: &Option<String>,
action_callback: &Option<Arc<dyn Fn() + Send + Sync>>,
show_close: bool,
body: &Option<ToastBodyFn>,
custom_content: &Option<ToastBodyFn>,
toast_handle: ToastHandle,
) -> Div {
let theme = ThemeState::get();
if let Some(custom_fn) = custom_content {
return custom_fn()
.class("cn-toast")
.attr("data-variant", variant.attr())
.w(360.0);
}
let toast = div()
.class("cn-toast")
.attr("data-variant", variant.attr())
.w(360.0);
let mut inner = div().w_full().flex_row().items_start().gap_3().p_4();
if let Some(svg_str) = icon_svg {
inner = inner.child(
div()
.class("cn-toast-icon")
.flex_shrink_0()
.child(svg(svg_str).size(20.0, 20.0)),
);
}
let mut content = div().flex_1().flex_col().gap_1();
if let Some(title_text) = title {
content = content.child(
text(title_text)
.class("cn-toast-title")
.size(theme.typography().text_sm)
.medium(),
);
}
if let Some(body_fn) = body {
content = content.child(body_fn());
} else if let Some(desc) = description {
content = content.child(
blinc_layout::text::Text::bound(clone_reactive(desc))
.class("cn-toast-description")
.size(theme.typography().text_sm),
);
}
inner = inner.child(content);
if let Some(label) = action_label {
let callback = action_callback.clone();
inner = inner.child(
button(label)
.variant(ButtonVariant::Outline)
.size(ButtonSize::Small)
.on_click(move |_| {
if let Some(ref cb) = callback {
cb();
}
toast_handle.dismiss();
}),
);
}
if show_close {
let close_icon = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" x2="6" y1="6" y2="18"/><line x1="6" x2="18" y1="6" y2="18"/></svg>"#;
inner = inner.child(
div()
.class("cn-toast-close")
.flex_shrink_0()
.w(24.0)
.h(24.0)
.items_center()
.justify_center()
.cursor_pointer()
.on_click(move |_| {
toast_handle.dismiss();
})
.child(svg(close_icon).size(16.0, 16.0)),
);
}
toast.child(inner)
}
pub fn toast(title: impl Into<String>) -> ToastBuilder {
ToastBuilder::new(title)
}
pub fn toast_success(title: impl Into<String>) -> ToastBuilder {
ToastBuilder::new(title).variant(ToastVariant::Success)
}
pub fn toast_warning(title: impl Into<String>) -> ToastBuilder {
ToastBuilder::new(title).variant(ToastVariant::Warning)
}
pub fn toast_error(title: impl Into<String>) -> ToastBuilder {
ToastBuilder::new(title).variant(ToastVariant::Destructive)
}
pub fn toast_custom<F>(content: F) -> ToastBuilder
where
F: Fn() -> Div + Send + Sync + 'static,
{
ToastBuilder::custom(content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_toast_builder() {
let toast = ToastBuilder::new("Test");
assert_eq!(toast.title, Some("Test".to_string()));
assert_eq!(toast.duration_ms, 5000);
}
#[test]
fn test_toast_with_description() {
let toast = ToastBuilder::new("Title").description("Description");
assert!(matches!(toast.description, Some(Reactive::Const(ref d)) if d == "Description"));
}
#[test]
fn test_toast_persistent() {
let toast = ToastBuilder::new("Persistent").persistent();
assert_eq!(toast.duration_ms, 0);
}
#[test]
fn test_toast_variants() {
assert!(ToastVariant::Default.icon_svg().is_none());
assert!(ToastVariant::Success.icon_svg().is_some());
assert!(ToastVariant::Warning.icon_svg().is_some());
assert!(ToastVariant::Destructive.icon_svg().is_some());
}
}