use crate::{
AnyElement, Context, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Window,
div, px,
};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ToastPosition {
#[default]
TopRight,
BottomRight,
TopCenter,
}
#[derive(Clone)]
pub struct Toast {
pub title: SharedString,
pub body: Option<SharedString>,
pub duration: Duration,
pub position: ToastPosition,
}
impl Toast {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
body: None,
duration: Duration::from_secs(3),
position: ToastPosition::default(),
}
}
pub fn body(mut self, body: impl Into<SharedString>) -> Self {
self.body = Some(body.into());
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn position(mut self, position: ToastPosition) -> Self {
self.position = position;
self
}
}
struct ToastEntry {
toast: Toast,
}
pub struct ToastStack {
toasts: Vec<ToastEntry>,
position: ToastPosition,
}
impl ToastStack {
pub fn new() -> Self {
Self {
toasts: Vec::new(),
position: ToastPosition::default(),
}
}
pub fn with_position(mut self, position: ToastPosition) -> Self {
self.position = position;
self
}
pub fn push(&mut self, toast: Toast, window: &mut Window, cx: &mut Context<Self>) {
let duration = toast.duration;
let index = self.toasts.len();
let weak_self = cx.entity().downgrade();
let _ = cx.spawn_in(window, async move |_, cx| {
cx.background_executor().timer(duration).await;
if let Some(stack) = weak_self.upgrade() {
let _ = stack.update(cx, |stack, cx| {
if index < stack.toasts.len() {
stack.toasts.remove(index);
cx.notify();
}
});
}
});
self.toasts.push(ToastEntry { toast });
cx.notify();
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.toasts.clear();
cx.notify();
}
pub fn len(&self) -> usize {
self.toasts.len()
}
pub fn is_empty(&self) -> bool {
self.toasts.is_empty()
}
fn offset_for_index(&self, index: usize) -> Pixels {
px(88.0 * index as f32)
}
}
impl Render for ToastStack {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let position = self.position;
let mut container = div().absolute().flex().flex_col().gap_2().p_4();
container = match position {
ToastPosition::TopRight => container.top_0().right_0(),
ToastPosition::BottomRight => container.bottom_0().right_0(),
ToastPosition::TopCenter => container.top_0().left(px(50.0)).ml(-px(100.0)),
};
let toasts: Vec<AnyElement> = self
.toasts
.iter()
.enumerate()
.map(|(index, entry)| {
render_toast(&entry.toast, index, position, self.offset_for_index(index))
})
.collect();
container.children(toasts)
}
}
fn render_toast(
toast: &Toast,
_index: usize,
position: ToastPosition,
offset: Pixels,
) -> AnyElement {
let mut toast_div = div()
.absolute()
.w(px(320.0))
.rounded_lg()
.shadow_md()
.p_4()
.bg(crate::rgb(0x1e1e1e))
.text_color(crate::white())
.child(div().text_sm().child(toast.title.clone()));
if let Some(ref body) = toast.body {
toast_div = toast_div.child(
div()
.mt_1()
.text_xs()
.text_color(crate::hsla(0.0, 0.0, 1.0, 0.7))
.child(body.clone()),
);
}
toast_div = match position {
ToastPosition::TopRight | ToastPosition::TopCenter => toast_div.top(offset),
ToastPosition::BottomRight => toast_div.bottom(offset),
};
toast_div.into_any_element()
}