Add progress bar component (#28518)
- Adds the progress bar component Release Notes: - N/A
This commit is contained in:
parent
b0b52f299c
commit
3abf95216c
12 changed files with 377 additions and 18 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -65,6 +65,7 @@ dependencies = [
|
|||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"context_server",
|
||||
"convert_case 0.8.0",
|
||||
"db",
|
||||
|
@ -85,6 +86,7 @@ dependencies = [
|
|||
"language",
|
||||
"language_model",
|
||||
"language_model_selector",
|
||||
"linkme",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
|
|
|
@ -32,6 +32,7 @@ client.workspace = true
|
|||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
|
@ -51,6 +52,7 @@ itertools.workspace = true
|
|||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
|
@ -85,9 +87,9 @@ ui.workspace = true
|
|||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod agent_notification;
|
||||
mod context_pill;
|
||||
mod user_spending;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use context_pill::*;
|
||||
// pub use user_spending::*;
|
||||
|
|
186
crates/agent/src/ui/user_spending.rs
Normal file
186
crates/agent/src/ui/user_spending.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use gpui::{Entity, Render};
|
||||
use ui::{ProgressBar, prelude::*};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct UserSpending {
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
free_tier_progress: Entity<ProgressBar>,
|
||||
over_tier_progress: Entity<ProgressBar>,
|
||||
}
|
||||
|
||||
impl UserSpending {
|
||||
pub fn new(
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let free_tier_capped = free_tier_current == free_tier_cap;
|
||||
let free_tier_near_capped =
|
||||
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
|
||||
let over_tier_capped = over_tier_current == over_tier_cap;
|
||||
let over_tier_near_capped =
|
||||
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
|
||||
|
||||
let free_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"free_tier",
|
||||
free_tier_current as f32,
|
||||
free_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let over_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"over_tier",
|
||||
over_tier_current as f32,
|
||||
over_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if free_tier_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if free_tier_near_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
if over_tier_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if over_tier_near_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
free_tier_current,
|
||||
free_tier_cap,
|
||||
over_tier_current,
|
||||
over_tier_cap,
|
||||
free_tier_progress,
|
||||
over_tier_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for UserSpending {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let formatted_free_tier = format!(
|
||||
"${} / ${}",
|
||||
self.free_tier_current as f32 / 100.0,
|
||||
self.free_tier_cap as f32 / 100.0
|
||||
);
|
||||
let formatted_over_tier = format!(
|
||||
"${} / ${}",
|
||||
self.over_tier_current as f32 / 100.0,
|
||||
self.over_tier_cap as f32 / 100.0
|
||||
);
|
||||
|
||||
v_group()
|
||||
.elevation_2(cx)
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.w(px(360.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_free_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.free_tier_progress.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Current Spending").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_over_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.over_tier_progress.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UserSpending {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
|
||||
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
|
||||
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
|
||||
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
|
||||
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"New User",
|
||||
div().size_full().child(new_user.clone()).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -187,22 +187,20 @@ impl ComponentPreview {
|
|||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let known_scopes = [
|
||||
ComponentScope::Layout,
|
||||
ComponentScope::Input,
|
||||
ComponentScope::Editor,
|
||||
ComponentScope::Notification,
|
||||
ComponentScope::Collaboration,
|
||||
ComponentScope::VersionControl,
|
||||
ComponentScope::None,
|
||||
];
|
||||
|
||||
// Always show all components first
|
||||
entries.push(PreviewEntry::AllComponents);
|
||||
entries.push(PreviewEntry::Separator);
|
||||
|
||||
for scope in known_scopes.iter() {
|
||||
if let Some(components) = scope_groups.remove(scope) {
|
||||
let mut scopes: Vec<_> = scope_groups
|
||||
.keys()
|
||||
.filter(|scope| !matches!(**scope, ComponentScope::None))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
scopes.sort_by_key(|s| s.to_string());
|
||||
|
||||
for scope in scopes {
|
||||
if let Some(components) = scope_groups.remove(&scope) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
||||
let mut sorted_components = components;
|
||||
|
@ -215,6 +213,7 @@ impl ComponentPreview {
|
|||
}
|
||||
}
|
||||
|
||||
// Add uncategorized components last
|
||||
if let Some(components) = scope_groups.get(&ComponentScope::None) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::Separator);
|
||||
|
@ -272,7 +271,12 @@ impl ComponentPreview {
|
|||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::Separator => ListItem::new(ix)
|
||||
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
||||
.child(
|
||||
h_flex()
|
||||
.occlude()
|
||||
.pt_3()
|
||||
.child(Divider::horizontal_dashed()),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ mod notification;
|
|||
mod numeric_stepper;
|
||||
mod popover;
|
||||
mod popover_menu;
|
||||
mod progress;
|
||||
mod radio;
|
||||
mod right_click_menu;
|
||||
mod scrollbar;
|
||||
|
@ -61,6 +62,7 @@ pub use notification::*;
|
|||
pub use numeric_stepper::*;
|
||||
pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
pub use progress::*;
|
||||
pub use radio::*;
|
||||
pub use right_click_menu::*;
|
||||
pub use scrollbar::*;
|
||||
|
|
|
@ -267,7 +267,7 @@ impl RenderOnce for IconWithIndicator {
|
|||
|
||||
impl Component for Icon {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Images
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
|
|
@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
|
|||
|
||||
impl Component for DecoratedIcon {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Images
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
|
|
@ -199,7 +199,7 @@ impl RenderOnce for Label {
|
|||
|
||||
impl Component for Label {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Typography
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
|
2
crates/ui/src/components/progress.rs
Normal file
2
crates/ui/src/components/progress.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod progress_bar;
|
||||
pub use progress_bar::*;
|
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use documented::Documented;
|
||||
use gpui::{Hsla, point};
|
||||
|
||||
use crate::components::Label;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A progress bar is a horizontal bar that communicates the status of a process.
|
||||
///
|
||||
/// A progress bar should not be used to represent indeterminate progress.
|
||||
#[derive(RegisterComponent, Documented)]
|
||||
pub struct ProgressBar {
|
||||
id: ElementId,
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
bg_color: Hsla,
|
||||
fg_color: Hsla,
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
/// Create a new progress bar with the given value and maximum value.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
value,
|
||||
max_value,
|
||||
bg_color: cx.theme().colors().background,
|
||||
fg_color: cx.theme().status().info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current value of the progress bar.
|
||||
pub fn value(&mut self, value: f32) -> &mut Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum value of the progress bar.
|
||||
pub fn max_value(&mut self, max_value: f32) -> &mut Self {
|
||||
self.max_value = max_value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the background color of the progress bar.
|
||||
pub fn bg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the foreground color of the progress bar.
|
||||
pub fn fg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
self.fg_color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProgressBar {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let fill_width = (self.value / self.max_value).clamp(0.02, 1.0);
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.h(px(8.0))
|
||||
.rounded_full()
|
||||
.py(px(2.0))
|
||||
.px(px(4.0))
|
||||
.bg(self.bg_color)
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.08),
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.rounded_full()
|
||||
.bg(self.fg_color)
|
||||
.w(relative(fill_width)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ProgressBar {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Status
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some(Self::DOCS)
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let max_value = 180.0;
|
||||
|
||||
let empty_progress_bar = cx.new(|cx| ProgressBar::new("empty", 0.0, max_value, cx));
|
||||
let partial_progress_bar =
|
||||
cx.new(|cx| ProgressBar::new("partial", max_value * 0.35, max_value, cx));
|
||||
let filled_progress_bar = cx.new(|cx| ProgressBar::new("filled", max_value, max_value, cx));
|
||||
|
||||
Some(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.w(px(240.0))
|
||||
.child(div().child("Progress Bar"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("0%"))
|
||||
.child(Label::new("Empty")),
|
||||
)
|
||||
.child(empty_progress_bar.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("38%"))
|
||||
.child(Label::new("Partial")),
|
||||
)
|
||||
.child(partial_progress_bar.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("100%"))
|
||||
.child(Label::new("Complete")),
|
||||
)
|
||||
.child(filled_progress_bar.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -235,7 +235,7 @@ impl Headline {
|
|||
|
||||
impl Component for Headline {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
ComponentScope::Typography
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue