1use rosace_core::types::{Point, Rect, Size};
2use rosace_render::{Color, DrawCommand};
3use super::{Widget, LayoutCtx, PaintCtx};
4
5pub struct Skeleton {
8 width: Option<f32>,
9 height: f32,
10 radius: f32,
11 color: Color,
15 vertical: bool,
17 is_circle: bool,
21}
22
23impl Skeleton {
24 pub fn new() -> Self { Self { width: None, height: 16.0, radius: 6.0, color: Color::rgb(255, 255, 255), vertical: false, is_circle: false } }
25 pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
26 pub fn height(mut self, h: f32) -> Self { self.height = h; self }
27 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
28 pub fn color(mut self, c: Color) -> Self { self.color = c; self }
30 pub fn vertical(mut self, v: bool) -> Self { self.vertical = v; self }
32 pub fn circle(size: f32) -> Self { Self { width: Some(size), height: size, radius: size / 2.0, color: Color::rgb(255, 255, 255), vertical: false, is_circle: true } }
34}
35
36impl Default for Skeleton { fn default() -> Self { Self::new() } }
37
38impl Widget for Skeleton {
39 fn layout(&self, ctx: &LayoutCtx) -> Size {
40 let w = self.width.unwrap_or_else(|| super::avail_w(ctx.constraints));
41 ctx.constraints.constrain(Size { width: w, height: self.height })
42 }
43
44 fn paint(&self, ctx: &mut PaintCtx) {
45 let r = ctx.rect;
46 let (cr, cg, cb) = (self.color.r, self.color.g, self.color.b);
47 let base = Color::rgba(cr, cg, cb, 30);
48 let hi = Color::rgba(cr, cg, cb, 95);
49
50 ctx.fill_rrect(r, self.radius, base);
52
53 let phase = (super::anim_clock() / 1.3).fract(); if self.is_circle {
56 let pulse = 1.0 - (phase * 2.0 - 1.0).abs(); let alpha = (hi.a as f32 * pulse) as u8;
63 ctx.fill_rrect(r, self.radius, Color::rgba(cr, cg, cb, alpha));
64 } else {
65 let clear = Color::rgba(cr, cg, cb, 0);
66 ctx.record(DrawCommand::PushClip { rect: r });
68 if self.vertical {
69 let bh = (r.size.height * 0.35).max(24.0);
70 let y = r.origin.y - bh + (r.size.height + bh) * phase; let half = bh / 2.0;
72 ctx.fill_gradient(
73 Rect { origin: Point { x: r.origin.x, y }, size: Size { width: r.size.width, height: half } },
74 0.0, clear, hi, true);
75 ctx.fill_gradient(
76 Rect { origin: Point { x: r.origin.x, y: y + half }, size: Size { width: r.size.width, height: half } },
77 0.0, hi, clear, true);
78 } else {
79 let bw = (r.size.width * 0.35).max(24.0);
80 let x = r.origin.x - bw + (r.size.width + bw) * phase; let half = bw / 2.0;
82 ctx.fill_gradient(
84 Rect { origin: Point { x, y: r.origin.y }, size: Size { width: half, height: r.size.height } },
85 0.0, clear, hi, false);
86 ctx.fill_gradient(
87 Rect { origin: Point { x: x + half, y: r.origin.y }, size: Size { width: half, height: r.size.height } },
88 0.0, hi, clear, false);
89 }
90 ctx.record(DrawCommand::PopClip);
91 }
92 ctx.request_animation();
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use rosace_layout::Constraints;
100
101 #[test]
102 fn color_builder_does_not_change_layout_size() {
103 let font = rosace_render::FontCache::embedded();
104 let theme = rosace_theme::built_in::dark_theme();
105 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
106 let base = Skeleton::new().width(100.0).height(20.0);
107 let customized = Skeleton::new().width(100.0).height(20.0).color(Color::rgb(200, 50, 50));
108 assert_eq!(base.layout(&ctx), customized.layout(&ctx));
109 }
110}