1use std::time::Duration;
10
11use gpui::prelude::*;
12use gpui::{deferred, div, px, Context, FontWeight, IntoElement, SharedString, Window};
13
14use crate::devtools::ProbedAny;
15use crate::theme::{theme, ColorName, Size};
16
17struct Toast {
18 id: usize,
19 title: Option<SharedString>,
20 message: SharedString,
21 color: ColorName,
22}
23
24pub struct ToastStack {
27 toasts: Vec<Toast>,
28 next_id: usize,
29 duration: Option<Duration>,
30}
31
32impl ToastStack {
33 pub fn new() -> Self {
34 ToastStack {
35 toasts: Vec::new(),
36 next_id: 0,
37 duration: Some(Duration::from_secs(4)),
38 }
39 }
40
41 pub fn duration(mut self, duration: Option<Duration>) -> Self {
45 self.set_duration(duration);
46 self
47 }
48
49 pub fn set_duration(&mut self, duration: Option<Duration>) {
52 self.duration = duration;
53 }
54
55 pub fn push(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) -> usize {
59 self.push_toast(None, message.into(), ColorName::Blue, cx)
60 }
61
62 pub fn push_titled(
64 &mut self,
65 title: impl Into<SharedString>,
66 message: impl Into<SharedString>,
67 color: ColorName,
68 cx: &mut Context<Self>,
69 ) -> usize {
70 self.push_toast(Some(title.into()), message.into(), color, cx)
71 }
72
73 fn push_toast(
74 &mut self,
75 title: Option<SharedString>,
76 message: SharedString,
77 color: ColorName,
78 cx: &mut Context<Self>,
79 ) -> usize {
80 let id = self.next_id;
81 self.next_id += 1;
82 self.toasts.push(Toast {
83 id,
84 title,
85 message,
86 color,
87 });
88 cx.notify();
89
90 if let Some(delay) = self.duration {
91 cx.spawn(async move |this, cx| {
94 cx.background_executor().timer(delay).await;
95 this.update(cx, |this, cx| this.remove(id, cx)).ok();
96 })
97 .detach();
98 }
99
100 id
101 }
102
103 pub fn remove(&mut self, id: usize, cx: &mut Context<Self>) {
105 self.toasts.retain(|t| t.id != id);
106 cx.notify();
107 }
108
109 pub fn clear(&mut self, cx: &mut Context<Self>) {
110 self.toasts.clear();
111 cx.notify();
112 }
113
114 pub fn len(&self) -> usize {
115 self.toasts.len()
116 }
117
118 pub fn is_empty(&self) -> bool {
119 self.toasts.is_empty()
120 }
121}
122
123impl Default for ToastStack {
124 fn default() -> Self {
125 ToastStack::new()
126 }
127}
128
129impl Render for ToastStack {
130 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
131 let mut root = div();
132 if self.toasts.is_empty() {
133 return root.into_any_element();
134 }
135
136 let t = theme(cx);
137 let surface = t.surface().hsla();
138 let border = t.border().hsla();
139 let text = t.text().hsla();
140 let dimmed = t.dimmed().hsla();
141 let radius = t.radius(Size::Md);
142 let font_sm = t.font_size(Size::Sm);
143
144 let mut stack = div()
145 .absolute()
146 .top(px(16.0))
147 .right(px(16.0))
148 .flex()
149 .flex_col()
150 .gap(px(10.0));
151
152 for toast in &self.toasts {
153 let id = toast.id;
154 let accent = t.color(toast.color, t.primary_shade()).hsla();
155
156 let mut content = div().flex().flex_col().gap(px(2.0)).flex_1();
157 if let Some(title) = toast.title.clone() {
158 content = content.child(
159 div()
160 .font_weight(FontWeight::BOLD)
161 .text_size(px(font_sm))
162 .text_color(text)
163 .child(title),
164 );
165 }
166 content = content.child(
167 div()
168 .text_size(px(font_sm))
169 .text_color(dimmed)
170 .child(toast.message.clone()),
171 );
172
173 let card = div()
174 .flex()
175 .items_start()
176 .gap(px(12.0))
177 .w(px(320.0))
178 .p(px(t.spacing(Size::Md)))
179 .rounded(px(radius))
180 .bg(surface)
181 .border_1()
182 .border_color(border)
183 .shadow_md()
184 .child(div().w(px(4.0)).h(px(38.0)).rounded(px(4.0)).bg(accent))
185 .child(content)
186 .child(
187 div()
188 .id(("guise-toast-close", id))
189 .text_color(dimmed)
190 .hover(move |s| s.text_color(text))
191 .child(SharedString::new_static("\u{00d7}"))
192 .on_click(cx.listener(move |this, _ev, _window, cx| this.remove(id, cx))),
193 );
194
195 stack = stack.child(card);
196 }
197
198 root = root.child(deferred(stack));
199 root.probe_any("ToastStack").into_any_element()
200 }
201}