ui/components/notification/
announcement_toast.rs1use crate::{ListBulletItem, prelude::*};
2use component::{Component, ComponentScope, example_group, single_example};
3use gpui::{AnyElement, ClickEvent, Hsla, IntoElement, ParentElement, SharedString};
4use smallvec::SmallVec;
5
6fn role(severity: Severity, step: u16) -> Hsla {
7 match severity {
8 Severity::Info => palette::primary(step),
9 Severity::Success => palette::success(step),
10 Severity::Warning => palette::warning(step),
11 Severity::Error => palette::danger(step),
12 }
13}
14
15fn icon_for(severity: Severity) -> IconName {
16 match severity {
17 Severity::Info => IconName::Info,
18 Severity::Success => IconName::CheckCircle,
19 Severity::Warning => IconName::ExclamationTriangle,
20 Severity::Error => IconName::XCircle,
21 }
22}
23
24#[derive(IntoElement, RegisterComponent)]
25pub struct AnnouncementToast {
26 illustration: Option<AnyElement>,
27 severity: Option<Severity>,
28 heading: Option<SharedString>,
29 description: Option<SharedString>,
30 bullet_items: SmallVec<[AnyElement; 6]>,
31 primary_action_label: SharedString,
32 primary_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
33 secondary_action_label: SharedString,
34 secondary_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
35 dismiss_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
36}
37
38impl AnnouncementToast {
39 pub fn new() -> Self {
40 Self {
41 illustration: None,
42 severity: None,
43 heading: None,
44 description: None,
45 bullet_items: SmallVec::new(),
46 primary_action_label: "Try Now".into(),
47 primary_on_click: Box::new(|_, _, _| {}),
48 secondary_action_label: "Learn More".into(),
49 secondary_on_click: Box::new(|_, _, _| {}),
50 dismiss_on_click: Box::new(|_, _, _| {}),
51 }
52 }
53
54 pub fn illustration(mut self, illustration: impl IntoElement) -> Self {
55 self.illustration = Some(illustration.into_any_element());
56 self
57 }
58
59 pub fn severity(mut self, severity: Severity) -> Self {
62 self.severity = Some(severity);
63 self
64 }
65
66 pub fn heading(mut self, heading: impl Into<SharedString>) -> Self {
67 self.heading = Some(heading.into());
68 self
69 }
70
71 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
72 self.description = Some(description.into());
73 self
74 }
75
76 pub fn bullet_item(mut self, item: impl IntoElement) -> Self {
77 self.bullet_items.push(item.into_any_element());
78 self
79 }
80
81 pub fn bullet_items(mut self, items: impl IntoIterator<Item = impl IntoElement>) -> Self {
82 self.bullet_items
83 .extend(items.into_iter().map(IntoElement::into_any_element));
84 self
85 }
86
87 pub fn primary_action_label(mut self, primary_action_label: impl Into<SharedString>) -> Self {
88 self.primary_action_label = primary_action_label.into();
89 self
90 }
91
92 pub fn primary_on_click(
93 mut self,
94 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
95 ) -> Self {
96 self.primary_on_click = Box::new(handler);
97 self
98 }
99
100 pub fn secondary_action_label(
101 mut self,
102 secondary_action_label: impl Into<SharedString>,
103 ) -> Self {
104 self.secondary_action_label = secondary_action_label.into();
105 self
106 }
107
108 pub fn secondary_on_click(
109 mut self,
110 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
111 ) -> Self {
112 self.secondary_on_click = Box::new(handler);
113 self
114 }
115
116 pub fn dismiss_on_click(
117 mut self,
118 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
119 ) -> Self {
120 self.dismiss_on_click = Box::new(handler);
121 self
122 }
123}
124
125impl RenderOnce for AnnouncementToast {
126 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
127 let has_illustration = self.illustration.is_some();
128 let illustration = self.illustration;
129
130 let severity = self.severity;
131
132 v_flex()
133 .id("announcement-toast")
134 .occlude()
135 .relative()
136 .w_full()
137 .max_w(px(384.))
138 .bg(semantic::elevated_surface(cx))
139 .border_1()
140 .border_color(semantic::border(cx))
141 .rounded_lg()
142 .shadow_level(Shadow::Lg)
143 .when_some(illustration, |this, i| this.child(i))
144 .child(
145 v_flex()
146 .p_4()
147 .gap_4()
148 .when(has_illustration, |s| {
149 s.border_t_1().border_color(semantic::border_muted(cx))
150 })
151 .child(
152 v_flex()
153 .min_w_0()
154 .when_some(self.heading, |this, heading| {
155 this.child(
156 h_flex()
157 .gap_2()
158 .when(!has_illustration, |row| {
159 row.when_some(severity, |row, severity| {
160 row.child(
161 Icon::new(icon_for(severity))
162 .size(IconSize::Small)
163 .color(Color::Custom(role(severity, 500))),
164 )
165 })
166 })
167 .child(Headline::new(heading).size(HeadlineSize::Small)),
168 )
169 })
170 .when_some(self.description, |this, description| {
171 this.child(Label::new(description).color(Color::Muted))
172 }),
173 )
174 .when(!self.bullet_items.is_empty(), |this| {
175 this.child(v_flex().min_w_0().gap_1().children(self.bullet_items))
176 })
177 .child(
178 v_flex()
179 .gap_1()
180 .child(
181 Button::new("try-now", self.primary_action_label)
182 .primary()
183 .full_width()
184 .on_click(self.primary_on_click),
185 )
186 .child(
187 Button::new("release-notes", self.secondary_action_label)
188 .style(ButtonStyle::OutlinedGhost)
189 .full_width()
190 .on_click(self.secondary_on_click),
191 ),
192 ),
193 )
194 .child(
195 div().absolute().top_1().right_1().child(
196 IconButton::new("dismiss", IconName::XMark)
197 .icon_size(IconSize::Small)
198 .on_click(self.dismiss_on_click),
199 ),
200 )
201 }
202}
203
204impl Component for AnnouncementToast {
205 fn scope() -> ComponentScope {
206 ComponentScope::Notification
207 }
208
209 fn description() -> Option<&'static str> {
210 Some("A special toast for announcing new and exciting features.")
211 }
212
213 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
214 let examples = vec![
215 single_example(
216 "Basic",
217 div()
218 .w_80()
219 .child(
220 AnnouncementToast::new()
221 .heading("Introducing Parallel Agents")
222 .description(
223 "Run multiple agent threads simultaneously across projects.",
224 )
225 .bullet_item(ListBulletItem::new(
226 "Mix and match the app's agent with any ACP-compatible agent",
227 ))
228 .bullet_item(ListBulletItem::new(
229 "Optional worktree isolation keeps agents from conflicting",
230 ))
231 .bullet_item(ListBulletItem::new(
232 "Updated workspace layout designed for agentic workflows",
233 ))
234 .primary_action_label("Try Now")
235 .secondary_action_label("Learn More"),
236 )
237 .into_any_element(),
238 ),
239 single_example(
240 "With Severity",
241 div()
242 .w_80()
243 .child(
244 AnnouncementToast::new()
245 .severity(Severity::Success)
246 .heading("Changes saved")
247 .description("Your workspace settings were updated successfully.")
248 .primary_action_label("Dismiss")
249 .secondary_action_label("View Details"),
250 )
251 .into_any_element(),
252 ),
253 ];
254
255 Some(
256 v_flex()
257 .gap_6()
258 .child(example_group(examples).vertical())
259 .into_any_element(),
260 )
261 }
262}