Skip to main content

frus_widgets/
skeleton.rs

1//! [`Skeleton`]: a loading placeholder whose intensity **pulses** over time, a
2//! shimmer. It reuses the continuous clock (`Status::time` plus `continuous`).
3
4use frus_core::{Color, Rect, Scene};
5use frus_layout::{Dimension, Style};
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11/// Vitesse de pulsation (radians/seconde).
12const SPEED: f32 = 2.5;
13
14/// An animated loading block.
15pub struct Skeleton {
16    width: Dimension,
17    height: f32,
18    radius: f32,
19}
20
21impl Skeleton {
22    /// Creates a placeholder: full width, and 16 tall by default.
23    pub fn new() -> Self {
24        Self {
25            width: Dimension::Auto,
26            height: 16.0,
27            radius: 6.0,
28        }
29    }
30
31    /// Sets the width, in logical pixels.
32    pub fn width(mut self, width: f32) -> Self {
33        self.width = Dimension::Length(width);
34        self
35    }
36
37    /// Sets the height, in logical pixels.
38    pub fn height(mut self, height: f32) -> Self {
39        self.height = height;
40        self
41    }
42
43    /// Sets the corner radius.
44    pub fn radius(mut self, radius: f32) -> Self {
45        self.radius = radius;
46        self
47    }
48}
49
50impl Default for Skeleton {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl<Msg> Widget<Msg> for Skeleton {
57    fn style(&self) -> Style {
58        Style {
59            width: self.width,
60            height: Dimension::Length(self.height),
61            ..Default::default()
62        }
63    }
64
65    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
66        &[]
67    }
68
69    fn continuous(&self) -> bool {
70        true
71    }
72
73    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
74        let o = status.opacity;
75        // A `0..1` pulse driven by time.
76        let pulse = 0.5 + 0.5 * (status.time * SPEED).sin();
77        let alpha = (0.18 + 0.22 * pulse) * o;
78        scene.draw_rect(
79            bounds,
80            theme.muted.fade(alpha),
81            self.radius,
82            0.0,
83            Color::TRANSPARENT,
84        );
85    }
86
87    fn on_click(&self) -> Option<Msg> {
88        None
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use frus_core::Primitive;
96
97    fn alpha_at(time: f32) -> f32 {
98        let sk = Skeleton::new().width(120.0);
99        let status = Status {
100            time,
101            ..Default::default()
102        };
103        let mut scene = Scene::new();
104        Widget::<()>::paint(
105            &sk,
106            Rect::new(0.0, 0.0, 120.0, 16.0),
107            status,
108            &Theme::default(),
109            &mut scene,
110        );
111        match scene.primitives()[0] {
112            Primitive::Rect { color, .. } => color.a,
113            _ => panic!("expected a rect"),
114        }
115    }
116
117    #[test]
118    fn is_continuous() {
119        assert!(Widget::<()>::continuous(&Skeleton::new()));
120    }
121
122    #[test]
123    fn intensity_varies_with_time() {
124        // Two distinct instants of the pulse give different opacities.
125        assert_ne!(alpha_at(0.0), alpha_at(0.6));
126    }
127}