1use gpui::AnyElement;
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum BorderPosition {
7 Top,
8 Bottom,
9}
10
11/// A callout component for displaying important information that requires user attention.
12///
13/// # Usage Example
14///
15/// ```
16/// use ui::{Callout};
17///
18/// Callout::new()
19/// .severity(Severity::Warning)
20/// .icon(IconName::Warning)
21/// .title(Label::new("Be aware of your subscription!"))
22/// .description(Label::new("Your subscription is about to expire. Renew now!"))
23/// .actions_slot(Button::new("renew", "Renew Now"))
24/// ```
25///
26#[derive(IntoElement, RegisterComponent)]
27pub struct Callout {
28 severity: Severity,
29 icon: Option<IconName>,
30 title: Option<SharedString>,
31 description: Option<SharedString>,
32 actions_slot: Option<AnyElement>,
33 dismiss_action: Option<AnyElement>,
34 line_height: Option<Pixels>,
35 border_position: BorderPosition,
36}
37
38impl Callout {
39 /// Creates a new `Callout` component with default styling.
40 pub fn new() -> Self {
41 Self {
42 severity: Severity::Info,
43 icon: None,
44 title: None,
45 description: None,
46 actions_slot: None,
47 dismiss_action: None,
48 line_height: None,
49 border_position: BorderPosition::Top,
50 }
51 }
52
53 /// Sets the severity of the callout.
54 pub fn severity(mut self, severity: Severity) -> Self {
55 self.severity = severity;
56 self
57 }
58
59 /// Sets the icon to display in the callout.
60 pub fn icon(mut self, icon: IconName) -> Self {
61 self.icon = Some(icon);
62 self
63 }
64
65 /// Sets the title of the callout.
66 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
67 self.title = Some(title.into());
68 self
69 }
70
71 /// Sets the description of the callout.
72 /// The description can be single or multi-line text.
73 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
74 self.description = Some(description.into());
75 self
76 }
77
78 /// Sets the primary call-to-action button.
79 pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
80 self.actions_slot = Some(action.into_any_element());
81 self
82 }
83
84 /// Sets an optional dismiss button, which is usually an icon button with a close icon.
85 /// This button is always rendered as the last one to the far right.
86 pub fn dismiss_action(mut self, action: impl IntoElement) -> Self {
87 self.dismiss_action = Some(action.into_any_element());
88 self
89 }
90
91 /// Sets a custom line height for the callout content.
92 pub fn line_height(mut self, line_height: Pixels) -> Self {
93 self.line_height = Some(line_height);
94 self
95 }
96
97 /// Sets the border position in the callout.
98 pub fn border_position(mut self, border_position: BorderPosition) -> Self {
99 self.border_position = border_position;
100 self
101 }
102}
103
104impl RenderOnce for Callout {
105 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
106 let line_height = self.line_height.unwrap_or(window.line_height());
107
108 let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some();
109
110 let (icon, icon_color, bg_color) = match self.severity {
111 Severity::Info => (
112 IconName::Info,
113 Color::Muted,
114 cx.theme().colors().panel_background.opacity(0.),
115 ),
116 Severity::Success => (
117 IconName::Check,
118 Color::Success,
119 cx.theme().status().success.opacity(0.1),
120 ),
121 Severity::Warning => (
122 IconName::Warning,
123 Color::Warning,
124 cx.theme().status().warning_background.opacity(0.2),
125 ),
126 Severity::Error => (
127 IconName::XCircle,
128 Color::Error,
129 cx.theme().status().error.opacity(0.08),
130 ),
131 };
132
133 h_flex()
134 .min_w_0()
135 .w_full()
136 .p_2()
137 .gap_2()
138 .items_start()
139 .map(|this| match self.border_position {
140 BorderPosition::Top => this.border_t_1(),
141 BorderPosition::Bottom => this.border_b_1(),
142 })
143 .border_color(cx.theme().colors().border)
144 .bg(bg_color)
145 .overflow_x_hidden()
146 .when(self.icon.is_some(), |this| {
147 this.child(
148 h_flex()
149 .h(line_height)
150 .justify_center()
151 .child(Icon::new(icon).size(IconSize::Small).color(icon_color)),
152 )
153 })
154 .child(
155 v_flex()
156 .min_w_0()
157 .w_full()
158 .child(
159 h_flex()
160 .min_h(line_height)
161 .w_full()
162 .gap_1()
163 .justify_between()
164 .flex_wrap()
165 .when_some(self.title, |this, title| {
166 this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
167 })
168 .when(has_actions, |this| {
169 this.child(
170 h_flex()
171 .gap_0p5()
172 .when_some(self.actions_slot, |this, action| {
173 this.child(action)
174 })
175 .when_some(self.dismiss_action, |this, action| {
176 this.child(action)
177 }),
178 )
179 }),
180 )
181 .when_some(self.description, |this, description| {
182 this.child(
183 div()
184 .w_full()
185 .flex_1()
186 .text_ui_sm(cx)
187 .text_color(cx.theme().colors().text_muted)
188 .child(description),
189 )
190 }),
191 )
192 }
193}
194
195impl Component for Callout {
196 fn scope() -> ComponentScope {
197 ComponentScope::DataDisplay
198 }
199
200 fn description() -> Option<&'static str> {
201 Some(
202 "Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.",
203 )
204 }
205
206 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
207 let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small);
208 let multiple_actions = || {
209 h_flex()
210 .gap_0p5()
211 .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small))
212 .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small))
213 };
214
215 let basic_examples = vec![
216 single_example(
217 "Simple with Title Only",
218 Callout::new()
219 .icon(IconName::Info)
220 .title("System maintenance scheduled for tonight")
221 .actions_slot(single_action())
222 .into_any_element(),
223 )
224 .width(px(580.)),
225 single_example(
226 "With Title and Description",
227 Callout::new()
228 .icon(IconName::Warning)
229 .title("Your settings contain deprecated values")
230 .description(
231 "We'll backup your current settings and update them to the new format.",
232 )
233 .actions_slot(single_action())
234 .into_any_element(),
235 )
236 .width(px(580.)),
237 single_example(
238 "Error with Multiple Actions",
239 Callout::new()
240 .icon(IconName::Close)
241 .title("Thread reached the token limit")
242 .description("Start a new thread from a summary to continue the conversation.")
243 .actions_slot(multiple_actions())
244 .into_any_element(),
245 )
246 .width(px(580.)),
247 single_example(
248 "Multi-line Description",
249 Callout::new()
250 .icon(IconName::Sparkle)
251 .title("Upgrade to Pro")
252 .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
253 .actions_slot(multiple_actions())
254 .into_any_element(),
255 )
256 .width(px(580.)),
257 ];
258
259 let severity_examples = vec![
260 single_example(
261 "Info",
262 Callout::new()
263 .icon(IconName::Info)
264 .title("System maintenance scheduled for tonight")
265 .actions_slot(single_action())
266 .into_any_element(),
267 ),
268 single_example(
269 "Warning",
270 Callout::new()
271 .severity(Severity::Warning)
272 .icon(IconName::Triangle)
273 .title("System maintenance scheduled for tonight")
274 .actions_slot(single_action())
275 .into_any_element(),
276 ),
277 single_example(
278 "Error",
279 Callout::new()
280 .severity(Severity::Error)
281 .icon(IconName::XCircle)
282 .title("System maintenance scheduled for tonight")
283 .actions_slot(single_action())
284 .into_any_element(),
285 ),
286 single_example(
287 "Success",
288 Callout::new()
289 .severity(Severity::Success)
290 .icon(IconName::Check)
291 .title("System maintenance scheduled for tonight")
292 .actions_slot(single_action())
293 .into_any_element(),
294 ),
295 ];
296
297 Some(
298 v_flex()
299 .gap_4()
300 .child(example_group(basic_examples).vertical())
301 .child(example_group_with_title("Severity", severity_examples).vertical())
302 .into_any_element(),
303 )
304 }
305}