use std::f32::consts::{FRAC_PI_2, TAU};
use gpui::{
AnimationExt as _, AnyElement, App, Hsla, IntoElement, ParentElement, PathBuilder, Pixels,
Point, RenderOnce, SharedString, Styled, Window, canvas, div, point, px,
};
use gpui_kit_semantics::Semantic;
use gpui_kit_theme::{ActiveTheme, ControlSize, TypeScale};
use crate::display::progress::ProgressValue;
use crate::foundation::{Ident, Sizable, StyledExt};
use crate::motion::{self, MotionSpec};
const RING_SCALE: f32 = 1.4;
#[derive(Debug, IntoElement)]
pub struct ProgressCircle {
ident: Ident,
label: Option<SharedString>,
centre: Option<SharedString>,
value: ProgressValue,
size: ControlSize,
}
impl ProgressCircle {
pub fn new(ident: impl Into<Ident>) -> Self {
Self {
ident: ident.into(),
label: None,
centre: None,
value: ProgressValue::default(),
size: ControlSize::Md,
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn fraction(mut self, fraction: f32) -> Self {
self.value.set_fraction(fraction);
self
}
pub fn count(mut self, done: usize, total: usize) -> Self {
self.value.set_count(done, total);
self
}
pub fn display(mut self, display: impl Into<SharedString>) -> Self {
self.value.display = Some(display.into());
self
}
pub fn centre(mut self, centre: impl Into<SharedString>) -> Self {
self.centre = Some(centre.into());
self
}
}
impl Sizable for ProgressCircle {
fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
}
impl RenderOnce for ProgressCircle {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let metrics = theme.control.get(self.size);
let diameter = (metrics.height * RING_SCALE).round();
let stroke = theme.borders.thick;
let radius = (diameter - stroke) / 2.0;
let track = theme.colors.hairline_strong;
let accent = theme.colors.accent;
let drawn = self.value.fraction.map(|fraction| {
motion::tracked(
&self.ident.semantic_id(),
fraction,
motion::resize(&theme),
window,
cx,
)
});
let muted = accent.opacity(theme.opacity.muted);
let still = motion::reduce_motion(cx);
let ring: AnyElement = if drawn.is_none() && !still {
let period = MotionSpec::new(
motion::Activity::Working.period_ms(&theme),
motion::Activity::Working.curve(&theme),
);
div()
.size(px(diameter))
.with_animation(
self.ident.child("turn").element_id(),
period.repeating(),
move |element, phase| {
element.child(ring_canvas(
diameter,
radius,
stroke,
track,
accent,
muted,
None,
Some(phase),
))
},
)
.into_any_element()
} else {
ring_canvas(diameter, radius, stroke, track, accent, muted, drawn, None)
.into_any_element()
};
let centre = self.centre.clone().map(|reading| {
div()
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.type_scale(&theme, TypeScale::Caption)
.text_color(theme.colors.text_muted)
.child(reading)
});
div()
.flex_none()
.relative()
.size(px(diameter))
.child(ring)
.children(centre)
.semantic_in(
cx,
self.value.spec(self.ident.semantic_id(), self.label, cx),
)
}
}
const TRAVELLING_ARC: f32 = 0.25;
#[allow(clippy::too_many_arguments)]
fn ring_canvas(
diameter: f32,
radius: f32,
stroke: f32,
track: Hsla,
accent: Hsla,
muted: Hsla,
drawn: Option<f32>,
phase: Option<f32>,
) -> impl IntoElement {
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
let centre = bounds.center();
arc(window, centre, radius, stroke, 0.0, 1.0, track);
match (drawn, phase) {
(Some(fraction), _) if fraction > 0.0 => {
arc(window, centre, radius, stroke, 0.0, fraction, accent)
}
(Some(_), _) => {}
(None, Some(phase)) => arc(
window,
centre,
radius,
stroke,
phase,
phase + TRAVELLING_ARC,
accent,
),
(None, None) => arc(window, centre, radius, stroke, 0.0, 1.0, muted),
}
},
)
.size(px(diameter))
}
fn arc(
window: &mut Window,
centre: Point<Pixels>,
radius: f32,
width: f32,
from: f32,
to: f32,
color: Hsla,
) {
if radius <= 0.0 || to <= from {
return;
}
let steps = (((to - from) * 96.0).ceil() as usize).max(2);
let at = |turn: f32| {
let angle = turn * TAU - FRAC_PI_2;
point(
centre.x + px(radius * angle.cos()),
centre.y + px(radius * angle.sin()),
)
};
let mut builder = PathBuilder::stroke(px(width));
builder.move_to(at(from));
for step in 1..=steps {
builder.line_to(at(from + (to - from) * step as f32 / steps as f32));
}
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}