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 ContainerWithContentExt for Skeleton {}
101
102impl Default for Skeleton {
103    fn default() -> Self {
104        Self::new(false)
105    }
106}
107
108impl Skeleton {
109    pub fn new(loading: bool) -> Self {
110        Self {
111            theme: None,
112            loading,
113            elements: Vec::new(),
114            layout: LayoutData::default(),
115            key: DiffKey::None,
116        }
117    }
118
119    /// Override the full theme partial at once.
120    pub fn theme(mut self, theme: SkeletonThemePartial) -> Self {
121        self.theme = Some(theme);
122        self
123    }
124}
125
126impl Component for Skeleton {
127    fn render(&self) -> impl IntoElement {
128        let loading = self.loading;
129        let elements = self.elements.clone();
130
131        let theme = get_theme!(&self.theme, SkeletonThemePreference, "skeleton");
132
133        let animation = use_animation_with_dependencies(&theme, |conf, theme| {
134            conf.on_creation(OnCreation::Run);
135            conf.on_change(OnChange::Rerun);
136            match theme.animation {
137                SkeletonAnimation::Pulse => {
138                    conf.on_finish(OnFinish::reverse());
139                    AnimNum::new(0.4, 1.0).duration(theme.duration)
140                }
141                SkeletonAnimation::Shimmer => {
142                    conf.on_finish(OnFinish::restart());
143                    AnimNum::new(theme.shimmer_from, theme.shimmer_to).duration(theme.duration)
144                }
145            }
146        });
147
148        let value = animation.get().value();
149        let is_pulse = theme.animation == SkeletonAnimation::Pulse;
150
151        rect()
152            .layout(self.layout.clone())
153            .maybe(loading, |el| {
154                el.background(theme.background)
155                    .corner_radius(theme.corner_radius)
156                    .overflow(Overflow::Clip)
157                    .maybe(is_pulse, |el| el.opacity(value))
158                    .maybe(!is_pulse, |el| {
159                        el.child(
160                            rect()
161                                .position(Position::new_absolute().left(value))
162                                .width(Size::px(theme.shimmer_width))
163                                .height(Size::fill())
164                                .background(
165                                    LinearGradient::new()
166                                        .angle(-90.)
167                                        .stop((theme.background, 0.))
168                                        .stop((theme.shimmer_color, 50.))
169                                        .stop((theme.background, 100.)),
170                                ),
171                        )
172                    })
173            })
174            .maybe(!loading, |el| el.children(elements))
175    }
176
177    fn render_key(&self) -> DiffKey {
178        self.key.clone().or(self.default_key())
179    }
180}