announcement_toast.rs

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