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 const 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 const 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 const 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 const 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 const 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}