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
|
@ -39,7 +39,7 @@ use proto::Plan;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||||
use util::{ResultExt as _, maybe};
|
use util::{ResultExt as _, maybe};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_llm_client::CompletionIntent;
|
use zed_llm_client::CompletionIntent;
|
||||||
|
@ -1175,6 +1175,7 @@ impl MessageEditor {
|
||||||
.map_or(false, |model| {
|
.map_or(false, |model| {
|
||||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||||
});
|
});
|
||||||
|
|
||||||
if !is_using_zed_provider {
|
if !is_using_zed_provider {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -1229,14 +1230,6 @@ impl MessageEditor {
|
||||||
token_usage_ratio: TokenUsageRatio,
|
token_usage_ratio: TokenUsageRatio,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<Div> {
|
) -> Option<Div> {
|
||||||
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
|
||||||
"Thread reached the token limit"
|
|
||||||
} else {
|
|
||||||
"Thread reaching the token limit soon"
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = "Start a new thread from a summary to continue the conversation.";
|
|
||||||
|
|
||||||
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
||||||
Icon::new(IconName::X)
|
Icon::new(IconName::X)
|
||||||
.color(Color::Error)
|
.color(Color::Error)
|
||||||
|
@ -1247,19 +1240,36 @@ impl MessageEditor {
|
||||||
.size(IconSize::XSmall)
|
.size(IconSize::XSmall)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
||||||
|
"Thread reached the token limit"
|
||||||
|
} else {
|
||||||
|
"Thread reaching the token limit soon"
|
||||||
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.child(ui::Callout::multi_line(
|
.border_t_1()
|
||||||
title,
|
.border_color(cx.theme().colors().border)
|
||||||
message,
|
.child(
|
||||||
icon,
|
Callout::new()
|
||||||
"Start New Thread",
|
.line_height(line_height)
|
||||||
Box::new(cx.listener(|this, _, window, cx| {
|
.icon(icon)
|
||||||
|
.title(title)
|
||||||
|
.description(
|
||||||
|
"Start a new thread from a summary to continue the conversation.",
|
||||||
|
)
|
||||||
|
.primary_action(
|
||||||
|
Button::new("start-new-thread", "Start New Thread")
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
let from_thread_id = Some(this.thread.read(cx).id().clone());
|
let from_thread_id = Some(this.thread.read(cx).id().clone());
|
||||||
window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
|
window.dispatch_action(
|
||||||
|
Box::new(NewThread { from_thread_id }),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})),
|
})),
|
||||||
))
|
),
|
||||||
.line_height(line_height),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use client::zed_urls;
|
||||||
use component::{empty_example, example_group_with_title, single_example};
|
use component::{empty_example, example_group_with_title, single_example};
|
||||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||||
use language_model::RequestUsage;
|
use language_model::RequestUsage;
|
||||||
use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
|
use ui::{Callout, prelude::*};
|
||||||
use zed_llm_client::{Plan, UsageLimit};
|
use zed_llm_client::{Plan, UsageLimit};
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
|
@ -91,14 +91,21 @@ impl RenderOnce for UsageCallout {
|
||||||
.size(IconSize::XSmall)
|
.size(IconSize::XSmall)
|
||||||
};
|
};
|
||||||
|
|
||||||
Callout::multi_line(
|
div()
|
||||||
title,
|
.border_t_1()
|
||||||
message,
|
.border_color(cx.theme().colors().border)
|
||||||
icon,
|
.child(
|
||||||
button_text,
|
Callout::new()
|
||||||
Box::new(move |_, _, cx| {
|
.icon(icon)
|
||||||
|
.title(title)
|
||||||
|
.description(message)
|
||||||
|
.primary_action(
|
||||||
|
Button::new("upgrade", button_text)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
cx.open_url(&url);
|
cx.open_url(&url);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
@ -189,10 +196,8 @@ impl Component for UsageCallout {
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
v_flex()
|
||||||
.p_4()
|
.p_4()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(free_examples)
|
.child(free_examples)
|
||||||
.child(trial_examples)
|
.child(trial_examples)
|
||||||
|
|
|
@ -1,51 +1,77 @@
|
||||||
use gpui::ClickEvent;
|
use gpui::AnyElement;
|
||||||
|
|
||||||
use crate::prelude::*;
|
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)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct Callout {
|
pub struct Callout {
|
||||||
title: SharedString,
|
icon: Option<Icon>,
|
||||||
message: Option<SharedString>,
|
title: Option<SharedString>,
|
||||||
icon: Icon,
|
description: Option<SharedString>,
|
||||||
cta_label: SharedString,
|
primary_action: Option<AnyElement>,
|
||||||
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
secondary_action: Option<AnyElement>,
|
||||||
line_height: Option<Pixels>,
|
line_height: Option<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Callout {
|
impl Callout {
|
||||||
pub fn single_line(
|
/// Creates a new `Callout` component with default styling.
|
||||||
title: impl Into<SharedString>,
|
pub fn new() -> Self {
|
||||||
icon: Icon,
|
|
||||||
cta_label: impl Into<SharedString>,
|
|
||||||
cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
title: title.into(),
|
icon: None,
|
||||||
message: None,
|
title: None,
|
||||||
icon,
|
description: None,
|
||||||
cta_label: cta_label.into(),
|
primary_action: None,
|
||||||
cta_action,
|
secondary_action: None,
|
||||||
line_height: None,
|
line_height: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_line(
|
/// Sets the icon to display in the callout.
|
||||||
title: impl Into<SharedString>,
|
pub fn icon(mut self, icon: Icon) -> Self {
|
||||||
message: impl Into<SharedString>,
|
self.icon = Some(icon);
|
||||||
icon: Icon,
|
self
|
||||||
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 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 {
|
pub fn line_height(mut self, line_height: Pixels) -> Self {
|
||||||
self.line_height = Some(line_height);
|
self.line_height = Some(line_height);
|
||||||
self
|
self
|
||||||
|
@ -57,57 +83,54 @@ impl RenderOnce for Callout {
|
||||||
let line_height = self.line_height.unwrap_or(window.line_height());
|
let line_height = self.line_height.unwrap_or(window.line_height());
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.w_full()
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.w_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.bg(cx.theme().colors().panel_background)
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border)
|
|
||||||
.overflow_x_hidden()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.flex_shrink()
|
|
||||||
.overflow_hidden()
|
|
||||||
.gap_2()
|
|
||||||
.items_start()
|
.items_start()
|
||||||
.child(
|
.bg(cx.theme().colors().panel_background)
|
||||||
h_flex()
|
.overflow_x_hidden()
|
||||||
.h(line_height)
|
.when_some(self.icon, |this, icon| {
|
||||||
.items_center()
|
this.child(h_flex().h(line_height).justify_center().child(icon))
|
||||||
.justify_center()
|
})
|
||||||
.child(self.icon),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_shrink()
|
.w_full()
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.h(line_height)
|
.h(line_height)
|
||||||
.items_center()
|
.w_full()
|
||||||
.child(Label::new(self.title).size(LabelSize::Small)),
|
.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)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.when_some(self.message, |this, message| {
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.description, |this, description| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.child(message)
|
.child(description)
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
.text_color(cx.theme().colors().text_muted),
|
.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),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,28 +149,73 @@ impl Component for Callout {
|
||||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||||
let callout_examples = vec![
|
let callout_examples = vec![
|
||||||
single_example(
|
single_example(
|
||||||
"Single Line",
|
"Simple with Title Only",
|
||||||
Callout::single_line(
|
Callout::new()
|
||||||
"Your settings contain deprecated values, please update them.",
|
.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(
|
||||||
|
"With Title and Description",
|
||||||
|
Callout::new()
|
||||||
|
.icon(
|
||||||
Icon::new(IconName::Warning)
|
Icon::new(IconName::Warning)
|
||||||
.color(Color::Warning)
|
.color(Color::Warning)
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small),
|
||||||
"Backup & Update",
|
)
|
||||||
Box::new(|_, _, _| {}),
|
.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(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
.width(px(580.)),
|
.width(px(580.)),
|
||||||
single_example(
|
single_example(
|
||||||
"Multi Line",
|
"Error with Multiple Actions",
|
||||||
Callout::multi_line(
|
Callout::new()
|
||||||
"Thread reached the token limit",
|
.icon(
|
||||||
"Start a new thread from a summary to continue the conversation.",
|
|
||||||
Icon::new(IconName::X)
|
Icon::new(IconName::X)
|
||||||
.color(Color::Error)
|
.color(Color::Error)
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small),
|
||||||
"Start New Thread",
|
)
|
||||||
Box::new(|_, _, _| {}),
|
.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(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue