crates/feature_flags/src/flags.rs 🔗
@@ -40,7 +40,7 @@ impl FeatureFlag for SubagentsFeatureFlag {
const NAME: &'static str = "subagents";
fn enabled_for_staff() -> bool {
- false
+ true
}
}
Danilo Leal created
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
crates/feature_flags/src/flags.rs | 2
crates/ui/src/components/progress.rs | 3
crates/ui/src/components/progress/circular_progress.rs | 215 ++++++++++++
crates/ui/src/components/progress/progress_bar.rs | 93 ++--
4 files changed, 260 insertions(+), 53 deletions(-)
@@ -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(),
)
}
}