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