announcement_toast.rs

  1use crate::{ListBulletItem, Vector, VectorName, prelude::*};
  2use component::{Component, ComponentScope, example_group, single_example};
  3use gpui::{
  4    AnyElement, ClickEvent, IntoElement, ParentElement, SharedString, linear_color_stop,
  5    linear_gradient,
  6};
  7use smallvec::SmallVec;
  8
  9#[derive(IntoElement, RegisterComponent)]
 10pub struct AnnouncementToast {
 11    illustration: Option<AnyElement>,
 12    heading: Option<SharedString>,
 13    description: Option<SharedString>,
 14    bullet_items: SmallVec<[AnyElement; 6]>,
 15    primary_action_label: SharedString,
 16    primary_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
 17    secondary_action_label: SharedString,
 18    secondary_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
 19    dismiss_on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
 20}
 21
 22impl AnnouncementToast {
 23    pub fn new() -> Self {
 24        Self {
 25            illustration: None,
 26            heading: None,
 27            description: None,
 28            bullet_items: SmallVec::new(),
 29            primary_action_label: "Try Now".into(),
 30            primary_on_click: Box::new(|_, _, _| {}),
 31            secondary_action_label: "Learn More".into(),
 32            secondary_on_click: Box::new(|_, _, _| {}),
 33            dismiss_on_click: Box::new(|_, _, _| {}),
 34        }
 35    }
 36
 37    pub fn illustration(mut self, illustration: impl IntoElement) -> Self {
 38        self.illustration = Some(illustration.into_any_element());
 39        self
 40    }
 41
 42    pub fn heading(mut self, heading: impl Into<SharedString>) -> Self {
 43        self.heading = Some(heading.into());
 44        self
 45    }
 46
 47    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
 48        self.description = Some(description.into());
 49        self
 50    }
 51
 52    pub fn bullet_item(mut self, item: impl IntoElement) -> Self {
 53        self.bullet_items.push(item.into_any_element());
 54        self
 55    }
 56
 57    pub fn bullet_items(mut self, items: impl IntoIterator<Item = impl IntoElement>) -> Self {
 58        self.bullet_items
 59            .extend(items.into_iter().map(IntoElement::into_any_element));
 60        self
 61    }
 62
 63    pub fn primary_action_label(mut self, primary_action_label: impl Into<SharedString>) -> Self {
 64        self.primary_action_label = primary_action_label.into();
 65        self
 66    }
 67
 68    pub fn primary_on_click(
 69        mut self,
 70        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 71    ) -> Self {
 72        self.primary_on_click = Box::new(handler);
 73        self
 74    }
 75
 76    pub fn secondary_action_label(
 77        mut self,
 78        secondary_action_label: impl Into<SharedString>,
 79    ) -> Self {
 80        self.secondary_action_label = secondary_action_label.into();
 81        self
 82    }
 83
 84    pub fn secondary_on_click(
 85        mut self,
 86        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 87    ) -> Self {
 88        self.secondary_on_click = Box::new(handler);
 89        self
 90    }
 91
 92    pub fn dismiss_on_click(
 93        mut self,
 94        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 95    ) -> Self {
 96        self.dismiss_on_click = Box::new(handler);
 97        self
 98    }
 99}
100
101impl RenderOnce for AnnouncementToast {
102    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
103        let has_illustration = self.illustration.is_some();
104        let illustration = self.illustration;
105
106        v_flex()
107            .relative()
108            .w_full()
109            .elevation_3(cx)
110            .when_some(illustration, |this, i| this.child(i))
111            .child(
112                v_flex()
113                    .p_4()
114                    .gap_4()
115                    .when(has_illustration, |s| {
116                        s.border_t_1()
117                            .border_color(cx.theme().colors().border_variant)
118                    })
119                    .child(
120                        v_flex()
121                            .min_w_0()
122                            .when_some(self.heading, |this, heading| {
123                                this.child(Headline::new(heading).size(HeadlineSize::Small))
124                            })
125                            .when_some(self.description, |this, description| {
126                                this.child(Label::new(description).color(Color::Muted))
127                            }),
128                    )
129                    .when(!self.bullet_items.is_empty(), |this| {
130                        this.child(v_flex().min_w_0().gap_1().children(self.bullet_items))
131                    })
132                    .child(
133                        v_flex()
134                            .gap_1()
135                            .child(
136                                Button::new("try-now", self.primary_action_label)
137                                    .style(ButtonStyle::Tinted(crate::TintColor::Accent))
138                                    .full_width()
139                                    .on_click(self.primary_on_click),
140                            )
141                            .child(
142                                Button::new("release-notes", self.secondary_action_label)
143                                    .style(ButtonStyle::OutlinedGhost)
144                                    .full_width()
145                                    .on_click(self.secondary_on_click),
146                            ),
147                    ),
148            )
149            .child(
150                div().absolute().top_1().right_1().child(
151                    IconButton::new("dismiss", IconName::Close)
152                        .icon_size(IconSize::Small)
153                        .on_click(self.dismiss_on_click),
154                ),
155            )
156    }
157}
158
159impl Component for AnnouncementToast {
160    fn scope() -> ComponentScope {
161        ComponentScope::Notification
162    }
163
164    fn description() -> Option<&'static str> {
165        Some("A special toast for announcing new and exciting features.")
166    }
167
168    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
169        let illustration = h_flex()
170            .relative()
171            .h(rems_from_px(126.))
172            .bg(cx.theme().colors().editor_background)
173            .justify_center()
174            .gap_8()
175            .rounded_t_md()
176            .overflow_hidden()
177            .child(
178                div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
179                    Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
180                        .color(Color::Custom(cx.theme().colors().text.opacity(0.02))),
181                ),
182            )
183            .child(div().absolute().inset_0().size_full().bg(linear_gradient(
184                0.,
185                linear_color_stop(
186                    cx.theme().colors().elevated_surface_background.opacity(0.1),
187                    0.9,
188                ),
189                linear_color_stop(
190                    cx.theme().colors().elevated_surface_background.opacity(0.),
191                    0.,
192                ),
193            )))
194            .child(
195                div()
196                    .absolute()
197                    .inset_0()
198                    .size_full()
199                    .bg(gpui::black().opacity(0.15)),
200            )
201            .child(
202                Vector::new(
203                    VectorName::AcpLogoSerif,
204                    rems_from_px(257.),
205                    rems_from_px(47.),
206                )
207                .color(Color::Custom(cx.theme().colors().text.opacity(0.8))),
208            );
209
210        let examples = vec![single_example(
211            "Basic",
212            div()
213                .w_80()
214                .child(
215                    AnnouncementToast::new()
216                        .illustration(illustration)
217                        .heading("Introducing Parallel Agents")
218                        .description("Run multiple agent threads simultaneously across projects.")
219                        .bullet_item(ListBulletItem::new(
220                            "Mix and match Zed's agent with any ACP-compatible agent",
221                        ))
222                        .bullet_item(ListBulletItem::new(
223                            "Optional worktree isolation keeps agents from conflicting",
224                        ))
225                        .bullet_item(ListBulletItem::new(
226                            "Updated workspace layout designed for agentic workflows",
227                        ))
228                        .primary_action_label("Try Now")
229                        .secondary_action_label("Learn More"),
230                )
231                .into_any_element(),
232        )];
233
234        Some(
235            v_flex()
236                .gap_6()
237                .child(example_group(examples).vertical())
238                .into_any_element(),
239        )
240    }
241}