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