use std::f64::consts::PI;
use accesskit::Role;
use kurbo::{Affine, Cap, Stroke};
use smallvec::SmallVec;
use tracing::trace;
use vello::Scene;
use crate::kurbo::Line;
use crate::widget::{WidgetMut, WidgetRef};
use crate::{
theme, AccessCtx, AccessEvent, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Vec2, Widget,
};
pub struct Spinner {
t: f64,
color: Color,
}
impl Spinner {
pub fn new() -> Spinner {
Spinner::default()
}
pub fn with_color(mut self, color: impl Into<Color>) -> Self {
self.color = color.into();
self
}
}
impl WidgetMut<'_, Spinner> {
pub fn set_color(&mut self, color: impl Into<Color>) {
self.widget.color = color.into();
self.ctx.request_paint();
}
}
impl Default for Spinner {
fn default() -> Self {
Spinner {
t: 0.0,
color: theme::TEXT_COLOR,
}
}
}
impl Widget for Spinner {
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {}
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
match event {
LifeCycle::WidgetAdded => {
ctx.request_anim_frame();
ctx.request_paint();
}
LifeCycle::AnimFrame(interval) => {
self.t += (*interval as f64) * 1e-9;
if self.t >= 1.0 {
self.t = 0.0;
}
ctx.request_anim_frame();
ctx.request_paint();
}
_ => (),
}
}
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = if bc.is_width_bounded() && bc.is_height_bounded() {
bc.max()
} else {
bc.constrain(Size::new(
theme::BASIC_WIDGET_HEIGHT,
theme::BASIC_WIDGET_HEIGHT,
))
};
trace!("Computed size: {}", size);
size
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
let t = self.t;
let (width, height) = (ctx.size().width, ctx.size().height);
let center = Point::new(width / 2.0, height / 2.0);
let (r, g, b, original_alpha) = {
let c = self.color;
(c.r, c.g, c.b, c.a)
};
let scale_factor = width.min(height) / 40.0;
for step in 1..=12 {
let step = f64::from(step);
let fade_t = (t * 12.0 + 1.0).trunc();
let fade = ((fade_t + step).rem_euclid(12.0) / 12.0) + 1.0 / 12.0;
let angle = Vec2::from_angle((step / 12.0) * -2.0 * PI);
let ambit_start = center + (10.0 * scale_factor * angle);
let ambit_end = center + (20.0 * scale_factor * angle);
let alpha = (fade * original_alpha as f64) as u8;
let color = Color::rgba8(r, g, b, alpha);
scene.stroke(
&Stroke::new(3.0 * scale_factor).with_caps(Cap::Square),
Affine::IDENTITY,
color,
None,
&Line::new(ambit_start, ambit_end),
);
}
}
fn accessibility_role(&self) -> Role {
Role::Unknown
}
fn accessibility(&mut self, _ctx: &mut AccessCtx) {}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_render_snapshot;
use crate::testing::TestHarness;
#[test]
fn simple_spinner() {
let spinner = Spinner::new();
let mut harness = TestHarness::create(spinner);
assert_render_snapshot!(harness, "spinner_init");
}
#[test]
fn edit_spinner() {
let image_1 = {
let spinner = Spinner::new().with_color(Color::PURPLE);
let mut harness = TestHarness::create_with_size(spinner, Size::new(30.0, 30.0));
harness.render()
};
let image_2 = {
let spinner = Spinner::new();
let mut harness = TestHarness::create_with_size(spinner, Size::new(30.0, 30.0));
harness.edit_root_widget(|mut spinner| {
let mut spinner = spinner.downcast::<Spinner>();
spinner.set_color(Color::PURPLE);
});
harness.render()
};
assert!(image_1 == image_2);
}
}