use std::ops::{Deref, DerefMut};
use blinc_animation::SharedAnimatedTimeline;
use blinc_layout::div::{Div, ElementBuilder, ElementTypeId};
use blinc_layout::prelude::*;
use blinc_theme::{ColorToken, RadiusToken, ThemeState};
pub struct Skeleton {
inner: Div,
}
impl Skeleton {
pub fn new() -> Self {
let theme = ThemeState::get();
let bg = theme.color(ColorToken::SurfaceElevated);
let radius = theme.radius(RadiusToken::Default);
let inner = div().class("cn-skeleton").bg(bg).rounded(radius);
Self { inner }
}
pub fn circle(size: f32) -> Self {
let theme = ThemeState::get();
let bg = theme.color(ColorToken::SurfaceElevated);
let inner = div()
.class("cn-skeleton")
.bg(bg)
.w(size)
.h(size)
.rounded(theme.radius(RadiusToken::Full));
Self { inner }
}
pub fn w(mut self, width: f32) -> Self {
self.inner = self.inner.w(width);
self
}
pub fn h(mut self, height: f32) -> Self {
self.inner = self.inner.h(height);
self
}
pub fn w_full(mut self) -> Self {
self.inner = self.inner.w_full();
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.inner = self.inner.rounded(radius);
self
}
pub fn shimmer(self, timeline: SharedAnimatedTimeline) -> AnimatedSkeleton {
AnimatedSkeleton::new(self, timeline)
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
pub struct AnimatedSkeleton {
skeleton: Skeleton,
timeline: SharedAnimatedTimeline,
duration_ms: u32,
min_opacity: f32,
max_opacity: f32,
}
impl AnimatedSkeleton {
fn new(skeleton: Skeleton, timeline: SharedAnimatedTimeline) -> Self {
Self {
skeleton,
timeline,
duration_ms: 1500, min_opacity: 0.4,
max_opacity: 1.0,
}
}
pub fn duration_ms(mut self, duration: u32) -> Self {
self.duration_ms = duration;
self
}
pub fn min_opacity(mut self, opacity: f32) -> Self {
self.min_opacity = opacity.clamp(0.0, 1.0);
self
}
pub fn max_opacity(mut self, opacity: f32) -> Self {
self.max_opacity = opacity.clamp(0.0, 1.0);
self
}
}
impl ElementBuilder for AnimatedSkeleton {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
let half_duration = self.duration_ms / 2;
let (entry1, entry2) = self.timeline.lock().unwrap().configure(|t| {
let id1 = t.add(0, half_duration, 0.0, 1.0); let id2 = t.add(half_duration as i32, half_duration, 1.0, 0.0); t.set_loop(-1); t.start();
(id1, id2)
});
let timeline = self.timeline.clone();
let min_opacity = self.min_opacity;
let max_opacity = self.max_opacity;
let theme = ThemeState::get();
let bg_color = theme.color(ColorToken::SurfaceElevated);
let radius = theme.radius(RadiusToken::Default);
use blinc_core::{Brush, CornerRadius, DrawContext, Rect};
use blinc_layout::canvas::{canvas, CanvasBounds};
let skeleton_style = self
.skeleton
.inner
.layout_style()
.cloned()
.unwrap_or_default();
let width = match skeleton_style.size.width {
taffy::Dimension::Length(l) => Some(l),
_ => None,
};
let height = match skeleton_style.size.height {
taffy::Dimension::Length(l) => Some(l),
_ => None,
};
let is_full_width =
matches!(skeleton_style.size.width, taffy::Dimension::Percent(p) if p >= 0.99);
let mut canvas_builder = canvas(move |ctx: &mut dyn DrawContext, bounds: CanvasBounds| {
let t1 = timeline.lock().unwrap().get(entry1).unwrap_or(0.0);
let t2 = timeline.lock().unwrap().get(entry2).unwrap_or(0.0);
let t_value = if t1 > 0.0 { t1 } else { t2 };
let opacity = min_opacity + t_value * (max_opacity - min_opacity);
let color = bg_color.with_alpha(opacity);
ctx.fill_rect(
Rect::new(0.0, 0.0, bounds.width, bounds.height),
CornerRadius::uniform(radius),
Brush::Solid(color),
);
});
if let Some(w) = width {
canvas_builder = canvas_builder.w(w);
}
if let Some(h) = height {
canvas_builder = canvas_builder.h(h);
}
if is_full_width {
canvas_builder = canvas_builder.w_full();
}
canvas_builder.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
blinc_layout::element::RenderProps::default()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
&[]
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Div
}
}
impl Default for Skeleton {
fn default() -> Self {
Self::new()
}
}
impl Deref for Skeleton {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Skeleton {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for Skeleton {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
ElementBuilder::layout_style(&self.inner)
}
fn element_type_id(&self) -> ElementTypeId {
ElementBuilder::element_type_id(&self.inner)
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub fn skeleton() -> Skeleton {
Skeleton::new()
}
pub fn skeleton_circle(size: f32) -> Skeleton {
Skeleton::circle(size)
}
#[cfg(test)]
mod tests {
use super::*;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_skeleton_default() {
init_theme();
let _ = skeleton();
}
#[test]
fn test_skeleton_sized() {
init_theme();
let _ = skeleton().h(20.0).w(200.0);
}
#[test]
fn test_skeleton_circle() {
init_theme();
let _ = skeleton_circle(48.0);
}
}