1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ToastVariant {
11 #[default]
13 Info,
14 Success,
16 Warning,
18 Error,
20}
21
22impl ToastVariant {
23 fn icon(&self) -> &'static str {
24 match self {
25 ToastVariant::Info => "ℹ",
26 ToastVariant::Success => "✓",
27 ToastVariant::Warning => "⚠",
28 ToastVariant::Error => "✕",
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum ToastPosition {
36 TopRight,
38 TopLeft,
40 #[default]
42 BottomRight,
43 BottomLeft,
45 TopCenter,
47 BottomCenter,
49}
50
51pub struct Toast {
53 id: ElementId,
54 title: Option<SharedString>,
55 message: SharedString,
56 variant: ToastVariant,
57 closeable: bool,
58 on_close: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
59}
60
61impl Toast {
62 pub fn new(id: impl Into<ElementId>, message: impl Into<SharedString>) -> Self {
64 Self {
65 id: id.into(),
66 title: None,
67 message: message.into(),
68 variant: ToastVariant::default(),
69 closeable: true,
70 on_close: None,
71 }
72 }
73
74 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
76 self.title = Some(title.into());
77 self
78 }
79
80 pub fn variant(mut self, variant: ToastVariant) -> Self {
82 self.variant = variant;
83 self
84 }
85
86 pub fn closeable(mut self, closeable: bool) -> Self {
88 self.closeable = closeable;
89 self
90 }
91
92 pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
94 self.on_close = Some(Box::new(handler));
95 self
96 }
97
98 pub fn build(self) -> Stateful<Div> {
100 let (bg, border, icon_color) = match self.variant {
102 ToastVariant::Info => (rgb(0x2a2a2a), rgb(0x007acc), rgb(0x007acc)),
103 ToastVariant::Success => (rgb(0x1a3a1a), rgb(0x2da44e), rgb(0x2da44e)),
104 ToastVariant::Warning => (rgb(0x3a3a1a), rgb(0xd29922), rgb(0xd29922)),
105 ToastVariant::Error => (rgb(0x3a1a1a), rgb(0xcc3333), rgb(0xcc3333)),
106 };
107 let icon = self.variant.icon();
108
109 let mut toast = div()
110 .id(self.id)
111 .w(px(320.0))
112 .flex()
113 .items_start()
114 .gap_3()
115 .px_4()
116 .py_3()
117 .bg(bg)
118 .border_1()
119 .border_color(border)
120 .rounded_lg()
121 .shadow_lg();
122
123 toast = toast.child(
125 div()
126 .text_lg()
127 .text_color(icon_color)
128 .mt(px(2.0))
129 .child(icon),
130 );
131
132 let mut content = div().flex_1().flex().flex_col().gap_1();
134
135 if let Some(title) = self.title {
136 content = content.child(
137 div()
138 .text_sm()
139 .font_weight(FontWeight::SEMIBOLD)
140 .text_color(rgb(0xffffff))
141 .child(title),
142 );
143 }
144
145 content = content.child(
146 div()
147 .text_sm()
148 .text_color(rgb(0xcccccc))
149 .child(self.message),
150 );
151
152 toast = toast.child(content);
153
154 if self.closeable {
156 if let Some(handler) = self.on_close {
157 let handler_ptr: *const dyn Fn(&mut Window, &mut App) = handler.as_ref();
158 toast = toast.child(
159 div()
160 .id("toast-close")
161 .text_sm()
162 .text_color(rgb(0x888888))
163 .cursor_pointer()
164 .hover(|s| s.text_color(rgb(0xffffff)))
165 .on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
166 (*handler_ptr)(window, cx);
167 })
168 .child("×"),
169 );
170 std::mem::forget(handler);
172 }
173 }
174
175 toast
176 }
177}
178
179impl IntoElement for Toast {
180 type Element = Stateful<Div>;
181
182 fn into_element(self) -> Self::Element {
183 self.build()
184 }
185}
186
187pub struct ToastContainer {
189 position: ToastPosition,
190 toasts: Vec<Toast>,
191}
192
193impl ToastContainer {
194 pub fn new(position: ToastPosition) -> Self {
196 Self {
197 position,
198 toasts: Vec::new(),
199 }
200 }
201
202 pub fn toast(mut self, toast: Toast) -> Self {
204 self.toasts.push(toast);
205 self
206 }
207
208 pub fn toasts(mut self, toasts: impl IntoIterator<Item = Toast>) -> Self {
210 self.toasts.extend(toasts);
211 self
212 }
213
214 pub fn build(self) -> Div {
216 let mut container = div().absolute().flex().flex_col().gap_2().p_4();
217
218 match self.position {
220 ToastPosition::TopRight => {
221 container = container.top_0().right_0();
222 }
223 ToastPosition::TopLeft => {
224 container = container.top_0().left_0();
225 }
226 ToastPosition::BottomRight => {
227 container = container.bottom_0().right_0();
228 }
229 ToastPosition::BottomLeft => {
230 container = container.bottom_0().left_0();
231 }
232 ToastPosition::TopCenter => {
233 container = container.top_0().left_0().right_0().items_center();
234 }
235 ToastPosition::BottomCenter => {
236 container = container.bottom_0().left_0().right_0().items_center();
237 }
238 }
239
240 for toast in self.toasts {
241 container = container.child(toast);
242 }
243
244 container
245 }
246}
247
248impl IntoElement for ToastContainer {
249 type Element = Div;
250
251 fn into_element(self) -> Self::Element {
252 self.build()
253 }
254}