1use gpui::{AnyElement, Hsla};
2
3use crate::prelude::*;
4
5/// A callout component for displaying important information that requires user attention.
6///
7/// # Usage Example
8///
9/// ```
10/// use ui::{Callout};
11///
12/// Callout::new()
13/// .icon(Icon::new(IconName::Warning).color(Color::Warning))
14/// .title(Label::new("Be aware of your subscription!"))
15/// .description(Label::new("Your subscription is about to expire. Renew now!"))
16/// .primary_action(Button::new("renew", "Renew Now"))
17/// .secondary_action(Button::new("remind", "Remind Me Later"))
18/// ```
19///
20#[derive(IntoElement, RegisterComponent)]
21pub struct Callout {
22 icon: Option<Icon>,
23 title: Option<SharedString>,
24 description: Option<SharedString>,
25 primary_action: Option<AnyElement>,
26 secondary_action: Option<AnyElement>,
27 tertiary_action: Option<AnyElement>,
28 line_height: Option<Pixels>,
29 bg_color: Option<Hsla>,
30}
31
32impl Callout {
33 /// Creates a new `Callout` component with default styling.
34 pub fn new() -> Self {
35 Self {
36 icon: None,
37 title: None,
38 description: None,
39 primary_action: None,
40 secondary_action: None,
41 tertiary_action: None,
42 line_height: None,
43 bg_color: None,
44 }
45 }
46
47 /// Sets the icon to display in the callout.
48 pub fn icon(mut self, icon: Icon) -> Self {
49 self.icon = Some(icon);
50 self
51 }
52
53 /// Sets the title of the callout.
54 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
55 self.title = Some(title.into());
56 self
57 }
58
59 /// Sets the description of the callout.
60 /// The description can be single or multi-line text.
61 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
62 self.description = Some(description.into());
63 self
64 }
65
66 /// Sets the primary call-to-action button.
67 pub fn primary_action(mut self, action: impl IntoElement) -> Self {
68 self.primary_action = Some(action.into_any_element());
69 self
70 }
71
72 /// Sets an optional secondary call-to-action button.
73 pub fn secondary_action(mut self, action: impl IntoElement) -> Self {
74 self.secondary_action = Some(action.into_any_element());
75 self
76 }
77
78 /// Sets an optional tertiary call-to-action button.
79 pub fn tertiary_action(mut self, action: impl IntoElement) -> Self {
80 self.tertiary_action = Some(action.into_any_element());
81 self
82 }
83
84 /// Sets a custom line height for the callout content.
85 pub fn line_height(mut self, line_height: Pixels) -> Self {
86 self.line_height = Some(line_height);
87 self
88 }
89
90 /// Sets a custom background color for the callout content.
91 pub fn bg_color(mut self, color: Hsla) -> Self {
92 self.bg_color = Some(color);
93 self
94 }
95}
96
97impl RenderOnce for Callout {
98 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
99 let line_height = self.line_height.unwrap_or(window.line_height());
100 let bg_color = self
101 .bg_color
102 .unwrap_or(cx.theme().colors().panel_background);
103 let has_actions = self.primary_action.is_some()
104 || self.secondary_action.is_some()
105 || self.tertiary_action.is_some();
106
107 h_flex()
108 .p_2()
109 .gap_2()
110 .items_start()
111 .bg(bg_color)
112 .overflow_x_hidden()
113 .when_some(self.icon, |this, icon| {
114 this.child(h_flex().h(line_height).justify_center().child(icon))
115 })
116 .child(
117 v_flex()
118 .min_w_0()
119 .w_full()
120 .child(
121 h_flex()
122 .h(line_height)
123 .w_full()
124 .gap_1()
125 .justify_between()
126 .when_some(self.title, |this, title| {
127 this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
128 })
129 .when(has_actions, |this| {
130 this.child(
131 h_flex()
132 .gap_0p5()
133 .when_some(self.tertiary_action, |this, action| {
134 this.child(action)
135 })
136 .when_some(self.secondary_action, |this, action| {
137 this.child(action)
138 })
139 .when_some(self.primary_action, |this, action| {
140 this.child(action)
141 }),
142 )
143 }),
144 )
145 .when_some(self.description, |this, description| {
146 this.child(
147 div()
148 .w_full()
149 .flex_1()
150 .text_ui_sm(cx)
151 .text_color(cx.theme().colors().text_muted)
152 .child(description),
153 )
154 }),
155 )
156 }
157}
158
159impl Component for Callout {
160 fn scope() -> ComponentScope {
161 ComponentScope::DataDisplay
162 }
163
164 fn description() -> Option<&'static str> {
165 Some(
166 "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.",
167 )
168 }
169
170 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
171 let callout_examples = vec![
172 single_example(
173 "Simple with Title Only",
174 Callout::new()
175 .icon(
176 Icon::new(IconName::Info)
177 .color(Color::Accent)
178 .size(IconSize::Small),
179 )
180 .title("System maintenance scheduled for tonight")
181 .primary_action(
182 Button::new("got-it", "Got it", cx).label_size(LabelSize::Small),
183 )
184 .into_any_element(),
185 )
186 .width(px(580.)),
187 single_example(
188 "With Title and Description",
189 Callout::new()
190 .icon(
191 Icon::new(IconName::Warning)
192 .color(Color::Warning)
193 .size(IconSize::Small),
194 )
195 .title("Your settings contain deprecated values")
196 .description(
197 "We'll backup your current settings and update them to the new format.",
198 )
199 .primary_action(
200 Button::new("update", "Backup & Update", cx).label_size(LabelSize::Small),
201 )
202 .secondary_action(
203 Button::new("dismiss", "Dismiss", cx).label_size(LabelSize::Small),
204 )
205 .into_any_element(),
206 )
207 .width(px(580.)),
208 single_example(
209 "Error with Multiple Actions",
210 Callout::new()
211 .icon(
212 Icon::new(IconName::Close)
213 .color(Color::Error)
214 .size(IconSize::Small),
215 )
216 .title("Thread reached the token limit")
217 .description("Start a new thread from a summary to continue the conversation.")
218 .primary_action(
219 Button::new("new-thread", "Start New Thread", cx)
220 .label_size(LabelSize::Small),
221 )
222 .secondary_action(
223 Button::new("view-summary", "View Summary", cx)
224 .label_size(LabelSize::Small),
225 )
226 .into_any_element(),
227 )
228 .width(px(580.)),
229 single_example(
230 "Multi-line Description",
231 Callout::new()
232 .icon(
233 Icon::new(IconName::Sparkle)
234 .color(Color::Accent)
235 .size(IconSize::Small),
236 )
237 .title("Upgrade to Pro")
238 .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
239 .primary_action(
240 Button::new("upgrade", "Upgrade Now", cx).label_size(LabelSize::Small),
241 )
242 .secondary_action(
243 Button::new("learn-more", "Learn More", cx).label_size(LabelSize::Small),
244 )
245 .into_any_element(),
246 )
247 .width(px(580.)),
248 ];
249
250 Some(
251 example_group(callout_examples)
252 .vertical()
253 .into_any_element(),
254 )
255 }
256}