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