1use std::f32::consts::{FRAC_PI_2, TAU};
8
9use gpui::{
10 AnimationExt as _, AnyElement, App, Hsla, IntoElement, ParentElement, PathBuilder, Pixels,
11 Point, RenderOnce, SharedString, Styled, Window, canvas, div, point, px,
12};
13use gpui_kit_semantics::Semantic;
14use gpui_kit_theme::{ActiveTheme, ControlSize, TypeScale};
15
16use crate::display::progress::ProgressValue;
17use crate::foundation::{Ident, Sizable, StyledExt};
18use crate::motion::{self, MotionSpec};
19
20const RING_SCALE: f32 = 1.4;
22
23#[derive(Debug, IntoElement)]
25pub struct ProgressCircle {
26 ident: Ident,
27 label: Option<SharedString>,
28 centre: Option<SharedString>,
30 value: ProgressValue,
31 size: ControlSize,
32}
33
34impl ProgressCircle {
35 pub fn new(ident: impl Into<Ident>) -> Self {
36 Self {
37 ident: ident.into(),
38 label: None,
39 centre: None,
40 value: ProgressValue::default(),
41 size: ControlSize::Md,
42 }
43 }
44
45 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
47 self.label = Some(label.into());
48 self
49 }
50
51 pub fn fraction(mut self, fraction: f32) -> Self {
53 self.value.set_fraction(fraction);
54 self
55 }
56
57 pub fn count(mut self, done: usize, total: usize) -> Self {
60 self.value.set_count(done, total);
61 self
62 }
63
64 pub fn display(mut self, display: impl Into<SharedString>) -> Self {
66 self.value.display = Some(display.into());
67 self
68 }
69
70 pub fn centre(mut self, centre: impl Into<SharedString>) -> Self {
73 self.centre = Some(centre.into());
74 self
75 }
76}
77
78impl Sizable for ProgressCircle {
79 fn control_size(mut self, size: ControlSize) -> Self {
80 self.size = size;
81 self
82 }
83}
84
85impl RenderOnce for ProgressCircle {
86 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
87 let theme = cx.theme().clone();
88 let metrics = theme.control.get(self.size);
89 let diameter = (metrics.height * RING_SCALE).round();
90 let stroke = theme.borders.thick;
91 let radius = (diameter - stroke) / 2.0;
92 let track = theme.colors.hairline_strong;
93 let accent = theme.colors.accent;
94
95 let drawn = self.value.fraction.map(|fraction| {
98 motion::tracked(
99 &self.ident.semantic_id(),
100 fraction,
101 motion::resize(&theme),
102 window,
103 cx,
104 )
105 });
106
107 let muted = accent.opacity(theme.opacity.muted);
108 let still = motion::reduce_motion(cx);
115 let ring: AnyElement = if drawn.is_none() && !still {
116 let period = MotionSpec::new(
117 motion::Activity::Working.period_ms(&theme),
118 motion::Activity::Working.curve(&theme),
119 );
120 div()
121 .size(px(diameter))
122 .with_animation(
123 self.ident.child("turn").element_id(),
124 period.repeating(),
125 move |element, phase| {
126 element.child(ring_canvas(
127 diameter,
128 radius,
129 stroke,
130 track,
131 accent,
132 muted,
133 None,
134 Some(phase),
135 ))
136 },
137 )
138 .into_any_element()
139 } else {
140 ring_canvas(diameter, radius, stroke, track, accent, muted, drawn, None)
141 .into_any_element()
142 };
143
144 let centre = self.centre.clone().map(|reading| {
145 div()
146 .absolute()
147 .inset_0()
148 .flex()
149 .items_center()
150 .justify_center()
151 .type_scale(&theme, TypeScale::Caption)
152 .text_color(theme.colors.text_muted)
153 .child(reading)
154 });
155
156 div()
157 .flex_none()
158 .relative()
159 .size(px(diameter))
160 .child(ring)
161 .children(centre)
162 .semantic_in(
163 cx,
164 self.value.spec(self.ident.semantic_id(), self.label, cx),
165 )
166 }
167}
168
169const TRAVELLING_ARC: f32 = 0.25;
174
175#[allow(clippy::too_many_arguments)]
180fn ring_canvas(
181 diameter: f32,
182 radius: f32,
183 stroke: f32,
184 track: Hsla,
185 accent: Hsla,
186 muted: Hsla,
187 drawn: Option<f32>,
188 phase: Option<f32>,
191) -> impl IntoElement {
192 canvas(
193 |_, _, _| {},
194 move |bounds, _, window, _| {
195 let centre = bounds.center();
196 arc(window, centre, radius, stroke, 0.0, 1.0, track);
197 match (drawn, phase) {
198 (Some(fraction), _) if fraction > 0.0 => {
199 arc(window, centre, radius, stroke, 0.0, fraction, accent)
200 }
201 (Some(_), _) => {}
202 (None, Some(phase)) => arc(
203 window,
204 centre,
205 radius,
206 stroke,
207 phase,
208 phase + TRAVELLING_ARC,
209 accent,
210 ),
211 (None, None) => arc(window, centre, radius, stroke, 0.0, 1.0, muted),
212 }
213 },
214 )
215 .size(px(diameter))
216}
217
218fn arc(
223 window: &mut Window,
224 centre: Point<Pixels>,
225 radius: f32,
226 width: f32,
227 from: f32,
228 to: f32,
229 color: Hsla,
230) {
231 if radius <= 0.0 || to <= from {
232 return;
233 }
234 let steps = (((to - from) * 96.0).ceil() as usize).max(2);
235 let at = |turn: f32| {
236 let angle = turn * TAU - FRAC_PI_2;
237 point(
238 centre.x + px(radius * angle.cos()),
239 centre.y + px(radius * angle.sin()),
240 )
241 };
242
243 let mut builder = PathBuilder::stroke(px(width));
244 builder.move_to(at(from));
245 for step in 1..=steps {
246 builder.line_to(at(from + (to - from) * step as f32 / steps as f32));
247 }
248 if let Ok(path) = builder.build() {
249 window.paint_path(path, color);
250 }
251}