callout.rs

  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(Button::new("got-it", "Got it").label_size(LabelSize::Small))
182                    .into_any_element(),
183            )
184            .width(px(580.)),
185            single_example(
186                "With Title and Description",
187                Callout::new()
188                    .icon(
189                        Icon::new(IconName::Warning)
190                            .color(Color::Warning)
191                            .size(IconSize::Small),
192                    )
193                    .title("Your settings contain deprecated values")
194                    .description(
195                        "We'll backup your current settings and update them to the new format.",
196                    )
197                    .primary_action(
198                        Button::new("update", "Backup & Update").label_size(LabelSize::Small),
199                    )
200                    .secondary_action(
201                        Button::new("dismiss", "Dismiss").label_size(LabelSize::Small),
202                    )
203                    .into_any_element(),
204            )
205            .width(px(580.)),
206            single_example(
207                "Error with Multiple Actions",
208                Callout::new()
209                    .icon(
210                        Icon::new(IconName::Close)
211                            .color(Color::Error)
212                            .size(IconSize::Small),
213                    )
214                    .title("Thread reached the token limit")
215                    .description("Start a new thread from a summary to continue the conversation.")
216                    .primary_action(
217                        Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small),
218                    )
219                    .secondary_action(
220                        Button::new("view-summary", "View Summary").label_size(LabelSize::Small),
221                    )
222                    .into_any_element(),
223            )
224            .width(px(580.)),
225            single_example(
226                "Multi-line Description",
227                Callout::new()
228                    .icon(
229                        Icon::new(IconName::Sparkle)
230                            .color(Color::Accent)
231                            .size(IconSize::Small),
232                    )
233                    .title("Upgrade to Pro")
234                    .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
235                    .primary_action(
236                        Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small),
237                    )
238                    .secondary_action(
239                        Button::new("learn-more", "Learn More").label_size(LabelSize::Small),
240                    )
241                    .into_any_element(),
242            )
243            .width(px(580.)),
244        ];
245
246        Some(
247            example_group(callout_examples)
248                .vertical()
249                .into_any_element(),
250        )
251    }
252}