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