use alloc::boxed::Box;
use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::image::{BlitOpts, ImageDescriptor};
use rlvgl_core::renderer::Renderer;
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Rect, Widget};
pub enum FrameSource {
Decoded(Vec<ImageDescriptor<'static>>),
Static(&'static [ImageDescriptor<'static>]),
}
impl FrameSource {
pub fn frame_count(&self) -> usize {
match self {
FrameSource::Decoded(v) => v.len(),
FrameSource::Static(s) => s.len(),
}
}
pub fn get(&self, index: usize) -> Option<&ImageDescriptor<'static>> {
match self {
FrameSource::Decoded(v) => v.get(index),
FrameSource::Static(s) => s.get(index),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnimPlayState {
Running,
Paused,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnimImageLoopMode {
Loop,
Once,
Bounce,
}
pub struct AnimImage {
bounds: Rect,
pub style: Style,
frames: FrameSource,
ticks_per_frame: u32,
frame_tick: u32,
frame_index: usize,
play_state: AnimPlayState,
loop_mode: AnimImageLoopMode,
reverse: bool,
bounce_forward: bool,
on_complete: Option<Box<dyn FnMut()>>,
}
impl AnimImage {
pub fn new(bounds: Rect, frames: FrameSource) -> Self {
Self {
bounds,
style: Style::default(),
frames,
ticks_per_frame: 3,
frame_tick: 0,
frame_index: 0,
play_state: AnimPlayState::Running,
loop_mode: AnimImageLoopMode::Loop,
reverse: false,
bounce_forward: true,
on_complete: None,
}
}
pub fn set_ticks_per_frame(&mut self, n: u32) {
self.ticks_per_frame = n.max(1);
self.frame_tick = self.frame_tick.min(self.ticks_per_frame - 1);
}
pub fn ticks_per_frame(&self) -> u32 {
self.ticks_per_frame
}
pub fn frame_count(&self) -> usize {
self.frames.frame_count()
}
pub fn current_frame_index(&self) -> usize {
self.frame_index
}
pub fn set_current_frame(&mut self, index: usize) {
let count = self.frames.frame_count();
self.frame_index = if count == 0 { 0 } else { index.min(count - 1) };
}
pub fn play(&mut self) {
self.play_state = AnimPlayState::Running;
}
pub fn pause(&mut self) {
self.play_state = AnimPlayState::Paused;
}
pub fn set_play_state(&mut self, state: AnimPlayState) {
self.play_state = state;
}
pub fn play_state(&self) -> AnimPlayState {
self.play_state
}
pub fn is_playing(&self) -> bool {
self.play_state == AnimPlayState::Running
}
pub fn set_loop_mode(&mut self, mode: AnimImageLoopMode) {
self.loop_mode = mode;
}
pub fn loop_mode(&self) -> AnimImageLoopMode {
self.loop_mode
}
pub fn set_reverse(&mut self, reverse: bool) {
self.reverse = reverse;
self.bounce_forward = !reverse;
}
pub fn on_complete<F: FnMut() + 'static>(mut self, handler: F) -> Self {
self.on_complete = Some(Box::new(handler));
self
}
fn advance_tick(&mut self) -> bool {
let count = self.frames.frame_count();
if count == 0 {
return false;
}
self.frame_tick += 1;
if self.frame_tick < self.ticks_per_frame {
return false; }
self.frame_tick = 0;
let prev = self.frame_index;
let forward = if self.loop_mode == AnimImageLoopMode::Bounce {
self.bounce_forward
} else {
!self.reverse
};
if forward {
if self.frame_index + 1 < count {
self.frame_index += 1;
} else {
match self.loop_mode {
AnimImageLoopMode::Loop => {
self.frame_index = 0;
}
AnimImageLoopMode::Once => {
self.play_state = AnimPlayState::Paused;
if let Some(cb) = &mut self.on_complete {
cb();
}
}
AnimImageLoopMode::Bounce => {
self.bounce_forward = false;
if count >= 2 {
self.frame_index = count - 2;
}
}
}
}
} else if self.frame_index > 0 {
self.frame_index -= 1;
} else {
match self.loop_mode {
AnimImageLoopMode::Loop => {
self.frame_index = count - 1;
}
AnimImageLoopMode::Once => {
self.play_state = AnimPlayState::Paused;
if let Some(cb) = &mut self.on_complete {
cb();
}
}
AnimImageLoopMode::Bounce => {
self.bounce_forward = true;
if count >= 2 {
self.frame_index = 1;
}
}
}
}
self.frame_index != prev
}
}
impl Widget for AnimImage {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
if let Some(frame) = self.frames.get(self.frame_index) {
renderer.blit_image(self.bounds, frame, &BlitOpts::default());
}
}
fn handle_event(&mut self, event: &Event) -> bool {
if matches!(event, Event::Tick) && self.play_state == AnimPlayState::Running {
self.advance_tick();
return true;
}
false
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
}
#[cfg(test)]
mod tests {
use super::*;
use rlvgl_core::image::{ImageData, PixelFormat};
use rlvgl_core::widget::Color;
fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
fn make_frames(n: usize) -> FrameSource {
let mut frames: Vec<ImageDescriptor<'static>> = Vec::new();
for _ in 0..n {
frames.push(ImageDescriptor::new(
PixelFormat::Argb8888,
1,
1,
ImageData::Borrowed(&[]),
None,
));
}
FrameSource::Decoded(frames)
}
fn tick(anim: &mut AnimImage) -> bool {
anim.handle_event(&Event::Tick)
}
#[test]
fn tick_advances_frame_at_right_cadence() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(4));
a.set_ticks_per_frame(3);
assert!(tick(&mut a)); assert_eq!(a.current_frame_index(), 0);
assert!(tick(&mut a));
assert_eq!(a.current_frame_index(), 0);
assert!(tick(&mut a));
assert_eq!(a.current_frame_index(), 1);
}
#[test]
fn zero_period_is_clamped_to_one() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3));
a.set_ticks_per_frame(0);
assert_eq!(a.ticks_per_frame(), 1);
tick(&mut a);
assert_eq!(a.current_frame_index(), 1, "advances every tick");
}
#[test]
fn loop_mode_wraps_at_last_frame() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3));
a.set_ticks_per_frame(1);
a.set_loop_mode(AnimImageLoopMode::Loop);
tick(&mut a); tick(&mut a); tick(&mut a); assert_eq!(a.current_frame_index(), 0);
assert!(a.is_playing());
}
#[test]
fn once_mode_stops_at_last_frame() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3));
a.set_ticks_per_frame(1);
a.set_loop_mode(AnimImageLoopMode::Once);
tick(&mut a); tick(&mut a); tick(&mut a); assert_eq!(a.current_frame_index(), 2);
assert_eq!(a.play_state(), AnimPlayState::Paused);
}
#[test]
fn once_mode_fires_on_complete() {
use alloc::rc::Rc;
use core::cell::Cell;
let fired = Rc::new(Cell::new(0u32));
let fired_clone = fired.clone();
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3))
.on_complete(move || fired_clone.set(fired_clone.get() + 1));
a.set_ticks_per_frame(1);
a.set_loop_mode(AnimImageLoopMode::Once);
tick(&mut a); tick(&mut a); assert_eq!(a.current_frame_index(), 2);
assert_eq!(
fired.get(),
0,
"on_complete not fired until attempt to advance past last"
);
tick(&mut a); assert_eq!(fired.get(), 1, "on_complete fired exactly once");
assert_eq!(a.play_state(), AnimPlayState::Paused);
}
#[test]
fn bounce_mode_reverses_at_endpoints() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3));
a.set_ticks_per_frame(1);
a.set_loop_mode(AnimImageLoopMode::Bounce);
let expected = [1, 2, 1, 0, 1];
for &exp in &expected {
tick(&mut a);
assert_eq!(a.current_frame_index(), exp, "expected frame {exp}");
}
}
#[test]
fn paused_does_not_consume_tick() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(4));
a.pause();
let consumed = tick(&mut a);
assert!(!consumed, "Tick not consumed when Paused");
assert_eq!(a.current_frame_index(), 0, "frame unchanged while paused");
}
#[test]
fn play_resume_from_paused() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(4));
a.set_ticks_per_frame(1);
a.pause();
tick(&mut a);
assert_eq!(a.current_frame_index(), 0);
a.play();
tick(&mut a);
assert_eq!(a.current_frame_index(), 1);
}
#[test]
fn reverse_starts_at_last_frame_going_backward() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(3));
a.set_ticks_per_frame(1);
a.set_reverse(true);
tick(&mut a);
assert_eq!(a.current_frame_index(), 2, "loop backward wraps to last");
}
#[test]
fn set_bounds_repositions() {
let mut a = AnimImage::new(rect(0, 0, 50, 50), make_frames(2));
a.set_bounds(rect(5, 10, 100, 80));
assert_eq!(a.bounds(), rect(5, 10, 100, 80));
}
#[test]
fn set_current_frame_clamps_to_valid_range() {
let mut a = AnimImage::new(rect(0, 0, 10, 10), make_frames(4));
a.set_current_frame(100);
assert_eq!(a.current_frame_index(), 3);
a.set_current_frame(0);
assert_eq!(a.current_frame_index(), 0);
}
#[test]
fn draw_calls_blit_image_for_current_frame() {
struct BlitCatcher(bool);
impl Renderer for BlitCatcher {
fn fill_rect(&mut self, _r: Rect, _c: Color) {}
fn draw_text(&mut self, _p: (i32, i32), _t: &str, _c: Color) {}
fn blit_image(&mut self, _dest: Rect, _desc: &ImageDescriptor<'_>, _opts: &BlitOpts) {
self.0 = true;
}
}
let a = AnimImage::new(rect(0, 0, 10, 10), make_frames(1));
let mut r = BlitCatcher(false);
a.draw(&mut r);
assert!(
r.0,
"blit_image should be called for non-empty frame source"
);
}
}