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}