Skip to main content

freya_components/
skeleton.rs

1use std::time::Duration;
2
3use freya_animation::prelude::*;
4use freya_core::prelude::*;
5use torin::{
6    position::Position,
7    size::Size,
8};
9
10use crate::{
11    define_theme,
12    get_theme,
13    theming::{
14        component_themes::ColorsSheet,
15        macros::{
16            Preference,
17            ResolvablePreference,
18        },
19    },
20};
21
22/// Animation style for the skeleton placeholder.
23#[derive(PartialEq, Clone, Copy, Default, Debug)]
24pub enum SkeletonAnimation {
25    #[default]
26    Pulse,
27    Shimmer,
28}
29
30impl ResolvablePreference<SkeletonAnimation> for Preference<SkeletonAnimation> {
31    fn resolve(&self, _: &ColorsSheet) -> SkeletonAnimation {
32        match self {
33            Self::Reference(_) => panic!("Only Colors support references."),
34            Self::Specific(v) => *v,
35        }
36    }
37}
38
39define_theme! {
40    %[component]
41    pub Skeleton {
42        %[fields]
43        background: Color,
44        shimmer_color: Color,
45        duration: Duration,
46        animation: SkeletonAnimation,
47        corner_radius: CornerRadius,
48        shimmer_from: f32,
49        shimmer_to: f32,
50        shimmer_width: f32,
51    }
52}
53
54/// Skeleton loading placeholder with a configurable theme.
55///
56/// # Example
57///
58/// ```rust,no_run
59/// # use freya::prelude::*;
60/// # use std::time::Duration;
61/// fn app() -> impl IntoElement {
62///     let loading = use_state(|| true);
63///     Skeleton::new(*loading.read())
64///         .width(Size::px(200.))
65///         .height(Size::px(80.))
66///         .animation(SkeletonAnimation::Shimmer)
67///         .duration(Duration::from_millis(1200))
68///         .child("Some content")
69/// }
70/// ```
71#[derive(PartialEq)]
72pub struct Skeleton {
73    pub(crate) theme: Option<SkeletonThemePartial>,
74    loading: bool,
75    elements: Vec<Element>,
76    layout: LayoutData,
77    key: DiffKey,
78}
79
80impl KeyExt for Skeleton {
81    fn write_key(&mut self) -> &mut DiffKey {
82        &mut self.key
83    }
84}
85
86impl ChildrenExt for Skeleton {
87    fn get_children(&mut self) -> &mut Vec<Element> {
88        &mut self.elements
89    }
90}
91
92impl LayoutExt for Skeleton {
93    fn get_layout(&mut self) -> &mut LayoutData {
94        &mut self.layout
95    }
96}
97
98impl ContainerExt for Skeleton {}
99
100impl Default for Skeleton {
101    fn default() -> Self {
102        Self::new(false)
103    }
104}
105
106impl Skeleton {
107    pub fn new(loading: bool) -> Self {
108        Self {
109            theme: None,
110            loading,
111            elements: Vec::new(),
112            layout: LayoutData::default(),
113            key: DiffKey::None,
114        }
115    }
116
117    /// Override the full theme partial at once.
118    pub fn theme(mut self, theme: SkeletonThemePartial) -> Self {
119        self.theme = Some(theme);
120        self
121    }
122}
123
124impl Component for Skeleton {
125    fn render(&self) -> impl IntoElement {
126        let loading = self.loading;
127        let elements = self.elements.clone();
128
129        let theme = get_theme!(&self.theme, SkeletonThemePreference, "skeleton");
130
131        let animation = use_animation_with_dependencies(&theme, |conf, theme| {
132            conf.on_creation(OnCreation::Run);
133            conf.on_change(OnChange::Rerun);
134            match theme.animation {
135                SkeletonAnimation::Pulse => {
136                    conf.on_finish(OnFinish::reverse());
137                    AnimNum::new(0.4, 1.0).duration(theme.duration)
138                }
139                SkeletonAnimation::Shimmer => {
140                    conf.on_finish(OnFinish::restart());
141                    AnimNum::new(theme.shimmer_from, theme.shimmer_to).duration(theme.duration)
142                }
143            }
144        });
145
146        let value = animation.get().value();
147        let is_pulse = theme.animation == SkeletonAnimation::Pulse;
148
149        rect()
150            .layout(self.layout.clone())
151            .maybe(loading, |el| {
152                el.background(theme.background)
153                    .corner_radius(theme.corner_radius)
154                    .overflow(Overflow::Clip)
155                    .maybe(is_pulse, |el| el.opacity(value))
156                    .maybe(!is_pulse, |el| {
157                        el.child(
158                            rect()
159                                .position(Position::new_absolute().left(value))
160                                .width(Size::px(theme.shimmer_width))
161                                .height(Size::fill())
162                                .background_linear_gradient(
163                                    LinearGradient::new()
164                                        .angle(-90.)
165                                        .stop((theme.background, 0.))
166                                        .stop((theme.shimmer_color, 50.))
167                                        .stop((theme.background, 100.)),
168                                ),
169                        )
170                    })
171            })
172            .maybe(!loading, |el| el.children(elements))
173    }
174
175    fn render_key(&self) -> DiffKey {
176        self.key.clone().or(self.default_key())
177    }
178}