callout.rs

  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::{Callout};
 17///
 18/// Callout::new()
 19///     .severity(Severity::Warning)
 20///     .icon(IconName::Warning)
 21///     .title(Label::new("Be aware of your subscription!"))
 22///     .description(Label::new("Your subscription is about to expire. Renew now!"))
 23///     .actions_slot(Button::new("renew", "Renew Now"))
 24/// ```
 25///
 26#[derive(IntoElement, RegisterComponent)]
 27pub struct Callout {
 28    severity: Severity,
 29    icon: Option<IconName>,
 30    title: Option<SharedString>,
 31    description: Option<SharedString>,
 32    actions_slot: Option<AnyElement>,
 33    dismiss_action: Option<AnyElement>,
 34    line_height: Option<Pixels>,
 35    border_position: BorderPosition,
 36}
 37
 38impl Callout {
 39    /// Creates a new `Callout` component with default styling.
 40    pub fn new() -> Self {
 41        Self {
 42            severity: Severity::Info,
 43            icon: None,
 44            title: None,
 45            description: None,
 46            actions_slot: None,
 47            dismiss_action: None,
 48            line_height: None,
 49            border_position: BorderPosition::Top,
 50        }
 51    }
 52
 53    /// Sets the severity of the callout.
 54    pub fn severity(mut self, severity: Severity) -> Self {
 55        self.severity = severity;
 56        self
 57    }
 58
 59    /// Sets the icon to display in the callout.
 60    pub fn icon(mut self, icon: IconName) -> Self {
 61        self.icon = Some(icon);
 62        self
 63    }
 64
 65    /// Sets the title of the callout.
 66    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
 67        self.title = Some(title.into());
 68        self
 69    }
 70
 71    /// Sets the description of the callout.
 72    /// The description can be single or multi-line text.
 73    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
 74        self.description = Some(description.into());
 75        self
 76    }
 77
 78    /// Sets the primary call-to-action button.
 79    pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
 80        self.actions_slot = Some(action.into_any_element());
 81        self
 82    }
 83
 84    /// Sets an optional dismiss button, which is usually an icon button with a close icon.
 85    /// This button is always rendered as the last one to the far right.
 86    pub fn dismiss_action(mut self, action: impl IntoElement) -> Self {
 87        self.dismiss_action = Some(action.into_any_element());
 88        self
 89    }
 90
 91    /// Sets a custom line height for the callout content.
 92    pub fn line_height(mut self, line_height: Pixels) -> Self {
 93        self.line_height = Some(line_height);
 94        self
 95    }
 96
 97    /// Sets the border position in the callout.
 98    pub fn border_position(mut self, border_position: BorderPosition) -> Self {
 99        self.border_position = border_position;
100        self
101    }
102}
103
104impl RenderOnce for Callout {
105    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
106        let line_height = self.line_height.unwrap_or(window.line_height());
107
108        let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some();
109
110        let (icon, icon_color, bg_color) = match self.severity {
111            Severity::Info => (
112                IconName::Info,
113                Color::Muted,
114                cx.theme().colors().panel_background.opacity(0.),
115            ),
116            Severity::Success => (
117                IconName::Check,
118                Color::Success,
119                cx.theme().status().success.opacity(0.1),
120            ),
121            Severity::Warning => (
122                IconName::Warning,
123                Color::Warning,
124                cx.theme().status().warning_background.opacity(0.2),
125            ),
126            Severity::Error => (
127                IconName::XCircle,
128                Color::Error,
129                cx.theme().status().error.opacity(0.08),
130            ),
131        };
132
133        h_flex()
134            .min_w_0()
135            .w_full()
136            .p_2()
137            .gap_2()
138            .items_start()
139            .map(|this| match self.border_position {
140                BorderPosition::Top => this.border_t_1(),
141                BorderPosition::Bottom => this.border_b_1(),
142            })
143            .border_color(cx.theme().colors().border)
144            .bg(bg_color)
145            .overflow_x_hidden()
146            .when(self.icon.is_some(), |this| {
147                this.child(
148                    h_flex()
149                        .h(line_height)
150                        .justify_center()
151                        .child(Icon::new(icon).size(IconSize::Small).color(icon_color)),
152                )
153            })
154            .child(
155                v_flex()
156                    .min_w_0()
157                    .w_full()
158                    .child(
159                        h_flex()
160                            .min_h(line_height)
161                            .w_full()
162                            .gap_1()
163                            .justify_between()
164                            .flex_wrap()
165                            .when_some(self.title, |this, title| {
166                                this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
167                            })
168                            .when(has_actions, |this| {
169                                this.child(
170                                    h_flex()
171                                        .gap_0p5()
172                                        .when_some(self.actions_slot, |this, action| {
173                                            this.child(action)
174                                        })
175                                        .when_some(self.dismiss_action, |this, action| {
176                                            this.child(action)
177                                        }),
178                                )
179                            }),
180                    )
181                    .when_some(self.description, |this, description| {
182                        this.child(
183                            div()
184                                .w_full()
185                                .flex_1()
186                                .text_ui_sm(cx)
187                                .text_color(cx.theme().colors().text_muted)
188                                .child(description),
189                        )
190                    }),
191            )
192    }
193}
194
195impl Component for Callout {
196    fn scope() -> ComponentScope {
197        ComponentScope::DataDisplay
198    }
199
200    fn description() -> Option<&'static str> {
201        Some(
202            "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.",
203        )
204    }
205
206    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
207        let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small);
208        let multiple_actions = || {
209            h_flex()
210                .gap_0p5()
211                .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small))
212                .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small))
213        };
214
215        let basic_examples = vec![
216            single_example(
217                "Simple with Title Only",
218                Callout::new()
219                    .icon(IconName::Info)
220                    .title("System maintenance scheduled for tonight")
221                    .actions_slot(single_action())
222                    .into_any_element(),
223            )
224            .width(px(580.)),
225            single_example(
226                "With Title and Description",
227                Callout::new()
228                    .icon(IconName::Warning)
229                    .title("Your settings contain deprecated values")
230                    .description(
231                        "We'll backup your current settings and update them to the new format.",
232                    )
233                    .actions_slot(single_action())
234                    .into_any_element(),
235            )
236            .width(px(580.)),
237            single_example(
238                "Error with Multiple Actions",
239                Callout::new()
240                    .icon(IconName::Close)
241                    .title("Thread reached the token limit")
242                    .description("Start a new thread from a summary to continue the conversation.")
243                    .actions_slot(multiple_actions())
244                    .into_any_element(),
245            )
246            .width(px(580.)),
247            single_example(
248                "Multi-line Description",
249                Callout::new()
250                    .icon(IconName::Sparkle)
251                    .title("Upgrade to Pro")
252                    .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
253                    .actions_slot(multiple_actions())
254                    .into_any_element(),
255            )
256            .width(px(580.)),
257        ];
258
259        let severity_examples = vec![
260            single_example(
261                "Info",
262                Callout::new()
263                    .icon(IconName::Info)
264                    .title("System maintenance scheduled for tonight")
265                    .actions_slot(single_action())
266                    .into_any_element(),
267            ),
268            single_example(
269                "Warning",
270                Callout::new()
271                    .severity(Severity::Warning)
272                    .icon(IconName::Triangle)
273                    .title("System maintenance scheduled for tonight")
274                    .actions_slot(single_action())
275                    .into_any_element(),
276            ),
277            single_example(
278                "Error",
279                Callout::new()
280                    .severity(Severity::Error)
281                    .icon(IconName::XCircle)
282                    .title("System maintenance scheduled for tonight")
283                    .actions_slot(single_action())
284                    .into_any_element(),
285            ),
286            single_example(
287                "Success",
288                Callout::new()
289                    .severity(Severity::Success)
290                    .icon(IconName::Check)
291                    .title("System maintenance scheduled for tonight")
292                    .actions_slot(single_action())
293                    .into_any_element(),
294            ),
295        ];
296
297        Some(
298            v_flex()
299                .gap_4()
300                .child(example_group(basic_examples).vertical())
301                .child(example_group_with_title("Severity", severity_examples).vertical())
302                .into_any_element(),
303        )
304    }
305}