druid 0.8.3

Data-oriented Rust UI design toolkit.
Documentation
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! An animated spinner widget.

use std::f64::consts::PI;
use tracing::{instrument, trace};

use druid::kurbo::Line;
use druid::widget::prelude::*;
use druid::{theme, Color, Data, KeyOrValue, Point, Vec2};

/// 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`]: super::SizedBox
pub struct Spinner {
    t: f64,
    color: KeyOrValue<Color>,
}

impl Spinner {
    /// Create a spinner widget
    pub fn new() -> Spinner {
        Spinner::default()
    }

    /// Builder-style method for setting the spinner's color.
    ///
    /// The argument can be either a [`Color`] or a [`Key<Color>`].
    ///
    /// [`Key<Color>`]: crate::Key
    pub fn with_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
        self.color = color.into();
        self
    }

    /// Set the spinner's color.
    ///
    /// The argument can be either a [`Color`] or a [`Key<Color>`].
    ///
    /// [`Key<Color>`]: crate::Key
    pub fn set_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
        self.color = color.into();
    }
}

impl Default for Spinner {
    fn default() -> Self {
        Spinner {
            t: 0.0,
            color: theme::TEXT_COLOR.into(),
        }
    }
}

impl<T: Data> Widget<T> for Spinner {
    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, event, _data, _env))]
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &Env) {
        if let Event::AnimFrame(interval) = event {
            self.t += (*interval as f64) * 1e-9;
            if self.t >= 1.0 {
                self.t = 0.0;
            }
            ctx.request_anim_frame();
            ctx.request_paint();
        }
    }

    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, event, _data, _env))]
    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) {
        if let LifeCycle::WidgetAdded = event {
            ctx.request_anim_frame();
            ctx.request_paint();
        }
    }

    #[instrument(
        name = "Spinner",
        level = "trace",
        skip(self, _ctx, _old_data, _data, _env)
    )]
    fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) {}

    #[instrument(
        name = "Spinner",
        level = "trace",
        skip(self, _layout_ctx, bc, _data, env)
    )]
    fn layout(
        &mut self,
        _layout_ctx: &mut LayoutCtx,
        bc: &BoxConstraints,
        _data: &T,
        env: &Env,
    ) -> Size {
        bc.debug_check("Spinner");

        let size = if bc.is_width_bounded() && bc.is_height_bounded() {
            bc.max()
        } else {
            bc.constrain(Size::new(
                env.get(theme::BASIC_WIDGET_HEIGHT),
                env.get(theme::BASIC_WIDGET_HEIGHT),
            ))
        };

        trace!("Computed size: {}", size);
        size
    }

    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, _data, env))]
    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
        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) = Color::as_rgba(self.color.resolve(env));
        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 = Color::rgba(r, g, b, fade * original_alpha);

            ctx.stroke(
                Line::new(ambit_start, ambit_end),
                &color,
                3.0 * scale_factor,
            );
        }
    }
}