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 .relative()
105 .w_full()
106 .elevation_3(cx)
107 .when_some(illustration, |this, i| this.child(i))
108 .child(
109 v_flex()
110 .p_4()
111 .gap_4()
112 .when(has_illustration, |s| {
113 s.border_t_1()
114 .border_color(cx.theme().colors().border_variant)
115 })
116 .child(
117 v_flex()
118 .min_w_0()
119 .when_some(self.heading, |this, heading| {
120 this.child(Headline::new(heading).size(HeadlineSize::Small))
121 })
122 .when_some(self.description, |this, description| {
123 this.child(Label::new(description).color(Color::Muted))
124 }),
125 )
126 .when(!self.bullet_items.is_empty(), |this| {
127 this.child(v_flex().min_w_0().gap_1().children(self.bullet_items))
128 })
129 .child(
130 v_flex()
131 .gap_1()
132 .child(
133 Button::new("try-now", self.primary_action_label)
134 .style(ButtonStyle::Tinted(crate::TintColor::Accent))
135 .full_width()
136 .on_click(self.primary_on_click),
137 )
138 .child(
139 Button::new("release-notes", self.secondary_action_label)
140 .style(ButtonStyle::OutlinedGhost)
141 .full_width()
142 .on_click(self.secondary_on_click),
143 ),
144 ),
145 )
146 .child(
147 div().absolute().top_1().right_1().child(
148 IconButton::new("dismiss", IconName::Close)
149 .icon_size(IconSize::Small)
150 .on_click(self.dismiss_on_click),
151 ),
152 )
153 }
154}
155
156impl Component for AnnouncementToast {
157 fn scope() -> ComponentScope {
158 ComponentScope::Notification
159 }
160
161 fn description() -> Option<&'static str> {
162 Some("A special toast for announcing new and exciting features.")
163 }
164
165 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
166 let examples = vec![single_example(
167 "Basic",
168 div()
169 .w_80()
170 .child(
171 AnnouncementToast::new()
172 .heading("Introducing Parallel Agents")
173 .description("Run multiple agent threads simultaneously across projects.")
174 .bullet_item(ListBulletItem::new(
175 "Mix and match Zed's agent with any ACP-compatible agent",
176 ))
177 .bullet_item(ListBulletItem::new(
178 "Optional worktree isolation keeps agents from conflicting",
179 ))
180 .bullet_item(ListBulletItem::new(
181 "Updated workspace layout designed for agentic workflows",
182 ))
183 .primary_action_label("Try Now")
184 .secondary_action_label("Learn More"),
185 )
186 .into_any_element(),
187 )];
188
189 Some(
190 v_flex()
191 .gap_6()
192 .child(example_group(examples).vertical())
193 .into_any_element(),
194 )
195 }
196}