ui: Refactor the Callout component (#32684)
What motivated me to refactor this component was the fact that I wanted a new variant to allow having _two CTAs_ instead of just one. This variant should work with either a single or multiline description. But, given we were using a `Callout::single_line` and `Callout::multi_line` API, I'd then need to have both `Callout::single_line_one_button` and `Callout::single_line_two_buttons` type of variants, which just points to a combinatorial problem. With this refactor, the Callout now follows the same structure of the Banner component, where it's all `Callout::new` and every method is passed as if they were props in a React component, allowing for a more flexible design where you can customize button styles. Also made it slightly more robust for wrapping and removed the top border as that should be defined by the place it is being used in. Release Notes: - N/A
This commit is contained in:
parent
aa1cb9c1e1
commit
29f3e62850
3 changed files with 212 additions and 129 deletions
|
@ -1,51 +1,77 @@
|
|||
use gpui::ClickEvent;
|
||||
use gpui::AnyElement;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A callout component for displaying important information that requires user attention.
|
||||
///
|
||||
/// # Usage Example
|
||||
///
|
||||
/// ```
|
||||
/// use ui::{Callout};
|
||||
///
|
||||
/// Callout::new()
|
||||
/// .icon(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
/// .title(Label::new("Be aware of your subscription!"))
|
||||
/// .description(Label::new("Your subscription is about to expire. Renew now!"))
|
||||
/// .primary_action(Button::new("renew", "Renew Now"))
|
||||
/// .secondary_action(Button::new("remind", "Remind Me Later"))
|
||||
/// ```
|
||||
///
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct Callout {
|
||||
title: SharedString,
|
||||
message: Option<SharedString>,
|
||||
icon: Icon,
|
||||
cta_label: SharedString,
|
||||
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
icon: Option<Icon>,
|
||||
title: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
primary_action: Option<AnyElement>,
|
||||
secondary_action: Option<AnyElement>,
|
||||
line_height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl Callout {
|
||||
pub fn single_line(
|
||||
title: impl Into<SharedString>,
|
||||
icon: Icon,
|
||||
cta_label: impl Into<SharedString>,
|
||||
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
) -> Self {
|
||||
/// Creates a new `Callout` component with default styling.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
message: None,
|
||||
icon,
|
||||
cta_label: cta_label.into(),
|
||||
cta_action,
|
||||
icon: None,
|
||||
title: None,
|
||||
description: None,
|
||||
primary_action: None,
|
||||
secondary_action: None,
|
||||
line_height: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multi_line(
|
||||
title: impl Into<SharedString>,
|
||||
message: impl Into<SharedString>,
|
||||
icon: Icon,
|
||||
cta_label: impl Into<SharedString>,
|
||||
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
message: Some(message.into()),
|
||||
icon,
|
||||
cta_label: cta_label.into(),
|
||||
cta_action,
|
||||
line_height: None,
|
||||
}
|
||||
/// Sets the icon to display in the callout.
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the title of the callout.
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description of the callout.
|
||||
/// The description can be single or multi-line text.
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the primary call-to-action button.
|
||||
pub fn primary_action(mut self, action: impl IntoElement) -> Self {
|
||||
self.primary_action = Some(action.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets an optional secondary call-to-action button.
|
||||
pub fn secondary_action(mut self, action: impl IntoElement) -> Self {
|
||||
self.secondary_action = Some(action.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a custom line height for the callout content.
|
||||
pub fn line_height(mut self, line_height: Pixels) -> Self {
|
||||
self.line_height = Some(line_height);
|
||||
self
|
||||
|
@ -57,57 +83,54 @@ impl RenderOnce for Callout {
|
|||
let line_height = self.line_height.unwrap_or(window.line_height());
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.overflow_x_hidden()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(h_flex().h(line_height).justify_center().child(icon))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink()
|
||||
.overflow_hidden()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.icon),
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.when_some(self.title, |this, title| {
|
||||
this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
|
||||
})
|
||||
.when(
|
||||
self.primary_action.is_some() || self.secondary_action.is_some(),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when_some(self.secondary_action, |this, action| {
|
||||
this.child(action)
|
||||
})
|
||||
.when_some(self.primary_action, |this, action| {
|
||||
this.child(action)
|
||||
}),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_shrink()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.items_center()
|
||||
.child(Label::new(self.title).size(LabelSize::Small)),
|
||||
)
|
||||
.when_some(self.message, |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.child(message)
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Button::new("cta", self.cta_label)
|
||||
.on_click(self.cta_action)
|
||||
.style(ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small),
|
||||
),
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.child(description)
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -126,30 +149,75 @@ impl Component for Callout {
|
|||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let callout_examples = vec![
|
||||
single_example(
|
||||
"Single Line",
|
||||
Callout::single_line(
|
||||
"Your settings contain deprecated values, please update them.",
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
"Backup & Update",
|
||||
Box::new(|_, _, _| {}),
|
||||
)
|
||||
.into_any_element(),
|
||||
"Simple with Title Only",
|
||||
Callout::new()
|
||||
.icon(
|
||||
Icon::new(IconName::Info)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.title("System maintenance scheduled for tonight")
|
||||
.primary_action(Button::new("got-it", "Got it").label_size(LabelSize::Small))
|
||||
.into_any_element(),
|
||||
)
|
||||
.width(px(580.)),
|
||||
single_example(
|
||||
"Multi Line",
|
||||
Callout::multi_line(
|
||||
"Thread reached the token limit",
|
||||
"Start a new thread from a summary to continue the conversation.",
|
||||
Icon::new(IconName::X)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
"Start New Thread",
|
||||
Box::new(|_, _, _| {}),
|
||||
)
|
||||
.into_any_element(),
|
||||
"With Title and Description",
|
||||
Callout::new()
|
||||
.icon(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.title("Your settings contain deprecated values")
|
||||
.description(
|
||||
"We'll backup your current settings and update them to the new format.",
|
||||
)
|
||||
.primary_action(
|
||||
Button::new("update", "Backup & Update").label_size(LabelSize::Small),
|
||||
)
|
||||
.secondary_action(
|
||||
Button::new("dismiss", "Dismiss").label_size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.width(px(580.)),
|
||||
single_example(
|
||||
"Error with Multiple Actions",
|
||||
Callout::new()
|
||||
.icon(
|
||||
Icon::new(IconName::X)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.title("Thread reached the token limit")
|
||||
.description("Start a new thread from a summary to continue the conversation.")
|
||||
.primary_action(
|
||||
Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small),
|
||||
)
|
||||
.secondary_action(
|
||||
Button::new("view-summary", "View Summary").label_size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.width(px(580.)),
|
||||
single_example(
|
||||
"Multi-line Description",
|
||||
Callout::new()
|
||||
.icon(
|
||||
Icon::new(IconName::Sparkle)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.title("Upgrade to Pro")
|
||||
.description("• Unlimited threads\n• Priority support\n• Advanced analytics")
|
||||
.primary_action(
|
||||
Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small),
|
||||
)
|
||||
.secondary_action(
|
||||
Button::new("learn-more", "Learn More").label_size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.width(px(580.)),
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue