gpui_core/elements/
toast.rs1#![allow(deprecated)] use std::time::Duration;
4
5use crate::{
6 AnyElement, Context, IntoElement, ParentElement, Render, SharedString, Styled, Timer,
7 WeakEntity, Window, WindowAppearance, div, hsla, px,
8};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12#[deprecated(
13 since = "0.11.0",
14 note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
15)]
16pub enum ToastPosition {
17 #[default]
19 TopRight,
20 BottomRight,
22 TopCenter,
24}
25
26#[derive(Clone)]
28#[deprecated(
29 since = "0.11.0",
30 note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
31)]
32pub struct Toast {
33 title: SharedString,
34 body: Option<SharedString>,
35 duration: Duration,
36 position: ToastPosition,
37}
38
39impl Toast {
40 pub fn new(title: impl Into<SharedString>) -> Self {
42 Self {
43 title: title.into(),
44 body: None,
45 duration: Duration::from_secs(3),
46 position: ToastPosition::default(),
47 }
48 }
49
50 pub fn body(mut self, body: impl Into<SharedString>) -> Self {
52 self.body = Some(body.into());
53 self
54 }
55
56 pub fn duration(mut self, duration: Duration) -> Self {
58 self.duration = duration;
59 self
60 }
61
62 pub fn position(mut self, position: ToastPosition) -> Self {
64 self.position = position;
65 self
66 }
67}
68
69struct ToastEntry {
70 id: u64,
71 toast: Toast,
72}
73
74#[deprecated(
79 since = "0.11.0",
80 note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
81)]
82pub struct ToastStack {
83 toasts: Vec<ToastEntry>,
84 next_toast_id: u64,
85 position: ToastPosition,
86}
87
88impl ToastStack {
89 pub fn new() -> Self {
91 Self {
92 toasts: Vec::new(),
93 next_toast_id: 0,
94 position: ToastPosition::default(),
95 }
96 }
97
98 pub fn with_position(mut self, position: ToastPosition) -> Self {
100 self.position = position;
101 self
102 }
103
104 pub fn push(&mut self, toast: Toast, window: &Window, cx: &mut Context<Self>) {
106 let duration = toast.duration;
107 let id = self.next_toast_id;
108 self.next_toast_id += 1;
109 self.toasts.push(ToastEntry { id, toast });
110 cx.notify();
111
112 cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
113 Timer::after(duration).await;
114 this.update(cx, |stack, cx| {
115 let len = stack.toasts.len();
116 stack.toasts.retain(|entry| entry.id != id);
117 if stack.toasts.len() != len {
118 cx.notify();
119 }
120 })
121 .ok();
122 })
123 .detach();
124 }
125
126 pub fn clear(&mut self, cx: &mut Context<Self>) {
128 self.toasts.clear();
129 cx.notify();
130 }
131
132 fn is_dark_appearance(window: &Window) -> bool {
133 matches!(
134 window.appearance(),
135 WindowAppearance::Dark | WindowAppearance::VibrantDark
136 )
137 }
138}
139
140impl Render for ToastStack {
141 fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
142 let is_dark = Self::is_dark_appearance(window);
143 let position = self.position;
144
145 let mut container = div().flex().flex_col().gap_2().p_4().max_w(px(360.0));
146
147 match position {
148 ToastPosition::TopRight => {
149 container = container.absolute().top_0().right_0();
150 }
151 ToastPosition::BottomRight => {
152 container = container.absolute().bottom_0().right_0();
153 }
154 ToastPosition::TopCenter => {
155 container = container.absolute().top_0().left_auto().right_auto();
156 }
157 }
158
159 let children: Vec<AnyElement> = self
160 .toasts
161 .iter()
162 .map(|entry| render_toast_item(&entry.toast, is_dark))
163 .collect();
164
165 for child in children {
166 container = container.child(child);
167 }
168
169 container
170 }
171}
172
173fn render_toast_item(toast: &Toast, is_dark: bool) -> AnyElement {
174 let bg_color = if is_dark {
175 hsla(0.0, 0.0, 0.1, 0.92)
176 } else {
177 hsla(0.0, 0.0, 0.0, 0.85)
178 };
179
180 let text_color = if is_dark {
181 hsla(0.0, 0.0, 0.95, 1.0)
182 } else {
183 hsla(0.0, 0.0, 1.0, 1.0)
184 };
185
186 let secondary_text_color = if is_dark {
187 hsla(0.0, 0.0, 0.7, 1.0)
188 } else {
189 hsla(0.0, 0.0, 0.85, 1.0)
190 };
191
192 let title = toast.title.clone();
193 let body = toast.body.clone();
194
195 let mut toast_div = div()
196 .flex()
197 .flex_col()
198 .gap_1()
199 .py(px(12.0))
200 .px(px(16.0))
201 .rounded(px(8.0))
202 .bg(bg_color)
203 .shadow_lg()
204 .max_w(px(320.0))
205 .min_w(px(200.0))
206 .text_color(text_color)
207 .text_sm()
208 .child(div().font_weight(crate::FontWeight::SEMIBOLD).child(title));
209
210 if let Some(body_text) = body {
211 toast_div = toast_div.child(
212 div()
213 .text_xs()
214 .text_color(secondary_text_color)
215 .child(body_text),
216 );
217 }
218
219 toast_div.into_any_element()
220}