Skip to main content

guise/feedback/
ringprogress.rs

1//! `RingProgress` — a circular determinate progress indicator.
2//!
3//! gpui has no arc/conic primitive, so the fill is rendered as a clipped column
4//! rising from the bottom of a circle (a gauge), with the percentage centered on
5//! top. A true stroked ring would need a custom `canvas` paint pass.
6
7use gpui::prelude::*;
8use gpui::{div, px, relative, App, FontWeight, IntoElement, SharedString, Window};
9
10use crate::theme::{theme, ColorName};
11
12/// A circular progress gauge. `RingProgress::new(72.0).label("72%")`.
13#[derive(IntoElement)]
14pub struct RingProgress {
15    value: f32,
16    size: f32,
17    color: ColorName,
18    label: Option<SharedString>,
19}
20
21impl RingProgress {
22    pub fn new(value: f32) -> Self {
23        RingProgress {
24            value: value.clamp(0.0, 100.0),
25            size: 80.0,
26            color: ColorName::Blue,
27            label: None,
28        }
29    }
30
31    /// Diameter in px.
32    pub fn size(mut self, size: f32) -> Self {
33        self.size = size;
34        self
35    }
36
37    pub fn color(mut self, color: ColorName) -> Self {
38        self.color = color;
39        self
40    }
41
42    /// Centered label (defaults to the rounded percentage).
43    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
44        self.label = Some(label.into());
45        self
46    }
47}
48
49impl RenderOnce for RingProgress {
50    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
51        let t = theme(cx);
52        let accent = t.color(self.color, t.primary_shade()).alpha(0.85);
53        let track = if t.scheme.is_dark() {
54            t.color(ColorName::Dark, 4)
55        } else {
56            t.color(ColorName::Gray, 2)
57        }
58        .hsla();
59        let text = t.text().hsla();
60        let frac = (self.value / 100.0).clamp(0.0, 1.0);
61        let label = self
62            .label
63            .unwrap_or_else(|| SharedString::from(format!("{}%", self.value.round() as i64)));
64
65        div()
66            .relative()
67            .w(px(self.size))
68            .h(px(self.size))
69            .rounded(px(self.size / 2.0))
70            .overflow_hidden()
71            .bg(track)
72            .flex()
73            .items_center()
74            .justify_center()
75            // Fill rising from the bottom.
76            .child(
77                div()
78                    .absolute()
79                    .bottom(px(0.0))
80                    .left(px(0.0))
81                    .right(px(0.0))
82                    .h(relative(frac))
83                    .bg(accent),
84            )
85            // Centered label, painted over the fill.
86            .child(
87                div()
88                    .font_weight(FontWeight::SEMIBOLD)
89                    .text_size(px(self.size * 0.22))
90                    .text_color(text)
91                    .child(label),
92            )
93    }
94}