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            );
45        }
46    }
47
48    /// Returns `true` when the animation has reached its end point.
49    pub fn finished(&self) -> bool {
50        self.elapsed >= self.duration_ms
51    }
52}
53
54/// Simple linear slide animation for a [`Rect`].
55pub struct Slide {
56    rect: *mut Rect,
57    start: Rect,
58    end: Rect,
59    duration_ms: u32,
60    elapsed: u32,
61}
62
63impl Slide {
64    /// Create a new slide animation.
65    pub fn new(rect: &mut Rect, start: Rect, end: Rect, duration_ms: u32) -> Self {
66        Self {
67            rect: rect as *mut Rect,
68            start,
69            end,
70            duration_ms,
71            elapsed: 0,
72        }
73    }
74
75    /// Advance the animation by `delta_ms` milliseconds.
76    pub fn tick(&mut self, delta_ms: u32) {
77        self.elapsed = core::cmp::min(self.elapsed + delta_ms, self.duration_ms);
78        let p = self.elapsed as f32 / self.duration_ms as f32;
79        let lerp = |a: i32, b: i32| a as f32 + (b as f32 - a as f32) * p;
80        unsafe {
81            *self.rect = Rect {
82                x: lerp(self.start.x, self.end.x) as i32,
83                y: lerp(self.start.y, self.end.y) as i32,
84                width: lerp(self.start.width, self.end.width) as i32,
85                height: lerp(self.start.height, self.end.height) as i32,
86            };
87        }
88    }
89
90    /// Returns `true` once the slide has finished.
91    pub fn finished(&self) -> bool {
92        self.elapsed >= self.duration_ms
93    }
94}
95
96/// Animation timeline that updates multiple animations at once.
97pub struct Timeline {
98    fades: alloc::vec::Vec<Fade>,
99    slides: alloc::vec::Vec<Slide>,
100}
101
102impl Timeline {
103    /// Create an empty timeline.
104    pub fn new() -> Self {
105        Self {
106            fades: alloc::vec::Vec::new(),
107            slides: alloc::vec::Vec::new(),
108        }
109    }
110
111    /// Add a [`Fade`] animation to the timeline.
112    pub fn add_fade(&mut self, fade: Fade) {
113        self.fades.push(fade);
114    }
115    /// Add a [`Slide`] animation to the timeline.
116    pub fn add_slide(&mut self, slide: Slide) {
117        self.slides.push(slide);
118    }
119
120    /// Advance all animations by `delta_ms` milliseconds.
121    pub fn tick(&mut self, delta_ms: u32) {
122        for fade in &mut self.fades {
123            fade.tick(delta_ms);
124        }
125        for slide in &mut self.slides {
126            slide.tick(delta_ms);
127        }
128        self.fades.retain(|f| !f.finished());
129        self.slides.retain(|s| !s.finished());
130    }
131
132    /// Returns `true` if no animations remain in the timeline.
133    pub fn is_empty(&self) -> bool {
134        self.fades.is_empty() && self.slides.is_empty()
135    }
136}
137
138impl Default for Timeline {
139    fn default() -> Self {
140        Self::new()
141    }
142}