Skip to main content

zest_widget/widget/
spinner.rs

1//! Passive rotating-arc loading indicator.
2//!
3//! The widget itself holds no animation state: it draws a faint full
4//! background ring plus a brighter arc segment of a fixed `arc_deg`
5//! sweep, rotated to a host-provided `angle` (degrees). The host drives
6//! the rotation by storing an angle and bumping it on a timer, then
7//! rebuilding the widget each frame. See `examples/spinner.rs` for a
8//! self-rescheduling tick loop.
9
10use super::Widget;
11use core::marker::PhantomData;
12use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
13use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
14use zest_theme::Theme;
15
16/// Passive spinner: a rotating arc segment over a faint background ring.
17pub struct Spinner<'a, C: PixelColor, M: Clone> {
18    rect: Rectangle,
19    /// Current rotation offset in degrees (driven by the host).
20    angle: i32,
21    /// Length of the visible (bright) arc segment in degrees.
22    arc_deg: i32,
23    /// Stroke thickness in pixels.
24    width: u32,
25    /// Whether to draw the faint background ring behind the arc.
26    track: bool,
27    track_color: Option<C>,
28    arc_color: Option<C>,
29    w: Length,
30    h: Length,
31    _phantom: PhantomData<&'a M>,
32}
33
34impl<'a, C: PixelColor, M: Clone> Spinner<'a, C, M> {
35    /// New spinner rotated to `angle` degrees. Defaults: 90° visible
36    /// arc, 5px stroke, background ring on, theme colors, fills its slot.
37    pub fn new(angle: i32) -> Self {
38        Self {
39            rect: Rectangle::zero(),
40            angle,
41            arc_deg: 90,
42            width: 5,
43            track: true,
44            track_color: None,
45            arc_color: None,
46            w: Length::Fill,
47            h: Length::Fill,
48            _phantom: PhantomData,
49        }
50    }
51
52    /// Set the current rotation angle in degrees.
53    #[must_use]
54    pub fn angle(mut self, angle: i32) -> Self {
55        self.angle = angle;
56        self
57    }
58
59    /// Length of the bright arc segment in degrees.
60    #[must_use]
61    pub fn arc_deg(mut self, arc_deg: i32) -> Self {
62        self.arc_deg = arc_deg;
63        self
64    }
65
66    /// Stroke thickness in pixels.
67    #[must_use]
68    pub fn width_px(mut self, width: u32) -> Self {
69        self.width = width;
70        self
71    }
72
73    /// Toggle the faint full background ring.
74    #[must_use]
75    pub fn track(mut self, on: bool) -> Self {
76        self.track = on;
77        self
78    }
79
80    /// Override the background-ring color (default:
81    /// `theme.background.divider`).
82    #[must_use]
83    pub fn track_color(mut self, color: C) -> Self {
84        self.track_color = Some(color);
85        self
86    }
87
88    /// Override the rotating-arc color (default:
89    /// `theme.accent.base`).
90    #[must_use]
91    pub fn arc_color(mut self, color: C) -> Self {
92        self.arc_color = Some(color);
93        self
94    }
95
96    /// Width sizing intent.
97    #[must_use]
98    pub fn width(mut self, width: impl Into<Length>) -> Self {
99        self.w = width.into();
100        self
101    }
102
103    /// Height sizing intent.
104    #[must_use]
105    pub fn height(mut self, height: impl Into<Length>) -> Self {
106        self.h = height.into();
107        self
108    }
109
110    fn center(&self) -> Point {
111        Point::new(
112            self.rect.top_left.x + self.rect.size.width as i32 / 2,
113            self.rect.top_left.y + self.rect.size.height as i32 / 2,
114        )
115    }
116
117    fn radius(&self) -> u32 {
118        let smaller = self.rect.size.width.min(self.rect.size.height);
119        (smaller / 2).saturating_sub(self.width.div_ceil(2))
120    }
121}
122
123impl<'a, C: PixelColor, M: Clone> Widget<C, M> for Spinner<'a, C, M> {
124    fn measure(&mut self, constraints: Constraints) -> Size {
125        let w = self.w.resolve(constraints.max.width, constraints.max.width);
126        let h = self
127            .h
128            .resolve(constraints.max.height, constraints.max.height);
129        constraints.clamp(Size::new(w, h))
130    }
131
132    fn preferred_size(&self) -> (Length, Length) {
133        (self.w, self.h)
134    }
135
136    fn arrange(&mut self, rect: Rectangle) {
137        self.rect = rect;
138    }
139
140    fn rect(&self) -> Rectangle {
141        self.rect
142    }
143
144    fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
145        None
146    }
147
148    fn draw<'t>(
149        &self,
150        renderer: &mut dyn Renderer<C>,
151        theme: &Theme<'t, C>,
152    ) -> Result<(), RenderError> {
153        let center = self.center();
154        let radius = self.radius();
155        if radius == 0 {
156            return Ok(());
157        }
158        let track = self.track_color.unwrap_or(theme.background.divider);
159        let arc = self.arc_color.unwrap_or(theme.accent.base);
160
161        if self.track {
162            renderer.stroke_arc(center, radius, 0, 360, self.width, track)?;
163        }
164        renderer.stroke_arc(center, radius, self.angle, self.arc_deg, self.width, arc)?;
165        Ok(())
166    }
167}