use std::sync::LazyLock;
use std::time::Instant;
use fluidattacks_designs_domain::loading::{dash_state, Color, Size, DASH_MS, ROTATION_MS};
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use gpui::prelude::FluentBuilder as _;
use gpui::{
canvas, div, point, px, rgb, App, Bounds, IntoElement, ParentElement as _, PathBuilder, Pixels,
Point, RenderOnce, Rgba, SharedString, Styled as _, Window,
};
use crate::theme::ActiveTheme as _;
static EPOCH: LazyLock<Instant> = LazyLock::new(Instant::now);
const TAU: f32 = core::f32::consts::TAU;
const ARC_SEGMENTS: u16 = 64;
const DOT_SEGMENTS: u16 = 24;
#[derive(IntoElement)]
pub struct Loading {
size: Size,
color: Color,
label: Option<SharedString>,
}
impl Loading {
#[must_use]
pub const fn new() -> Self {
Self {
size: Size::Md,
color: Color::Red,
label: None,
}
}
#[must_use]
pub const fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
#[must_use]
pub const fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
#[must_use]
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
}
impl Default for Loading {
fn default() -> Self {
Self::new()
}
}
struct Geometry {
center_x: f32,
center_y: f32,
radius: f32,
width: f32,
}
impl Geometry {
fn of(bounds: &Bounds<Pixels>) -> Self {
let scale = f32::from(bounds.size.width) / 100.0;
let center = bounds.center();
Self {
center_x: f32::from(center.x),
center_y: f32::from(center.y),
radius: 45.0 * scale,
width: 10.0 * scale,
}
}
fn on_circle(&self, angle: f32) -> Point<Pixels> {
point(
px(self.radius.mul_add(angle.cos(), self.center_x)),
px(self.radius.mul_add(angle.sin(), self.center_y)),
)
}
}
fn stroke_arc(window: &mut Window, geometry: &Geometry, start: f32, sweep: f32, color: Rgba) {
let mut builder = PathBuilder::stroke(px(geometry.width));
builder.move_to(geometry.on_circle(start));
for segment in 1..=ARC_SEGMENTS {
let step = f32::from(segment) / f32::from(ARC_SEGMENTS);
builder.line_to(geometry.on_circle(sweep.mul_add(step, start)));
}
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}
fn fill_dot(window: &mut Window, geometry: &Geometry, angle: f32, color: Rgba) {
let end = geometry.on_circle(angle);
let cap = Geometry {
center_x: f32::from(end.x),
center_y: f32::from(end.y),
radius: geometry.width / 2.0,
width: 0.0,
};
let mut builder = PathBuilder::fill();
builder.move_to(cap.on_circle(0.0));
for segment in 1..=DOT_SEGMENTS {
let step = f32::from(segment) / f32::from(DOT_SEGMENTS);
builder.line_to(cap.on_circle(TAU * step));
}
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}
fn paint_spinner(bounds: &Bounds<Pixels>, track: Rgba, stroke: Rgba, window: &mut Window) {
let geometry = Geometry::of(bounds);
stroke_arc(window, &geometry, 0.0, TAU, track);
let elapsed = EPOCH.elapsed().as_secs_f32() * 1000.0;
let spin = (elapsed / ROTATION_MS).fract();
let (visible, dash_turn) = dash_state((elapsed / DASH_MS).fract());
let start = (spin + dash_turn) * TAU;
let sweep = visible * TAU;
stroke_arc(window, &geometry, start, sweep, stroke);
fill_dot(window, &geometry, start, stroke);
fill_dot(window, &geometry, start + sweep, stroke);
window.request_animation_frame();
}
impl RenderOnce for Loading {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = &cx.theme().colors;
let track = Rgba {
alpha: self.color.track_opacity(),
..rgb(self.color.track(theme))
};
let stroke = rgb(self.color.stroke(theme));
let label_color = rgb(self.color.label(theme));
div()
.flex()
.items_center()
.gap(px(8.0))
.child(
canvas(
|_, _, _| (),
move |bounds, (), window, _paint_cx| {
paint_spinner(&bounds, track, stroke, window);
},
)
.size(px(self.size.px())),
)
.when_some(self.label, |el, label| {
el.child(
div()
.font_family(FONT_PRIMARY)
.text_size(px(TextSize::Md.px()))
.line_height(px(TextSize::Md.line_height_px()))
.text_color(label_color)
.child(label),
)
})
}
}