Add progress bar component (#28518)

- Adds the progress bar component

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2025-04-10 12:11:58 -06:00 committed by GitHub
parent b0b52f299c
commit 3abf95216c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 377 additions and 18 deletions

2
Cargo.lock generated
View file

@ -65,6 +65,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"command_palette_hooks", "command_palette_hooks",
"component",
"context_server", "context_server",
"convert_case 0.8.0", "convert_case 0.8.0",
"db", "db",
@ -85,6 +86,7 @@ dependencies = [
"language", "language",
"language_model", "language_model",
"language_model_selector", "language_model_selector",
"linkme",
"log", "log",
"lsp", "lsp",
"markdown", "markdown",

View file

@ -32,6 +32,7 @@ client.workspace = true
clock.workspace = true clock.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
component.workspace = true
context_server.workspace = true context_server.workspace = true
convert_case.workspace = true convert_case.workspace = true
db.workspace = true db.workspace = true
@ -51,6 +52,7 @@ itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
language_model_selector.workspace = true language_model_selector.workspace = true
linkme.workspace = true
log.workspace = true log.workspace = true
lsp.workspace = true lsp.workspace = true
markdown.workspace = true markdown.workspace = true
@ -85,9 +87,9 @@ ui.workspace = true
ui_input.workspace = true ui_input.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies] [dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] }

View file

@ -1,5 +1,7 @@
mod agent_notification; mod agent_notification;
mod context_pill; mod context_pill;
mod user_spending;
pub use agent_notification::*; pub use agent_notification::*;
pub use context_pill::*; pub use context_pill::*;
// pub use user_spending::*;

View 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(),
)
}
}

View file

@ -187,22 +187,20 @@ impl ComponentPreview {
let mut entries = Vec::new(); 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 // Always show all components first
entries.push(PreviewEntry::AllComponents); entries.push(PreviewEntry::AllComponents);
entries.push(PreviewEntry::Separator); entries.push(PreviewEntry::Separator);
for scope in known_scopes.iter() { let mut scopes: Vec<_> = scope_groups
if let Some(components) = scope_groups.remove(scope) { .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() { if !components.is_empty() {
entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
let mut sorted_components = components; 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 let Some(components) = scope_groups.get(&ComponentScope::None) {
if !components.is_empty() { if !components.is_empty() {
entries.push(PreviewEntry::Separator); entries.push(PreviewEntry::Separator);
@ -272,7 +271,12 @@ impl ComponentPreview {
.into_any_element() .into_any_element()
} }
PreviewEntry::Separator => ListItem::new(ix) 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(), .into_any_element(),
} }
} }

View file

@ -22,6 +22,7 @@ mod notification;
mod numeric_stepper; mod numeric_stepper;
mod popover; mod popover;
mod popover_menu; mod popover_menu;
mod progress;
mod radio; mod radio;
mod right_click_menu; mod right_click_menu;
mod scrollbar; mod scrollbar;
@ -61,6 +62,7 @@ pub use notification::*;
pub use numeric_stepper::*; pub use numeric_stepper::*;
pub use popover::*; pub use popover::*;
pub use popover_menu::*; pub use popover_menu::*;
pub use progress::*;
pub use radio::*; pub use radio::*;
pub use right_click_menu::*; pub use right_click_menu::*;
pub use scrollbar::*; pub use scrollbar::*;

View file

@ -267,7 +267,7 @@ impl RenderOnce for IconWithIndicator {
impl Component for Icon { impl Component for Icon {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::None ComponentScope::Images
} }
fn description() -> Option<&'static str> { fn description() -> Option<&'static str> {

View file

@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
impl Component for DecoratedIcon { impl Component for DecoratedIcon {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::None ComponentScope::Images
} }
fn description() -> Option<&'static str> { fn description() -> Option<&'static str> {

View file

@ -199,7 +199,7 @@ impl RenderOnce for Label {
impl Component for Label { impl Component for Label {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::None ComponentScope::Typography
} }
fn description() -> Option<&'static str> { fn description() -> Option<&'static str> {

View file

@ -0,0 +1,2 @@
mod progress_bar;
pub use progress_bar::*;

View 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(),
)
}
}

View file

@ -235,7 +235,7 @@ impl Headline {
impl Component for Headline { impl Component for Headline {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::None ComponentScope::Typography
} }
fn description() -> Option<&'static str> { fn description() -> Option<&'static str> {