Skip to main content

azul_layout/widgets/
spinner.rs

1//! Spinner / activity-indicator widget — a small indeterminate "busy" ring. A
2//! stateless single styled node (a near-clone of the leaf-node construction of
3//! [`crate::widgets::badge::Badge`] / [`crate::widgets::progressbar::ProgressBar`]),
4//! drawn as a circular ring whose three sides use a faint "track" colour and
5//! whose top side uses a solid accent colour — the classic spinner look frozen
6//! mid-rotation.
7//!
8//! ## PARTIAL — STATIC ONLY (no spin animation). See `TODO2` below.
9//!
10//! TODO2: this spinner is **static** — it shows the indeterminate ring shape but
11//! does NOT rotate. Azul has no declarative CSS animation: there is no
12//! `@keyframes` / `animation` / `transition` CSS property (`css/src/props` only
13//! exposes one-shot `Transform`/`TransformOrigin` GPU props and the system-level
14//! `AnimationMetrics` toggle; `props/basic/animation.rs` is SVG-curve
15//! interpolation maths, not a style-driven keyframe engine). Producing real
16//! motion would require a timer-driven `Update` loop that re-issues a rotating
17//! `CssProperty::Transform` each tick (the same mechanism scroll-smoothing uses),
18//! driven from the host app — there is no widget-local way to start such a timer
19//! at DOM-build time. Rather than fake motion that cannot be produced, the ring
20//! is rendered statically; a future revision can add the timer-driven rotation
21//! once a widget-owned animation hook exists. (Compile-verified; not GUI-verified.)
22//!
23//! Key types: [`Spinner`].
24
25use azul_core::dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec};
26use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
27use azul_css::{
28    props::{
29        basic::{color::ColorU, *},
30        layout::{LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight},
31        property::{CssProperty, *},
32        style::{LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius},
33    },
34    AzString,
35};
36
37static SPINNER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-spinner"))];
38
39/// Default ring diameter, in logical px.
40const DEFAULT_SIZE: isize = 24;
41/// Faint "track" colour for the three inactive sides (#d0d4d9).
42const DEFAULT_TRACK_COLOR: ColorU = ColorU { r: 208, g: 212, b: 217, a: 255 };
43/// Solid accent colour for the active (top) arc (#0d6efd, accent blue).
44const DEFAULT_ACCENT_COLOR: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
45
46/// An indeterminate busy-indicator ring. Stateless; renders a single styled
47/// node. **Static** — the ring shows the spinner shape but does not rotate
48/// (see the module-level `TODO2`).
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[repr(C)]
51pub struct Spinner {
52    /// The ring diameter, in logical px.
53    pub size: isize,
54    /// Colour of the active (top) arc.
55    pub color: ColorU,
56    /// Colour of the three inactive ("track") sides.
57    pub track_color: ColorU,
58    /// The computed inline style for the ring.
59    pub spinner_style: CssPropertyWithConditionsVec,
60}
61
62/// Builds the ring style for the given diameter and colours. All three are
63/// instance-dependent, so the style is built at runtime per the recipe's
64/// "runtime vec when param-dependent" path (see `badge::build_badge_style`).
65fn build_spinner_style(size: isize, color: ColorU, track_color: ColorU) -> CssPropertyWithConditionsVec {
66    // Ring thickness scales with the diameter (min 2px); radius = size/2 → circle.
67    let border_width = (size / 8).max(2);
68    let radius = size / 2;
69    CssPropertyWithConditionsVec::from_vec(alloc::vec![
70        // Hug its own size inside a flex parent rather than stretch/grow.
71        CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
72        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
73            0,
74        ))),
75        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(size))),
76        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(size))),
77        // border: <border_width>px solid — three sides track, top accent.
78        CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
79            LayoutBorderTopWidth::const_px(border_width),
80        )),
81        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
82            LayoutBorderBottomWidth::const_px(border_width),
83        )),
84        CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
85            LayoutBorderLeftWidth::const_px(border_width),
86        )),
87        CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
88            LayoutBorderRightWidth::const_px(border_width),
89        )),
90        CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
91            inner: BorderStyle::Solid,
92        })),
93        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
94            StyleBorderBottomStyle {
95                inner: BorderStyle::Solid,
96            },
97        )),
98        CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
99            inner: BorderStyle::Solid,
100        })),
101        CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
102            StyleBorderRightStyle {
103                inner: BorderStyle::Solid,
104            },
105        )),
106        // top = accent (the visible "arc"); other three = faint track.
107        CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
108            inner: color,
109        })),
110        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
111            StyleBorderBottomColor { inner: track_color },
112        )),
113        CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
114            inner: track_color,
115        })),
116        CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
117            StyleBorderRightColor { inner: track_color },
118        )),
119        // border-radius: size/2 → a circle.
120        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
121            StyleBorderTopLeftRadius::const_px(radius),
122        )),
123        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
124            StyleBorderTopRightRadius::const_px(radius),
125        )),
126        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
127            StyleBorderBottomLeftRadius::const_px(radius),
128        )),
129        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
130            StyleBorderBottomRightRadius::const_px(radius),
131        )),
132    ])
133}
134
135impl Spinner {
136    /// Creates a new spinner with the default size (24px) and accent colour.
137    #[inline]
138    #[must_use] pub fn create() -> Self {
139        Self::with_size(DEFAULT_SIZE)
140    }
141
142    /// Creates a new spinner with the given diameter (logical px) and the
143    /// default colours.
144    #[inline]
145    #[must_use] pub fn with_size(size: isize) -> Self {
146        Self {
147            size,
148            color: DEFAULT_ACCENT_COLOR,
149            track_color: DEFAULT_TRACK_COLOR,
150            spinner_style: build_spinner_style(size, DEFAULT_ACCENT_COLOR, DEFAULT_TRACK_COLOR),
151        }
152    }
153
154    /// Sets the ring diameter (logical px), recomputing the style.
155    #[inline]
156    pub fn set_size(&mut self, size: isize) {
157        self.size = size;
158        self.spinner_style = build_spinner_style(size, self.color, self.track_color);
159    }
160
161    /// Builder-style setter for the ring diameter.
162    #[inline]
163    #[must_use] pub fn with_spinner_size(mut self, size: isize) -> Self {
164        self.set_size(size);
165        self
166    }
167
168    /// Sets the active-arc colour, recomputing the style.
169    #[inline]
170    pub fn set_color(&mut self, color: ColorU) {
171        self.color = color;
172        self.spinner_style = build_spinner_style(self.size, color, self.track_color);
173    }
174
175    /// Builder-style setter for the active-arc colour.
176    #[inline]
177    #[must_use] pub fn with_color(mut self, color: ColorU) -> Self {
178        self.set_color(color);
179        self
180    }
181
182    /// Sets the inactive "track" colour, recomputing the style.
183    #[inline]
184    pub fn set_track_color(&mut self, track_color: ColorU) {
185        self.track_color = track_color;
186        self.spinner_style = build_spinner_style(self.size, self.color, track_color);
187    }
188
189    /// Builder-style setter for the inactive "track" colour.
190    #[inline]
191    #[must_use] pub fn with_track_color(mut self, track_color: ColorU) -> Self {
192        self.set_track_color(track_color);
193        self
194    }
195
196    /// Replaces `self` with a default spinner and returns the original.
197    #[inline]
198    #[must_use] pub fn swap_with_default(&mut self) -> Self {
199        let mut s = Self::create();
200        core::mem::swap(&mut s, self);
201        s
202    }
203
204    /// Converts this spinner into a single DOM node with the
205    /// `__azul-native-spinner` class.
206    #[inline]
207    #[must_use] pub fn dom(self) -> Dom {
208        Dom::create_div()
209            .with_ids_and_classes(IdOrClassVec::from_const_slice(SPINNER_CLASS))
210            .with_css_props(self.spinner_style)
211    }
212}
213
214impl Default for Spinner {
215    fn default() -> Self {
216        Self::create()
217    }
218}
219
220impl From<Spinner> for Dom {
221    fn from(s: Spinner) -> Self {
222        s.dom()
223    }
224}