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",
"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",

View file

@ -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"] }

View file

@ -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::*;

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

View file

@ -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::*;

View file

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

View file

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

View file

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