From 331a04308a7c44af2d42f7438aa858df4bcc0d81 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ashraf Date: Thu, 16 Apr 2026 16:51:38 +0700 Subject: [PATCH 1/2] refactor: unify player state into single Status enum --- dotlottie-rs/examples/audio_player.rs | 21 ++-- dotlottie-rs/src/c_api/mod.rs | 37 ++----- dotlottie-rs/src/c_api/types.rs | 8 -- dotlottie-rs/src/player.rs | 106 +++++++++---------- dotlottie-rs/src/state_machine_engine/mod.rs | 4 +- dotlottie-rs/src/wasm/wasm_bindgen_api.rs | 39 ++++--- dotlottie-rs/tests/autoplay.rs | 10 +- dotlottie-rs/tests/markers.rs | 8 +- dotlottie-rs/tests/play.rs | 14 ++- dotlottie-rs/tests/play_mode.rs | 94 ++++++++++++---- dotlottie-rs/tests/segments.rs | 8 +- dotlottie-rs/tests/speed.rs | 8 +- dotlottie-rs/tests/state_machine.rs | 23 ++-- dotlottie-rs/tests/stop.rs | 22 ++-- dotlottie-rs/tests/theming.rs | 14 +-- dotlottie-rs/tests/tween.rs | 99 +++++++++++++++-- 16 files changed, 328 insertions(+), 187 deletions(-) diff --git a/dotlottie-rs/examples/audio_player.rs b/dotlottie-rs/examples/audio_player.rs index afe39b5b..220099b2 100644 --- a/dotlottie-rs/examples/audio_player.rs +++ b/dotlottie-rs/examples/audio_player.rs @@ -19,7 +19,7 @@ #![allow(clippy::print_stdout)] -use dotlottie_rs::{ColorSpace, DotLottieEvent, DotLottiePlayer}; +use dotlottie_rs::{ColorSpace, DotLottieEvent, DotLottiePlayer, Status}; use minifb::{Key, Window, WindowOptions}; use std::ffi::CString; use std::fs; @@ -134,13 +134,13 @@ fn main() { let plus_down = window.is_key_down(Key::Equal) || window.is_key_down(Key::NumPadPlus); let minus_down = window.is_key_down(Key::Minus) || window.is_key_down(Key::NumPadMinus); - if p_was_down && !p_down && (player.is_paused() || player.is_stopped()) { + if p_was_down && !p_down && player.status() != Status::Playing { let _ = player.play(); } - if s_was_down && !s_down && player.is_playing() { + if s_was_down && !s_down && player.status() == Status::Playing { let _ = player.pause(); } - if x_was_down && !x_down && !player.is_stopped() { + if x_was_down && !x_down && player.status() != Status::Stopped { let _ = player.stop(); } @@ -182,12 +182,11 @@ fn main() { } else { "" }; - let state = if player.is_playing() { - "PLAYING" - } else if player.is_paused() { - "PAUSED " - } else { - "STOPPED" + let state = match player.status() { + Status::Idle => "IDLE", + Status::Playing => "PLAYING", + Status::Paused => "PAUSED", + Status::Stopped => "STOPPED", }; let frame = player.current_frame(); let vol = player.audio_volume(); @@ -202,7 +201,7 @@ fn main() { while let Some(event) = player.poll_event() { let _ = player.current_frame(); match event { - DotLottieEvent::Load => println!(" -- Load (is_loaded={})", player.is_loaded()), + DotLottieEvent::Load => println!(" -- Load (status={:?})", player.status()), DotLottieEvent::LoadError => { eprintln!(" !! LoadError — animation failed to load into ThorVG"); } diff --git a/dotlottie-rs/src/c_api/mod.rs b/dotlottie-rs/src/c_api/mod.rs index 23aa57e6..4b07fc09 100644 --- a/dotlottie-rs/src/c_api/mod.rs +++ b/dotlottie-rs/src/c_api/mod.rs @@ -7,7 +7,7 @@ use crate::lottie_renderer::{ ColorSlot, ColorValue, GlContext, GlDisplay, GlSurface, ImageSlot, PositionSlot, ScalarSlot, ScalarValue, TextDocument, TextSlot, VectorSlot, WgpuDevice, WgpuInstance, WgpuTarget, }; -use crate::{DotLottiePlayer, DotLottiePlayerError, Layout, Mode, Rgba, Segment}; +use crate::{DotLottiePlayer, DotLottiePlayerError, Layout, Mode, Rgba, Segment, Status}; use crate::ColorSpace; @@ -716,38 +716,13 @@ pub unsafe extern "C" fn dotlottie_current_loop_count( }) } -/// Returns whether an animation is loaded. +/// Returns the current status (Idle, Playing, Paused, or Stopped). +/// Returns Idle if the pointer is invalid. #[no_mangle] -pub unsafe extern "C" fn dotlottie_is_loaded(ptr: *mut DotLottiePlayer) -> bool { +pub unsafe extern "C" fn dotlottie_status(ptr: *mut DotLottiePlayer) -> Status { match ptr.as_mut() { - Some(p) => p.is_loaded(), - _ => false, - } -} - -/// Returns the current playback status. -/// -/// Priority order: Playing > Paused > Stopped -/// -/// # Parameters -/// - `ptr`: Pointer to the DotLottiePlayer instance -/// -/// # Returns -/// The current PlaybackStatus (Playing, Paused, or Stopped) -/// Returns Stopped if the pointer is invalid -#[no_mangle] -pub unsafe extern "C" fn dotlottie_playback_status(ptr: *mut DotLottiePlayer) -> PlaybackStatus { - match ptr.as_mut() { - Some(p) => { - if p.is_playing() { - PlaybackStatus::Playing - } else if p.is_paused() { - PlaybackStatus::Paused - } else { - PlaybackStatus::Stopped - } - } - _ => PlaybackStatus::Stopped, + Some(p) => p.status(), + _ => Status::Idle, } } diff --git a/dotlottie-rs/src/c_api/types.rs b/dotlottie-rs/src/c_api/types.rs index 0284aa1f..0dbdc0b5 100644 --- a/dotlottie-rs/src/c_api/types.rs +++ b/dotlottie-rs/src/c_api/types.rs @@ -11,14 +11,6 @@ use core::str::FromStr; use crate::lottie_renderer::LottieRendererError; use crate::DotLottiePlayerError; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub enum PlaybackStatus { - Playing = 0, - Paused = 1, - Stopped = 2, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(C)] pub enum DotLottieResult { diff --git a/dotlottie-rs/src/player.rs b/dotlottie-rs/src/player.rs index 514eb5c8..00cab2b9 100644 --- a/dotlottie-rs/src/player.rs +++ b/dotlottie-rs/src/player.rs @@ -18,10 +18,14 @@ use crate::{DotLottieManager, Manifest}; #[cfg(feature = "state-machines")] use crate::{StateMachineEngine, StateMachineEngineError}; -pub enum PlaybackState { - Playing, - Paused, - Stopped, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub enum Status { + Idle = 0, + Playing = 1, + Paused = 2, + Stopped = 3, + Tweening = 4, } #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] @@ -57,8 +61,7 @@ pub enum CompletionEvent { pub struct DotLottiePlayer { pub(crate) renderer: Box, - playback_state: PlaybackState, - is_loaded: bool, + status: Status, elapsed_frames: f32, current_loop_count: u32, #[cfg(feature = "dotlottie")] @@ -69,7 +72,7 @@ pub struct DotLottiePlayer { active_marker: Option, event_queue: EventQueue, completion_event: CompletionEvent, - // Playback config properties + // Config properties mode: Mode, loop_animation: bool, loop_count: u32, @@ -78,6 +81,7 @@ pub struct DotLottiePlayer { autoplay: bool, layout: Layout, tween_state: Option, + resume_status: Status, #[cfg(feature = "theming")] theme_id: Option, #[cfg(feature = "dotlottie")] @@ -119,8 +123,7 @@ impl DotLottiePlayer { pub fn with_renderer(renderer: R) -> Self { DotLottiePlayer { renderer: ::new(renderer), - playback_state: PlaybackState::Stopped, - is_loaded: false, + status: Status::Idle, elapsed_frames: 0.0, current_loop_count: 0, mode: Mode::Forward, @@ -131,6 +134,7 @@ impl DotLottiePlayer { autoplay: false, layout: Layout::default(), tween_state: None, + resume_status: Status::Stopped, #[cfg(feature = "theming")] theme_id: None, #[cfg(feature = "dotlottie")] @@ -180,34 +184,20 @@ impl DotLottiePlayer { self.renderer.segment().map_or(0.0, |seg| seg.end) } - pub fn is_loaded(&self) -> bool { - self.is_loaded - } - - #[inline] - pub fn is_playing(&self) -> bool { - matches!(self.playback_state, PlaybackState::Playing) - } - #[inline] - pub fn is_paused(&self) -> bool { - matches!(self.playback_state, PlaybackState::Paused) - } - - #[inline] - pub fn is_stopped(&self) -> bool { - matches!(self.playback_state, PlaybackState::Stopped) + pub fn status(&self) -> Status { + self.status } pub fn play(&mut self) -> Result<(), DotLottiePlayerError> { - if !self.is_loaded { + if self.status == Status::Idle { return Err(DotLottiePlayerError::AnimationNotLoaded); } - if self.is_playing() { + if self.status == Status::Playing || self.status == Status::Tweening { return Err(DotLottiePlayerError::InsufficientCondition); } - if self.is_complete() && self.is_stopped() { + if self.is_complete() && self.status == Status::Stopped { self.elapsed_frames = 0.0; match self.mode { Mode::Forward | Mode::Bounce => { @@ -227,7 +217,7 @@ impl DotLottiePlayer { }; } - self.playback_state = PlaybackState::Playing; + self.status = Status::Playing; #[cfg(feature = "audio")] if let Some(am) = &mut self.audio_manager { @@ -240,13 +230,13 @@ impl DotLottiePlayer { } pub fn pause(&mut self) -> Result<(), DotLottiePlayerError> { - if !self.is_loaded { + if self.status == Status::Idle { return Err(DotLottiePlayerError::AnimationNotLoaded); } - if !self.is_playing() { + if self.status != Status::Playing { return Err(DotLottiePlayerError::InsufficientCondition); } - self.playback_state = PlaybackState::Paused; + self.status = Status::Paused; #[cfg(feature = "audio")] if let Some(am) = &mut self.audio_manager { @@ -258,14 +248,17 @@ impl DotLottiePlayer { } pub fn stop(&mut self) -> Result<(), DotLottiePlayerError> { - if !self.is_loaded { + if self.status == Status::Idle { return Err(DotLottiePlayerError::AnimationNotLoaded); } - if self.is_stopped() { + if self.status == Status::Stopped { return Err(DotLottiePlayerError::InsufficientCondition); } + if self.status == Status::Tweening { + self.tween_state = None; + } - self.playback_state = PlaybackState::Stopped; + self.status = Status::Stopped; let start_frame = self.start_frame(); let end_frame = self.end_frame(); @@ -312,7 +305,7 @@ impl DotLottiePlayer { } fn next_frame(&mut self) -> f32 { - if !self.is_loaded || !self.is_playing() { + if self.status != Status::Playing { return self.current_frame(); } @@ -455,7 +448,7 @@ impl DotLottiePlayer { #[inline] fn advance_frames(&mut self, dt: f32) { - if self.is_playing() { + if self.status == Status::Playing { let duration = self.duration(); if duration > 0.0 { let fps = self.total_frames() / duration; @@ -488,7 +481,7 @@ impl DotLottiePlayer { .push(DotLottieEvent::Frame { frame_no: no }); #[cfg(feature = "audio")] - if self.is_playing() { + if self.status == Status::Playing { if let Some(am) = &mut self.audio_manager { am.update(no); } @@ -541,7 +534,7 @@ impl DotLottiePlayer { // Completion logic only applies during active playback — not when the // caller renders manually (e.g. scrubbing while paused/stopped). - if self.is_playing() && self.is_complete() { + if self.status == Status::Playing && self.is_complete() { if self.loop_animation { let count_complete = self.loop_count > 0 && self.current_loop_count() >= self.loop_count; @@ -565,7 +558,7 @@ impl DotLottiePlayer { self.reset_current_loop_count(); } } else { - self.playback_state = PlaybackState::Stopped; + self.status = Status::Stopped; self.emit_on_complete(); } } @@ -836,7 +829,8 @@ impl DotLottiePlayer { F: FnOnce(&mut dyn LottieRenderer) -> Result<(), LottieRendererError>, { self.clear(); - self.playback_state = PlaybackState::Stopped; + self.tween_state = None; + self.status = Status::Idle; self.elapsed_frames = 0.0; self.current_loop_count = 0; @@ -846,7 +840,11 @@ impl DotLottiePlayer { return Err(DotLottiePlayerError::Unknown); } - self.is_loaded = loaded; + if !loaded { + return Err(DotLottiePlayerError::Unknown); + } + + self.status = Status::Stopped; let start_frame = self.start_frame(); let end_frame = self.end_frame(); @@ -864,11 +862,7 @@ impl DotLottiePlayer { let _ = self.renderer.render(); - if loaded { - Ok(()) - } else { - Err(DotLottiePlayerError::Unknown) - } + Ok(()) } pub fn load_animation_data( @@ -1043,7 +1037,7 @@ impl DotLottiePlayer { } pub fn is_complete(&self) -> bool { - if !self.is_loaded() { + if self.status == Status::Idle || self.status == Status::Tweening { return false; } @@ -1364,19 +1358,19 @@ impl DotLottiePlayer { duration: f32, easing: [f32; 4], ) -> Result<(), DotLottiePlayerError> { - if self.is_tweening() { + if self.status == Status::Idle { + return Err(DotLottiePlayerError::AnimationNotLoaded); + } + if self.status == Status::Tweening { return Err(DotLottiePlayerError::InsufficientCondition); } let from = self.current_frame(); self.tween_state = Some(TweenState::new(from, to, duration, easing)?); + self.resume_status = self.status; + self.status = Status::Tweening; Ok(()) } - #[inline] - pub fn is_tweening(&self) -> bool { - self.tween_state.is_some() - } - pub(crate) fn sync_tween_frame(&mut self, frame: f32) { self.renderer.sync_current_frame(frame); self.elapsed_frames = match self.direction { @@ -1403,6 +1397,7 @@ impl DotLottiePlayer { Direction::Reverse => self.end_frame() - to, }; self.tween_state = None; + self.status = self.resume_status; } Ok(status) @@ -1448,7 +1443,7 @@ impl DotLottiePlayer { pub fn tick(&mut self, dt: f32) -> Result { let dt = dt.max(0.0); - if self.is_tweening() { + if self.status == Status::Tweening { match self.tween_advance(dt) { Ok(_) => { self.render()?; @@ -1456,6 +1451,7 @@ impl DotLottiePlayer { } Err(e) => { self.tween_state = None; + self.status = self.resume_status; Err(e) } } diff --git a/dotlottie-rs/src/state_machine_engine/mod.rs b/dotlottie-rs/src/state_machine_engine/mod.rs index 3cbb608b..c1d52a3f 100644 --- a/dotlottie-rs/src/state_machine_engine/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/mod.rs @@ -1408,8 +1408,8 @@ impl<'a> StateMachineEngine<'a> { self.check_completion(); - let needs_resume = - self.status == StateMachineEngineStatus::Tweening && !self.player.is_tweening(); + let needs_resume = self.status == StateMachineEngineStatus::Tweening + && self.player.status() != crate::Status::Tweening; if needs_resume { self.resume_from_tweening(); diff --git a/dotlottie-rs/src/wasm/wasm_bindgen_api.rs b/dotlottie-rs/src/wasm/wasm_bindgen_api.rs index bd384333..c4e6b865 100644 --- a/dotlottie-rs/src/wasm/wasm_bindgen_api.rs +++ b/dotlottie-rs/src/wasm/wasm_bindgen_api.rs @@ -138,6 +138,29 @@ impl From for Mode { } } +/// Current status of the animation player. +#[wasm_bindgen] +#[derive(Clone, Copy, PartialEq)] +pub enum Status { + Idle = 0, + Playing = 1, + Paused = 2, + Stopped = 3, + Tweening = 4, +} + +impl From for Status { + fn from(s: crate::Status) -> Self { + match s { + crate::Status::Idle => Status::Idle, + crate::Status::Playing => Status::Playing, + crate::Status::Paused => Status::Paused, + crate::Status::Stopped => Status::Stopped, + crate::Status::Tweening => Status::Tweening, + } + } +} + // ─── JS object helpers ──────────────────────────────────────────────────────── fn js_obj_with_type(type_name: &str) -> Object { @@ -462,24 +485,12 @@ impl DotLottiePlayerWasm { // ── State queries ───────────────────────────────────────────────────────── - pub fn is_playing(&self) -> bool { - self.player.is_playing() - } - pub fn is_paused(&self) -> bool { - self.player.is_paused() - } - pub fn is_stopped(&self) -> bool { - self.player.is_stopped() - } - pub fn is_loaded(&self) -> bool { - self.player.is_loaded() + pub fn status(&self) -> Status { + self.player.status().into() } pub fn is_complete(&self) -> bool { self.player.is_complete() } - pub fn is_tweening(&self) -> bool { - self.player.is_tweening() - } // ── Frame queries ───────────────────────────────────────────────────────── diff --git a/dotlottie-rs/tests/autoplay.rs b/dotlottie-rs/tests/autoplay.rs index f4511a86..6273dd5e 100644 --- a/dotlottie-rs/tests/autoplay.rs +++ b/dotlottie-rs/tests/autoplay.rs @@ -1,6 +1,6 @@ use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Status}; mod test_utils; use crate::test_utils::{HEIGHT, WIDTH}; @@ -39,9 +39,7 @@ mod tests { let path = CString::new("assets/animations/lottie/test.json").unwrap(); assert!(player.load_animation_path(&path).is_ok()); - assert!(player.is_playing()); - assert!(!player.is_paused()); - assert!(!player.is_stopped()); + assert_eq!(player.status(), Status::Playing); assert!(!player.is_complete()); assert_eq!(player.current_frame(), 0.0); @@ -73,9 +71,7 @@ mod tests { assert!(loaded.is_ok()); - assert!(!player.is_playing()); - assert!(!player.is_paused()); - assert!(player.is_stopped()); + assert_eq!(player.status(), Status::Stopped); assert!(!player.is_complete()); assert!(player.current_frame() == 0.0); diff --git a/dotlottie-rs/tests/markers.rs b/dotlottie-rs/tests/markers.rs index 9fa9b674..5007eef8 100644 --- a/dotlottie-rs/tests/markers.rs +++ b/dotlottie-rs/tests/markers.rs @@ -1,6 +1,6 @@ use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, Segment}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Segment, Status}; mod test_utils; use crate::test_utils::{HEIGHT, WIDTH}; @@ -86,7 +86,11 @@ mod tests { assert_eq!(player.active_marker(), Some(marker_name.as_c_str())); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); // assert current frame is the marker start assert_eq!(player.current_frame(), 20.0); diff --git a/dotlottie-rs/tests/play.rs b/dotlottie-rs/tests/play.rs index df900ad3..6aef7ba7 100644 --- a/dotlottie-rs/tests/play.rs +++ b/dotlottie-rs/tests/play.rs @@ -3,7 +3,7 @@ mod test_utils; use std::ffi::CString; use crate::test_utils::{HEIGHT, WIDTH}; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError, Status}; #[cfg(test)] mod tests { @@ -50,7 +50,11 @@ mod tests { assert_eq!(player.play(), Ok(())); - assert!(player.is_playing(), "Expected player to be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Expected player to be playing" + ); assert_eq!( player.play(), @@ -127,7 +131,11 @@ mod tests { assert!(player.is_complete(), "Expected player to be complete"); - assert!(!player.is_playing(), "Expected player to not be playing"); + assert_ne!( + player.status(), + Status::Playing, + "Expected player to not be playing" + ); assert!( player.current_frame() == player.total_frames() - 1.0, diff --git a/dotlottie-rs/tests/play_mode.rs b/dotlottie-rs/tests/play_mode.rs index a9c397fa..94ae33f7 100644 --- a/dotlottie-rs/tests/play_mode.rs +++ b/dotlottie-rs/tests/play_mode.rs @@ -1,6 +1,6 @@ use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Status}; mod test_utils; @@ -46,11 +46,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -100,11 +104,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -148,11 +156,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -185,7 +197,7 @@ mod play_mode_tests { observed_completed = false; for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -237,11 +249,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() >= 3 { + if player.status() != Status::Playing || player.current_loop_count() >= 3 { break; } assert!( @@ -272,7 +288,7 @@ mod play_mode_tests { let _ = player.play(); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 10 { + if player.status() != Status::Playing || player.current_loop_count() > 10 { break; } assert!( @@ -388,7 +404,11 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); let mut rendered_frames: Vec = vec![]; @@ -443,11 +463,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -496,7 +520,11 @@ mod play_mode_tests { "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); let mut rendered_frames: Vec = vec![]; @@ -548,11 +576,15 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -604,7 +636,11 @@ mod play_mode_tests { let mut rendered_frames: Vec = vec![]; - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert!(!player.is_complete(), "Animation should not be complete"); while !player.is_complete() { @@ -672,10 +708,14 @@ mod play_mode_tests { Ok(()), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( @@ -723,7 +763,11 @@ mod play_mode_tests { "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); let mut rendered_frames: Vec = vec![]; @@ -792,10 +836,14 @@ mod play_mode_tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); for tick in 0..MAX_TICKS { - if player.is_paused() || player.is_stopped() || player.current_loop_count() > 5 { + if player.status() != Status::Playing || player.current_loop_count() > 5 { break; } assert!( diff --git a/dotlottie-rs/tests/segments.rs b/dotlottie-rs/tests/segments.rs index a4c94ff5..ffc2d755 100644 --- a/dotlottie-rs/tests/segments.rs +++ b/dotlottie-rs/tests/segments.rs @@ -3,7 +3,7 @@ use crate::test_utils::{HEIGHT, WIDTH}; use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, Mode, Segment}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Mode, Segment, Status}; #[cfg(test)] mod tests { @@ -33,7 +33,11 @@ mod tests { })); assert!(result.is_err(), "Invalid segment should be rejected"); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); let total_frames = player.total_frames(); diff --git a/dotlottie-rs/tests/speed.rs b/dotlottie-rs/tests/speed.rs index e19ce9d7..cf081c28 100644 --- a/dotlottie-rs/tests/speed.rs +++ b/dotlottie-rs/tests/speed.rs @@ -1,6 +1,6 @@ use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, Segment}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Segment, Status}; mod test_utils; use crate::test_utils::{HEIGHT, WIDTH}; @@ -106,7 +106,11 @@ mod tests { player.load_animation_path(&path).is_ok(), "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); let seg = player.segment().unwrap(); let expected_duration = diff --git a/dotlottie-rs/tests/state_machine.rs b/dotlottie-rs/tests/state_machine.rs index 5a899eab..c438f302 100644 --- a/dotlottie-rs/tests/state_machine.rs +++ b/dotlottie-rs/tests/state_machine.rs @@ -7,7 +7,7 @@ mod tests { use dotlottie_rs::{ actions::open_url_policy::OpenUrlPolicy, ColorSpace, DotLottiePlayer, Event, - StateMachineEngineStatus, + StateMachineEngineStatus, Status, }; use std::io::Read; @@ -35,7 +35,7 @@ mod tests { assert_eq!(player.load_dotlottie_data(&markers_buffer), Ok(())); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); let sm_id = CString::new("Exploding Pigeon").unwrap(); let mut sm = player @@ -363,7 +363,7 @@ mod tests { sm.player .tween(5.0, 2000.0, [0.0, 0.0, 1.0, 1.0]) .expect("initial tween should succeed"); - assert!(sm.player.is_tweening()); + assert_eq!(sm.player.status(), Status::Tweening); // Trigger a tweened transition (rating=2 → star_2). // tween will fail because the player is already tweening, @@ -416,8 +416,9 @@ mod tests { StateMachineEngineStatus::Tweening, "state machine should be in Tweening status when tweened transition targets a state without a segment" ); - assert!( - sm.player.is_tweening(), + assert_eq!( + sm.player.status(), + Status::Tweening, "player should be tweening after a tweened transition to a state without a segment" ); @@ -461,7 +462,11 @@ mod tests { StateMachineEngineStatus::Tweening, "state machine should be in Tweening status for a reverse mode state with segment" ); - assert!(sm.player.is_tweening(), "player should be tweening"); + assert_eq!( + sm.player.status(), + Status::Tweening, + "player should be tweening" + ); assert_eq!(sm.get_current_state_name(), "forward_state"); @@ -513,7 +518,11 @@ mod tests { StateMachineEngineStatus::Tweening, "state machine should be in Tweening status for a reverse mode state without segment" ); - assert!(sm.player.is_tweening(), "player should be tweening"); + assert_eq!( + sm.player.status(), + Status::Tweening, + "player should be tweening" + ); assert_eq!(sm.get_current_state_name(), "forward_state"); diff --git a/dotlottie-rs/tests/stop.rs b/dotlottie-rs/tests/stop.rs index 69e0afa8..d58b1616 100644 --- a/dotlottie-rs/tests/stop.rs +++ b/dotlottie-rs/tests/stop.rs @@ -3,7 +3,7 @@ use crate::test_utils::{HEIGHT, WIDTH}; use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError, Mode, Segment}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError, Mode, Segment, Status}; #[cfg(test)] mod tests { @@ -95,7 +95,11 @@ mod tests { "Animation should load" ); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); let Segment { start: start_frame, @@ -107,13 +111,19 @@ mod tests { assert_eq!(player.set_frame(mid_frame), Ok(()), "Frame should be set"); assert_eq!(player.render(), Ok(()), "Frame should render"); - assert!(player.is_playing(), "Animation should be playing"); + assert_eq!( + player.status(), + Status::Playing, + "Animation should be playing" + ); assert_eq!(player.stop(), Ok(()), "Animation should stop"); - assert!(!player.is_playing(), "Animation should not be playing"); - assert!(player.is_stopped(), "Animation should be stopped"); - assert!(!player.is_paused(), "Animation should not be paused"); + assert_eq!( + player.status(), + Status::Stopped, + "Animation should be stopped" + ); // based on the mode the current frame should be at the start or end match player.mode() { diff --git a/dotlottie-rs/tests/theming.rs b/dotlottie-rs/tests/theming.rs index 6ff2acf6..6a76be60 100644 --- a/dotlottie-rs/tests/theming.rs +++ b/dotlottie-rs/tests/theming.rs @@ -1,4 +1,4 @@ -use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, DotLottiePlayerError, Status}; use std::ffi::CString; mod test_utils; @@ -42,7 +42,7 @@ mod tests { ); assert_eq!(player.theme_id(), Some(valid_theme_id.as_c_str())); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } #[test] @@ -76,7 +76,7 @@ mod tests { "Expected theme to not load" ); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } #[test] @@ -178,7 +178,7 @@ mod tests { assert_eq!(player.load_animation_data(&data), Ok(())); assert!(player.theme_id().is_none()); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } #[test] @@ -216,7 +216,7 @@ mod tests { assert_eq!(player.load_animation_path(&path), Ok(())); assert!(player.theme_id().is_none()); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } #[test] @@ -252,7 +252,7 @@ mod tests { .is_ok()); assert!(player.theme_id().is_none()); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } #[test] @@ -290,6 +290,6 @@ mod tests { "Theme should persist after load_animation within the same .lottie container" ); - assert!(player.is_playing()); + assert_eq!(player.status(), Status::Playing); } } diff --git a/dotlottie-rs/tests/tween.rs b/dotlottie-rs/tests/tween.rs index 627534b3..1724d1d8 100644 --- a/dotlottie-rs/tests/tween.rs +++ b/dotlottie-rs/tests/tween.rs @@ -1,6 +1,6 @@ use std::ffi::CString; -use dotlottie_rs::{ColorSpace, DotLottiePlayer, TweenStatus}; +use dotlottie_rs::{ColorSpace, DotLottiePlayer, Status, TweenStatus}; mod test_utils; use crate::test_utils::{HEIGHT, WIDTH}; @@ -70,7 +70,7 @@ mod tests { player .tween(20.0, 1.0, [0.0, 0.0, 1.0, 1.0]) .expect("tween should start"); - assert!(player.is_tweening()); + assert_eq!(player.status(), Status::Tweening); // Pass enough dt to complete the 1ms tween let result = player.tween_advance(10.0); @@ -79,8 +79,9 @@ mod tests { Ok(TweenStatus::Completed), "tween_advance should return Ok(Completed) when tween completes" ); - assert!( - !player.is_tweening(), + assert_ne!( + player.status(), + Status::Tweening, "should not be tweening after tween completes" ); } @@ -97,13 +98,15 @@ mod tests { player .tween(20.0, 1.0, [0.0, 0.0, 1.0, 1.0]) .expect("tween should start"); + assert_eq!(player.status(), Status::Tweening); // Tick with enough dt to complete the 1ms tween let result = player.tick(10.0); assert!(result.is_ok(), "tick() should succeed, got {result:?}"); - assert!( - !player.is_tweening(), - "should not be tweening after tick completes the tween" + assert_eq!( + player.status(), + Status::Playing, + "should resume Playing after tween completes" ); let result = player.tick(1000.0 / 60.0); @@ -119,4 +122,86 @@ mod tests { "frame after tween should be near target (20.0), not jumped far ahead; got {frame}" ); } + + #[test] + fn tween_from_stopped_resumes_to_stopped() { + let (mut player, _buf) = setup_player(); + + assert_eq!(player.status(), Status::Stopped); + + player + .tween(10.0, 1.0, [0.0, 0.0, 1.0, 1.0]) + .expect("tween from stopped should succeed"); + assert_eq!(player.status(), Status::Tweening); + + let result = player.tween_advance(10.0); + assert_eq!(result, Ok(TweenStatus::Completed)); + assert_eq!( + player.status(), + Status::Stopped, + "should resume Stopped after tween completes" + ); + } + + #[test] + fn tween_from_paused_resumes_to_paused() { + let (mut player, _buf) = setup_player(); + + player.play().expect("play should succeed"); + player.pause().expect("pause should succeed"); + assert_eq!(player.status(), Status::Paused); + + player + .tween(10.0, 1.0, [0.0, 0.0, 1.0, 1.0]) + .expect("tween from paused should succeed"); + assert_eq!(player.status(), Status::Tweening); + + let result = player.tween_advance(10.0); + assert_eq!(result, Ok(TweenStatus::Completed)); + assert_eq!( + player.status(), + Status::Paused, + "should resume Paused after tween completes" + ); + } + + #[test] + fn play_during_tweening_returns_error() { + let (mut player, _buf) = setup_player(); + + player + .tween(10.0, 0.5, [0.0, 0.0, 1.0, 1.0]) + .expect("tween should start"); + + let result = player.play(); + assert!( + result.is_err(), + "play() during tweening should return error" + ); + } + + #[test] + fn stop_during_tweening_cancels_tween() { + let (mut player, _buf) = setup_player(); + + player.play().expect("play should succeed"); + + player + .tween(10.0, 0.5, [0.0, 0.0, 1.0, 1.0]) + .expect("tween should start"); + assert_eq!(player.status(), Status::Tweening); + + let result = player.stop(); + assert!(result.is_ok(), "stop() during tweening should succeed"); + assert_eq!( + player.status(), + Status::Stopped, + "should be Stopped after cancelling tween" + ); + assert_ne!( + player.status(), + Status::Tweening, + "should not be tweening after stop" + ); + } } From 4ba461324c4722b821c961acc9b94a9e77862d18 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ashraf Date: Fri, 17 Apr 2026 01:00:39 +0700 Subject: [PATCH 2/2] refactor: clear resume_status on stop and test pause-during-tween - stop() during Tweening now resets resume_status to Stopped so stale pre-tween status can't linger past a cancel. - Add regression test locking in that pause() during Tweening returns InsufficientCondition and leaves status at Tweening. --- dotlottie-rs/src/player.rs | 1 + dotlottie-rs/tests/tween.rs | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/dotlottie-rs/src/player.rs b/dotlottie-rs/src/player.rs index 00cab2b9..2ebbcd37 100644 --- a/dotlottie-rs/src/player.rs +++ b/dotlottie-rs/src/player.rs @@ -256,6 +256,7 @@ impl DotLottiePlayer { } if self.status == Status::Tweening { self.tween_state = None; + self.resume_status = Status::Stopped; } self.status = Status::Stopped; diff --git a/dotlottie-rs/tests/tween.rs b/dotlottie-rs/tests/tween.rs index 1724d1d8..706f940d 100644 --- a/dotlottie-rs/tests/tween.rs +++ b/dotlottie-rs/tests/tween.rs @@ -180,6 +180,29 @@ mod tests { ); } + #[test] + fn pause_during_tweening_returns_error() { + let (mut player, _buf) = setup_player(); + + player.play().expect("play should succeed"); + + player + .tween(10.0, 0.5, [0.0, 0.0, 1.0, 1.0]) + .expect("tween should start"); + assert_eq!(player.status(), Status::Tweening); + + let result = player.pause(); + assert!( + result.is_err(), + "pause() during tweening should return error" + ); + assert_eq!( + player.status(), + Status::Tweening, + "status should remain Tweening after rejected pause" + ); + } + #[test] fn stop_during_tweening_cancels_tween() { let (mut player, _buf) = setup_player();