xilem/masonry/src/widgets/spinner.rs

238 lines
6.4 KiB
Rust

// Copyright 2020 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! An animated spinner widget.
use std::f64::consts::PI;
use accesskit::{Node, Role};
use smallvec::SmallVec;
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Affine, Cap, Line, Stroke};
use crate::core::{
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent,
PropertiesMut, PropertiesRef, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget,
WidgetId, WidgetMut,
};
use crate::kurbo::{Point, Size, Vec2};
use crate::peniko::Color;
use crate::theme;
/// An animated spinner widget for showing a loading state.
///
/// To customize the spinner's size, you can place it inside a [`SizedBox`]
/// that has a fixed width and height.
///
/// [`SizedBox`]: crate::widgets::SizedBox
///
#[doc = crate::include_screenshot!("widget/screenshots/masonry__widget__spinner__tests__spinner_init.png", "Spinner frame.")]
pub struct Spinner {
t: f64,
color: Color,
}
// --- MARK: BUILDERS ---
impl Spinner {
/// Create a spinner widget
pub fn new() -> Self {
Self::default()
}
/// Builder-style method for setting the spinner's color.
pub fn with_color(mut self, color: impl Into<Color>) -> Self {
self.color = color.into();
self
}
}
const DEFAULT_SPINNER_COLOR: Color = theme::TEXT_COLOR;
impl Default for Spinner {
fn default() -> Self {
Self {
t: 0.0,
color: DEFAULT_SPINNER_COLOR,
}
}
}
// --- MARK: WIDGETMUT ---
impl Spinner {
/// Set the spinner's color.
pub fn set_color(this: &mut WidgetMut<'_, Self>, color: impl Into<Color>) {
this.widget.color = color.into();
this.ctx.request_paint_only();
}
/// Reset the spinner's color to its default value.
pub fn reset_color(this: &mut WidgetMut<'_, Self>) {
Self::set_color(this, DEFAULT_SPINNER_COLOR);
}
}
// --- MARK: IMPL WIDGET ---
impl Widget for Spinner {
fn on_pointer_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &PointerEvent,
) {
}
fn on_text_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &TextEvent,
) {
}
fn on_access_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &AccessEvent,
) {
}
fn on_anim_frame(
&mut self,
ctx: &mut UpdateCtx,
_props: &mut PropertiesMut<'_>,
interval: u64,
) {
self.t += (interval as f64) * 1e-9;
if self.t >= 1.0 {
self.t = self.t.rem_euclid(1.0);
}
ctx.request_anim_frame();
ctx.request_paint_only();
}
fn register_children(&mut self, _ctx: &mut RegisterCtx) {}
fn update(&mut self, ctx: &mut UpdateCtx, _props: &mut PropertiesMut<'_>, event: &Update) {
match event {
Update::WidgetAdded => {
ctx.request_anim_frame();
}
_ => (),
}
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
_props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> 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,
))
}
}
fn paint(&mut self, ctx: &mut PaintCtx, _props: &PropertiesRef<'_>, 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 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 color = self.color.multiply_alpha(fade as f32);
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 {
// Don't like to use that role, but I'm not seeing
// anything that matches in accesskit::Role
Role::Unknown
}
fn accessibility(
&mut self,
_ctx: &mut AccessCtx,
_props: &PropertiesRef<'_>,
_node: &mut Node,
) {
}
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
SmallVec::new()
}
fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span {
trace_span!("Spinner", id = ctx.widget_id().trace())
}
}
// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestHarness;
use crate::{assert_render_snapshot, palette};
#[test]
fn simple_spinner() {
let spinner = Spinner::new();
let window_size = Size::new(100.0, 100.0);
let mut harness = TestHarness::create_with_size(spinner, window_size);
assert_render_snapshot!(harness, "spinner_init");
harness.animate_ms(700);
assert_render_snapshot!(harness, "spinner_700ms");
harness.animate_ms(400);
assert_render_snapshot!(harness, "spinner_1100ms");
}
#[test]
fn edit_spinner() {
let image_1 = {
let spinner = Spinner::new().with_color(palette::css::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(&mut spinner, palette::css::PURPLE);
});
harness.render()
};
// We don't use assert_eq because we don't want rich assert
assert!(image_1 == image_2);
}
}