callout.rs

  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}