ui: Add circular progress component (#49100)
This PR adds a `CircularProgress` component, soon to be used in the agent panel! <img width="500" height="434" alt="Screenshot 2026-02-13 at 10 59@2x" src="https://github.com/user-attachments/assets/3e64aa93-f63e-4abc-bf88-063e10a02389" /> Release Notes: - N/A
This commit is contained in:
1 parent
d0049c8b62
commit
c28624733e
4 files changed
+260
-53
No files matched your search
@@ -40,7 +40,7 @@ impl FeatureFlag for SubagentsFeatureFlag {
|
||||
const NAME: &'static str = "subagents";
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
mod circular_progress;
|
||||
mod progress_bar;
|
||||
|
||||
pub use circular_progress::*;
|
||||
pub use progress_bar::*;
|
||||
@@ -0,0 +1,215 @@
|
||||
use documented::Documented;
|
||||
use gpui::{Hsla, PathBuilder, canvas, point};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A circular progress indicator that displays progress as an arc growing clockwise from the top.
|
||||
#[derive(IntoElement, RegisterComponent, Documented)]
|
||||
pub struct CircularProgress {
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
size: Pixels,
|
||||
bg_color: Hsla,
|
||||
progress_color: Hsla,
|
||||
}
|
||||
|
||||
impl CircularProgress {
|
||||
pub fn new(value: f32, max_value: f32, size: Pixels, cx: &App) -> Self {
|
||||
Self {
|
||||
value,
|
||||
max_value,
|
||||
size,
|
||||
bg_color: cx.theme().colors().border_variant,
|
||||
progress_color: cx.theme().status().info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current progress value.
|
||||
pub fn value(mut self, value: f32) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum value for the progress indicator.
|
||||
pub fn max_value(mut self, max_value: f32) -> Self {
|
||||
self.max_value = max_value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the size (diameter) of the circular progress indicator.
|
||||
pub fn size(mut self, size: Pixels) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background circle color.
|
||||
pub fn bg_color(mut self, color: Hsla) -> Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the progress arc color.
|
||||
pub fn progress_color(mut self, color: Hsla) -> Self {
|
||||
self.progress_color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CircularProgress {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let value = self.value;
|
||||
let max_value = self.max_value;
|
||||
let size = self.size;
|
||||
let bg_color = self.bg_color;
|
||||
let progress_color = self.progress_color;
|
||||
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _cx| {
|
||||
let current_value = value;
|
||||
|
||||
let center_x = bounds.origin.x + bounds.size.width / 2.0;
|
||||
let center_y = bounds.origin.y + bounds.size.height / 2.0;
|
||||
|
||||
let stroke_width = px(4.0);
|
||||
let radius = (size / 2.0) - stroke_width;
|
||||
|
||||
// Draw background circle (full 360 degrees)
|
||||
let mut bg_builder = PathBuilder::stroke(stroke_width);
|
||||
|
||||
// Start at rightmost point
|
||||
bg_builder.move_to(point(center_x + radius, center_y));
|
||||
|
||||
// Draw full circle using two 180-degree arcs
|
||||
bg_builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
true,
|
||||
point(center_x - radius, center_y),
|
||||
);
|
||||
bg_builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
true,
|
||||
point(center_x + radius, center_y),
|
||||
);
|
||||
bg_builder.close();
|
||||
|
||||
if let Ok(path) = bg_builder.build() {
|
||||
window.paint_path(path, bg_color);
|
||||
}
|
||||
|
||||
// Draw progress arc if there's any progress
|
||||
let progress = (current_value / max_value).clamp(0.0, 1.0);
|
||||
if progress > 0.0 {
|
||||
let mut progress_builder = PathBuilder::stroke(stroke_width);
|
||||
|
||||
// Handle 100% progress as a special case by drawing a full circle
|
||||
if progress >= 0.999 {
|
||||
// Start at rightmost point
|
||||
progress_builder.move_to(point(center_x + radius, center_y));
|
||||
|
||||
// Draw full circle using two 180-degree arcs
|
||||
progress_builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
true,
|
||||
point(center_x - radius, center_y),
|
||||
);
|
||||
progress_builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
false,
|
||||
true,
|
||||
point(center_x + radius, center_y),
|
||||
);
|
||||
progress_builder.close();
|
||||
} else {
|
||||
// Start at 12 o'clock (top) position
|
||||
let start_x = center_x;
|
||||
let start_y = center_y - radius;
|
||||
progress_builder.move_to(point(start_x, start_y));
|
||||
|
||||
// Calculate the end point of the arc based on progress
|
||||
// Progress sweeps clockwise from -90° (top)
|
||||
let angle = -PI / 2.0 + (progress * 2.0 * PI);
|
||||
let end_x = center_x + radius * angle.cos();
|
||||
let end_y = center_y + radius * angle.sin();
|
||||
|
||||
// Use large_arc flag when progress > 0.5 (more than 180 degrees)
|
||||
let large_arc = progress > 0.5;
|
||||
|
||||
progress_builder.arc_to(
|
||||
point(radius, radius),
|
||||
px(0.),
|
||||
large_arc,
|
||||
true, // sweep clockwise
|
||||
point(end_x, end_y),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(path) = progress_builder.build() {
|
||||
window.paint_path(path, progress_color);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.size(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CircularProgress {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Status
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some(
|
||||
"A circular progress indicator that displays progress as an arc growing clockwise from the top.",
|
||||
)
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let max_value = 100.0;
|
||||
let container = || v_flex().items_center().gap_1();
|
||||
|
||||
Some(
|
||||
example_group(vec![single_example(
|
||||
"Examples",
|
||||
h_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
container()
|
||||
.child(CircularProgress::new(0.0, max_value, px(48.0), cx))
|
||||
.child(Label::new("0%").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(CircularProgress::new(25.0, max_value, px(48.0), cx))
|
||||
.child(Label::new("25%").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(CircularProgress::new(50.0, max_value, px(48.0), cx))
|
||||
.child(Label::new("50%").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(CircularProgress::new(75.0, max_value, px(48.0), cx))
|
||||
.child(Label::new("75%").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(CircularProgress::new(100.0, max_value, px(48.0), cx))
|
||||
.child(Label::new("100%").size(LabelSize::Small)),
|
||||
)
|
||||
.into_any_element(),
|
||||
)])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,9 @@ impl RenderOnce for ProgressBar {
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.h(px(8.0))
|
||||
.h_2()
|
||||
.p_0p5()
|
||||
.rounded_full()
|
||||
.p(px(2.0))
|
||||
.bg(self.bg_color)
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.08),
|
||||
@@ -99,58 +99,47 @@ impl Component for ProgressBar {
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let max_value = 180.0;
|
||||
let container = || v_flex().w_full().gap_1();
|
||||
|
||||
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(ProgressBar::new("empty", 0.0, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("38%"))
|
||||
.child(Label::new("Partial")),
|
||||
)
|
||||
.child(ProgressBar::new("partial", max_value * 0.35, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.child(Label::new("100%"))
|
||||
.child(Label::new("Complete")),
|
||||
)
|
||||
.child(ProgressBar::new("filled", max_value, max_value, cx)),
|
||||
)
|
||||
.into_any_element(),
|
||||
example_group(vec![single_example(
|
||||
"Examples",
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
container()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("0%"))
|
||||
.child(Label::new("Empty")),
|
||||
)
|
||||
.child(ProgressBar::new("empty", 0.0, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("38%"))
|
||||
.child(Label::new("Partial")),
|
||||
)
|
||||
.child(ProgressBar::new("partial", max_value * 0.35, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
container()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("100%"))
|
||||
.child(Label::new("Complete")),
|
||||
)
|
||||
.child(ProgressBar::new("filled", max_value, max_value, cx)),
|
||||
)
|
||||
.into_any_element(),
|
||||
)])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user