druid/widget/
spinner.rs

1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! An animated spinner widget.
16
17use std::f64::consts::PI;
18use tracing::{instrument, trace};
19
20use druid::kurbo::Line;
21use druid::widget::prelude::*;
22use druid::{theme, Color, Data, KeyOrValue, Point, Vec2};
23
24/// An animated spinner widget for showing a loading state.
25///
26/// To customize the spinner's size, you can place it inside a [`SizedBox`]
27/// that has a fixed width and height.
28///
29/// [`SizedBox`]: super::SizedBox
30pub struct Spinner {
31    t: f64,
32    color: KeyOrValue<Color>,
33}
34
35impl Spinner {
36    /// Create a spinner widget
37    pub fn new() -> Spinner {
38        Spinner::default()
39    }
40
41    /// Builder-style method for setting the spinner's color.
42    ///
43    /// The argument can be either a [`Color`] or a [`Key<Color>`].
44    ///
45    /// [`Key<Color>`]: crate::Key
46    pub fn with_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
47        self.color = color.into();
48        self
49    }
50
51    /// Set the spinner's color.
52    ///
53    /// The argument can be either a [`Color`] or a [`Key<Color>`].
54    ///
55    /// [`Key<Color>`]: crate::Key
56    pub fn set_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
57        self.color = color.into();
58    }
59}
60
61impl Default for Spinner {
62    fn default() -> Self {
63        Spinner {
64            t: 0.0,
65            color: theme::TEXT_COLOR.into(),
66        }
67    }
68}
69
70impl<T: Data> Widget<T> for Spinner {
71    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, event, _data, _env))]
72    fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &Env) {
73        if let Event::AnimFrame(interval) = event {
74            self.t += (*interval as f64) * 1e-9;
75            if self.t >= 1.0 {
76                self.t = 0.0;
77            }
78            ctx.request_anim_frame();
79            ctx.request_paint();
80        }
81    }
82
83    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, event, _data, _env))]
84    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) {
85        if let LifeCycle::WidgetAdded = event {
86            ctx.request_anim_frame();
87            ctx.request_paint();
88        }
89    }
90
91    #[instrument(
92        name = "Spinner",
93        level = "trace",
94        skip(self, _ctx, _old_data, _data, _env)
95    )]
96    fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) {}
97
98    #[instrument(
99        name = "Spinner",
100        level = "trace",
101        skip(self, _layout_ctx, bc, _data, env)
102    )]
103    fn layout(
104        &mut self,
105        _layout_ctx: &mut LayoutCtx,
106        bc: &BoxConstraints,
107        _data: &T,
108        env: &Env,
109    ) -> Size {
110        bc.debug_check("Spinner");
111
112        let size = if bc.is_width_bounded() && bc.is_height_bounded() {
113            bc.max()
114        } else {
115            bc.constrain(Size::new(
116                env.get(theme::BASIC_WIDGET_HEIGHT),
117                env.get(theme::BASIC_WIDGET_HEIGHT),
118            ))
119        };
120
121        trace!("Computed size: {}", size);
122        size
123    }
124
125    #[instrument(name = "Spinner", level = "trace", skip(self, ctx, _data, env))]
126    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
127        let t = self.t;
128        let (width, height) = (ctx.size().width, ctx.size().height);
129        let center = Point::new(width / 2.0, height / 2.0);
130        let (r, g, b, original_alpha) = Color::as_rgba(self.color.resolve(env));
131        let scale_factor = width.min(height) / 40.0;
132
133        for step in 1..=12 {
134            let step = f64::from(step);
135            let fade_t = (t * 12.0 + 1.0).trunc();
136            let fade = ((fade_t + step).rem_euclid(12.0) / 12.0) + 1.0 / 12.0;
137            let angle = Vec2::from_angle((step / 12.0) * -2.0 * PI);
138            let ambit_start = center + (10.0 * scale_factor * angle);
139            let ambit_end = center + (20.0 * scale_factor * angle);
140            let color = Color::rgba(r, g, b, fade * original_alpha);
141
142            ctx.stroke(
143                Line::new(ambit_start, ambit_end),
144                &color,
145                3.0 * scale_factor,
146            );
147        }
148    }
149}