gpui_core/elements/
toast.rs1use 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 id: u64,
61 toast: Toast,
62}
63
64pub struct ToastStack {
69 toasts: Vec<ToastEntry>,
70 next_toast_id: u64,
71 position: ToastPosition,
72}
73
74impl ToastStack {
75 pub fn new() -> Self {
77 Self {
78 toasts: Vec::new(),
79 next_toast_id: 0,
80 position: ToastPosition::default(),
81 }
82 }
83
84 pub fn with_position(mut self, position: ToastPosition) -> Self {
86 self.position = position;
87 self
88 }
89
90 pub fn push(&mut self, toast: Toast, window: &Window, cx: &mut Context<Self>) {
92 let duration = toast.duration;
93 let id = self.next_toast_id;
94 self.next_toast_id += 1;
95 self.toasts.push(ToastEntry { id, toast });
96 cx.notify();
97
98 cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
99 Timer::after(duration).await;
100 this.update(cx, |stack, cx| {
101 let len = stack.toasts.len();
102 stack.toasts.retain(|entry| entry.id != id);
103 if stack.toasts.len() != len {
104 cx.notify();
105 }
106 })
107 .ok();
108 })
109 .detach();
110 }
111
112 pub fn clear(&mut self, cx: &mut Context<Self>) {
114 self.toasts.clear();
115 cx.notify();
116 }
117
118 fn is_dark_appearance(window: &Window) -> bool {
119 matches!(
120 window.appearance(),
121 WindowAppearance::Dark | WindowAppearance::VibrantDark
122 )
123 }
124}
125
126impl Render for ToastStack {
127 fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
128 let is_dark = Self::is_dark_appearance(window);
129 let position = self.position;
130
131 let mut container = div().flex().flex_col().gap_2().p_4().max_w(px(360.0));
132
133 match position {
134 ToastPosition::TopRight => {
135 container = container.absolute().top_0().right_0();
136 }
137 ToastPosition::BottomRight => {
138 container = container.absolute().bottom_0().right_0();
139 }
140 ToastPosition::TopCenter => {
141 container = container.absolute().top_0().left_auto().right_auto();
142 }
143 }
144
145 let children: Vec<AnyElement> = self
146 .toasts
147 .iter()
148 .map(|entry| render_toast_item(&entry.toast, is_dark))
149 .collect();
150
151 for child in children {
152 container = container.child(child);
153 }
154
155 container
156 }
157}
158
159fn render_toast_item(toast: &Toast, is_dark: bool) -> AnyElement {
160 let bg_color = if is_dark {
161 hsla(0.0, 0.0, 0.1, 0.92)
162 } else {
163 hsla(0.0, 0.0, 0.0, 0.85)
164 };
165
166 let text_color = if is_dark {
167 hsla(0.0, 0.0, 0.95, 1.0)
168 } else {
169 hsla(0.0, 0.0, 1.0, 1.0)
170 };
171
172 let secondary_text_color = if is_dark {
173 hsla(0.0, 0.0, 0.7, 1.0)
174 } else {
175 hsla(0.0, 0.0, 0.85, 1.0)
176 };
177
178 let title = toast.title.clone();
179 let body = toast.body.clone();
180
181 let mut toast_div = div()
182 .flex()
183 .flex_col()
184 .gap_1()
185 .py(px(12.0))
186 .px(px(16.0))
187 .rounded(px(8.0))
188 .bg(bg_color)
189 .shadow_lg()
190 .max_w(px(320.0))
191 .min_w(px(200.0))
192 .text_color(text_color)
193 .text_sm()
194 .child(div().font_weight(crate::FontWeight::SEMIBOLD).child(title));
195
196 if let Some(body_text) = body {
197 toast_div = toast_div.child(
198 div()
199 .text_xs()
200 .text_color(secondary_text_color)
201 .child(body_text),
202 );
203 }
204
205 toast_div.into_any_element()
206}