1use std::time::Duration;
2
3use crate::{
4 AnyElement, Context, IntoElement, ParentElement, Render, SharedString, Styled, Timer,
5 WeakEntity, Window, WindowAppearance, div, hsla, px,
6};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub enum ToastPosition {
11 #[default]
13 TopRight,
14 BottomRight,
16 TopCenter,
18}
19
20#[derive(Clone)]
22pub struct Toast {
23 title: SharedString,
24 body: Option<SharedString>,
25 duration: Duration,
26 position: ToastPosition,
27}
28
29impl Toast {
30 pub fn new(title: impl Into<SharedString>) -> Self {
32 Self {
33 title: title.into(),
34 body: None,
35 duration: Duration::from_secs(3),
36 position: ToastPosition::default(),
37 }
38 }
39
40 pub fn body(mut self, body: impl Into<SharedString>) -> Self {
42 self.body = Some(body.into());
43 self
44 }
45
46 pub fn duration(mut self, duration: Duration) -> Self {
48 self.duration = duration;
49 self
50 }
51
52 pub fn position(mut self, position: ToastPosition) -> Self {
54 self.position = position;
55 self
56 }
57}
58
59struct ToastEntry {
60 toast: Toast,
61}
62
63pub struct ToastStack {
68 toasts: Vec<ToastEntry>,
69 position: ToastPosition,
70}
71
72impl ToastStack {
73 pub fn new() -> Self {
75 Self {
76 toasts: Vec::new(),
77 position: ToastPosition::default(),
78 }
79 }
80
81 pub fn with_position(mut self, position: ToastPosition) -> Self {
83 self.position = position;
84 self
85 }
86
87 pub fn push(&mut self, toast: Toast, window: &Window, cx: &mut Context<Self>) {
89 let duration = toast.duration;
90 self.toasts.push(ToastEntry { toast });
91 cx.notify();
92
93 let index = self.toasts.len() - 1;
94 cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
95 Timer::after(duration).await;
96 this.update(cx, |stack, cx| {
97 if index < stack.toasts.len() {
98 stack.toasts.remove(index);
99 cx.notify();
100 }
101 })
102 .ok();
103 })
104 .detach();
105 }
106
107 pub fn clear(&mut self, cx: &mut Context<Self>) {
109 self.toasts.clear();
110 cx.notify();
111 }
112
113 fn is_dark_appearance(window: &Window) -> bool {
114 matches!(
115 window.appearance(),
116 WindowAppearance::Dark | WindowAppearance::VibrantDark
117 )
118 }
119}
120
121impl Render for ToastStack {
122 fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
123 let is_dark = Self::is_dark_appearance(window);
124 let position = self.position;
125
126 let mut container = div().flex().flex_col().gap_2().p_4().max_w(px(360.0));
127
128 match position {
129 ToastPosition::TopRight => {
130 container = container.absolute().top_0().right_0();
131 }
132 ToastPosition::BottomRight => {
133 container = container.absolute().bottom_0().right_0();
134 }
135 ToastPosition::TopCenter => {
136 container = container.absolute().top_0().left_auto().right_auto();
137 }
138 }
139
140 let children: Vec<AnyElement> = self
141 .toasts
142 .iter()
143 .map(|entry| render_toast_item(&entry.toast, is_dark))
144 .collect();
145
146 for child in children {
147 container = container.child(child);
148 }
149
150 container
151 }
152}
153
154fn render_toast_item(toast: &Toast, is_dark: bool) -> AnyElement {
155 let bg_color = if is_dark {
156 hsla(0.0, 0.0, 0.1, 0.92)
157 } else {
158 hsla(0.0, 0.0, 0.0, 0.85)
159 };
160
161 let text_color = if is_dark {
162 hsla(0.0, 0.0, 0.95, 1.0)
163 } else {
164 hsla(0.0, 0.0, 1.0, 1.0)
165 };
166
167 let secondary_text_color = if is_dark {
168 hsla(0.0, 0.0, 0.7, 1.0)
169 } else {
170 hsla(0.0, 0.0, 0.85, 1.0)
171 };
172
173 let title = toast.title.clone();
174 let body = toast.body.clone();
175
176 let mut toast_div = div()
177 .flex()
178 .flex_col()
179 .gap_1()
180 .py(px(12.0))
181 .px(px(16.0))
182 .rounded(px(8.0))
183 .bg(bg_color)
184 .shadow_lg()
185 .max_w(px(320.0))
186 .min_w(px(200.0))
187 .text_color(text_color)
188 .text_sm()
189 .child(div().font_weight(crate::FontWeight::SEMIBOLD).child(title));
190
191 if let Some(body_text) = body {
192 toast_div = toast_div.child(
193 div()
194 .text_xs()
195 .text_color(secondary_text_color)
196 .child(body_text),
197 );
198 }
199
200 toast_div.into_any_element()
201}