use blinc_animation::SharedAnimatedTimeline;
use blinc_core::{Brush, Color, CornerRadius, DrawContext, Rect};
use blinc_layout::canvas::{CanvasBounds, CanvasRenderFn};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::RenderProps;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_theme::{ColorToken, ThemeState};
use std::f32::consts::PI;
use std::rc::Rc;
use std::sync::Arc;
use taffy::Style;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpinnerSize {
Small,
#[default]
Medium,
Large,
}
impl SpinnerSize {
fn diameter(&self) -> f32 {
match self {
SpinnerSize::Small => 16.0,
SpinnerSize::Medium => 24.0,
SpinnerSize::Large => 32.0,
}
}
fn border_width(&self) -> f32 {
match self {
SpinnerSize::Small => 2.0,
SpinnerSize::Medium => 2.5,
SpinnerSize::Large => 3.0,
}
}
}
pub struct Spinner {
timeline: SharedAnimatedTimeline,
size: SpinnerSize,
color: Option<Color>,
track_color: Option<Color>,
duration_ms: u32,
classes: Vec<String>,
user_id: Option<String>,
}
impl Spinner {
pub fn new(timeline: SharedAnimatedTimeline) -> Self {
Self {
timeline,
size: SpinnerSize::default(),
color: None,
track_color: None,
duration_ms: 1000, classes: Vec::new(),
user_id: None,
}
}
pub fn size(mut self, size: SpinnerSize) -> Self {
self.size = size;
self
}
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.color = Some(color.into());
self
}
pub fn track_color(mut self, color: impl Into<Color>) -> Self {
self.track_color = Some(color.into());
self
}
pub fn duration_ms(mut self, duration: u32) -> Self {
self.duration_ms = duration;
self
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.classes.push(name.into());
self
}
pub fn id(mut self, id: &str) -> Self {
self.user_id = Some(id.to_string());
self
}
fn create_render_fn(&self) -> CanvasRenderFn {
let theme = ThemeState::get();
let diameter = self.size.diameter();
let border_width = self.size.border_width();
let spinner_color = self
.color
.unwrap_or_else(|| theme.color(ColorToken::Primary));
let track_color = self
.track_color
.unwrap_or_else(|| theme.color(ColorToken::Border));
let timeline = Arc::clone(&self.timeline);
let duration_ms = self.duration_ms;
let entry_id = timeline.lock().unwrap().configure(|t| {
let id = t.add(0, duration_ms, 0.0, 360.0);
t.set_loop(-1); t.start();
id
});
let render_timeline = Arc::clone(&timeline);
Rc::new(move |ctx: &mut dyn DrawContext, bounds: CanvasBounds| {
let angle_deg = render_timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
let angle_rad = angle_deg * PI / 180.0;
let cx = bounds.width / 2.0;
let cy = bounds.height / 2.0;
let radius = (diameter - border_width) / 2.0;
let track_segments = 32;
for i in 0..track_segments {
let t1 = i as f32 / track_segments as f32;
let t2 = (i + 1) as f32 / track_segments as f32;
let a1 = t1 * PI * 2.0;
let a2 = t2 * PI * 2.0;
let x1 = cx + radius * a1.cos();
let y1 = cy + radius * a1.sin();
let x2 = cx + radius * a2.cos();
let y2 = cy + radius * a2.sin();
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt();
ctx.fill_rect(
Rect::new(
x1 - border_width / 2.0,
y1 - border_width / 2.0,
len + border_width,
border_width,
),
CornerRadius::uniform(border_width / 2.0),
Brush::Solid(track_color),
);
}
let arc_length = PI * 1.5; let segments = 24;
for i in 0..segments {
let t1 = i as f32 / segments as f32;
let t2 = (i + 1) as f32 / segments as f32;
let a1 = angle_rad + t1 * arc_length;
let a2 = angle_rad + t2 * arc_length;
let x1 = cx + radius * a1.cos();
let y1 = cy + radius * a1.sin();
let x2 = cx + radius * a2.cos();
let y2 = cy + radius * a2.sin();
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt();
let alpha = 0.3 + 0.7 * t1;
let color_with_alpha =
Color::rgba(spinner_color.r, spinner_color.g, spinner_color.b, alpha);
ctx.fill_rect(
Rect::new(
x1 - border_width / 2.0,
y1 - border_width / 2.0,
len + border_width,
border_width,
),
CornerRadius::uniform(border_width / 2.0),
Brush::Solid(color_with_alpha),
);
}
})
}
}
impl ElementBuilder for Spinner {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
let diameter = self.size.diameter();
let border_width = self.size.border_width();
let total_size = diameter + border_width * 2.0;
let style = Style {
size: taffy::Size {
width: taffy::Dimension::Length(total_size),
height: taffy::Dimension::Length(total_size),
},
..Default::default()
};
tree.create_node(style)
}
fn render_props(&self) -> RenderProps {
RenderProps::default()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
&[]
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Canvas
}
fn canvas_render_info(&self) -> Option<CanvasRenderFn> {
Some(self.create_render_fn())
}
fn layout_style(&self) -> Option<&Style> {
None
}
fn element_classes(&self) -> &[String] {
&self.classes
}
fn element_id(&self) -> Option<&str> {
self.user_id.as_deref()
}
}
pub fn spinner(timeline: SharedAnimatedTimeline) -> Spinner {
Spinner::new(timeline)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spinner_size_values() {
assert_eq!(SpinnerSize::Small.diameter(), 16.0);
assert_eq!(SpinnerSize::Medium.diameter(), 24.0);
assert_eq!(SpinnerSize::Large.diameter(), 32.0);
}
#[test]
fn test_spinner_border_widths() {
assert_eq!(SpinnerSize::Small.border_width(), 2.0);
assert_eq!(SpinnerSize::Medium.border_width(), 2.5);
assert_eq!(SpinnerSize::Large.border_width(), 3.0);
}
}