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