rlvgl_core/
animation.rs

1//! Animation primitives such as fades and slides.
2//!
3//! These helpers mirror LVGL's animation system while using safe Rust
4//! abstractions where possible.
5
6use crate::style::Style;
7use crate::widget::{Color, Rect};
8
9/// Simple linear fade animation for a style's background color.
10///
11/// The animation owns a mutable pointer to the [`Style`] being modified. This
12/// keeps the API lightweight for `no_std` targets at the cost of requiring
13/// unsafe access internally.
14pub struct Fade {
15    style: *mut Style,
16    start: Color,
17    end: Color,
18    duration_ms: u32,
19    elapsed: u32,
20}
21
22impl Fade {
23    /// Create a new fade animation.
24    pub fn new(style: &mut Style, start: Color, end: Color, duration_ms: u32) -> Self {
25        Self {
26            style: style as *mut Style,
27            start,
28            end,
29            duration_ms,
30            elapsed: 0,
31        }
32    }
33
34    /// Advance the animation by `delta_ms` milliseconds.
35    pub fn tick(&mut self, delta_ms: u32) {
36        self.elapsed = core::cmp::min(self.elapsed + delta_ms, self.duration_ms);
37        let progress = self.elapsed as f32 / self.duration_ms as f32;
38        let lerp = |a: u8, b: u8| a as f32 + (b as f32 - a as f32) * progress;
39        unsafe {
40            (*self.style).bg_color = Color(
41                lerp(self.start.0, self.end.0) as u8,
42                lerp(self.start.1, self.end.1) as u8,
43                lerp(self.start.2, self.end.2) as u8,
44                lerp(self.start.3, self.end.3) as u8,
45            );
46        }
47    }
48
49    /// Returns `true` when the animation has reached its end point.
50    pub fn finished(&self) -> bool {
51        self.elapsed >= self.duration_ms
52    }
53}
54
55/// Simple linear slide animation for a [`Rect`].
56pub struct Slide {
57    rect: *mut Rect,
58    start: Rect,
59    end: Rect,
60    duration_ms: u32,
61    elapsed: u32,
62}
63
64impl Slide {
65    /// Create a new slide animation.
66    pub fn new(rect: &mut Rect, start: Rect, end: Rect, duration_ms: u32) -> Self {
67        Self {
68            rect: rect as *mut Rect,
69            start,
70            end,
71            duration_ms,
72            elapsed: 0,
73        }
74    }
75
76    /// Advance the animation by `delta_ms` milliseconds.
77    pub fn tick(&mut self, delta_ms: u32) {
78        self.elapsed = core::cmp::min(self.elapsed + delta_ms, self.duration_ms);
79        let p = self.elapsed as f32 / self.duration_ms as f32;
80        let lerp = |a: i32, b: i32| a as f32 + (b as f32 - a as f32) * p;
81        unsafe {
82            *self.rect = Rect {
83                x: lerp(self.start.x, self.end.x) as i32,
84                y: lerp(self.start.y, self.end.y) as i32,
85                width: lerp(self.start.width, self.end.width) as i32,
86                height: lerp(self.start.height, self.end.height) as i32,
87            };
88        }
89    }
90
91    /// Returns `true` once the slide has finished.
92    pub fn finished(&self) -> bool {
93        self.elapsed >= self.duration_ms
94    }
95}
96
97/// Animation timeline that updates multiple animations at once.
98pub struct Timeline {
99    fades: alloc::vec::Vec<Fade>,
100    slides: alloc::vec::Vec<Slide>,
101}
102
103impl Timeline {
104    /// Create an empty timeline.
105    pub fn new() -> Self {
106        Self {
107            fades: alloc::vec::Vec::new(),
108            slides: alloc::vec::Vec::new(),
109        }
110    }
111
112    /// Add a [`Fade`] animation to the timeline.
113    pub fn add_fade(&mut self, fade: Fade) {
114        self.fades.push(fade);
115    }
116    /// Add a [`Slide`] animation to the timeline.
117    pub fn add_slide(&mut self, slide: Slide) {
118        self.slides.push(slide);
119    }
120
121    /// Advance all animations by `delta_ms` milliseconds.
122    pub fn tick(&mut self, delta_ms: u32) {
123        for fade in &mut self.fades {
124            fade.tick(delta_ms);
125        }
126        for slide in &mut self.slides {
127            slide.tick(delta_ms);
128        }
129        self.fades.retain(|f| !f.finished());
130        self.slides.retain(|s| !s.finished());
131    }
132
133    /// Returns `true` if no animations remain in the timeline.
134    pub fn is_empty(&self) -> bool {
135        self.fades.is_empty() && self.slides.is_empty()
136    }
137}
138
139impl Default for Timeline {
140    fn default() -> Self {
141        Self::new()
142    }
143}