Skip to main content

ui/components/notification/
announcement_toast.rs

1use 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    /// Sets a role-colored severity icon shown next to the heading (ignored when
60    /// an [`Self::illustration`] is set).
61    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}