use core::ops::{Add, AddAssign, Sub, SubAssign};
use rinia::FloatScalar;
#[cfg(feature = "zerocopy")]
use zerocopy::*;
use crate::{
FrameRate, Time, conversion,
macros::{impl_bytemuck_transparent, impl_inner_op_family_forwarding},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "zerocopy", derive(FromBytes, Immutable, IntoBytes, KnownLayout))]
#[repr(transparent)]
pub struct FrameIndex(u32);
impl FrameIndex {
pub const ZERO: Self = Self(0);
#[inline]
pub const fn new(value: u32) -> Self {
Self(value)
}
#[inline]
pub const fn get(&self) -> u32 {
self.0
}
#[inline]
pub fn from_time<T: FloatScalar>(time: Time<T>, rate: FrameRate<T>) -> Self {
conversion::frame_from_time(time, rate)
}
#[inline]
pub fn to_time<T: FloatScalar>(self, rate: FrameRate<T>) -> Time<T> {
conversion::time_from_frame(self, rate)
}
#[inline]
pub fn offset_from(self, other: FrameIndex) -> i32 {
let a = self.0 as i64;
let b = other.0 as i64;
(a - b) as i32
}
#[inline]
pub const fn next(self) -> Self {
Self(self.0.saturating_add(1))
}
#[inline]
pub const fn prev(self) -> Self {
Self(self.0.saturating_sub(1))
}
impl_inner_op_family_forwarding!(add, u32, 0);
impl_inner_op_family_forwarding!(sub, u32, 0);
}
impl From<u32> for FrameIndex {
#[inline]
fn from(value: u32) -> Self {
Self::new(value)
}
}
impl From<FrameIndex> for u32 {
#[inline]
fn from(frame: FrameIndex) -> Self {
frame.get()
}
}
impl Add<u32> for FrameIndex {
type Output = Self;
#[inline]
fn add(self, rhs: u32) -> Self::Output {
self.saturating_add(rhs)
}
}
impl AddAssign<u32> for FrameIndex {
#[inline]
fn add_assign(&mut self, rhs: u32) {
*self = self.saturating_add(rhs);
}
}
impl Sub<u32> for FrameIndex {
type Output = Self;
#[inline]
fn sub(self, rhs: u32) -> Self::Output {
self.saturating_sub(rhs)
}
}
impl SubAssign<u32> for FrameIndex {
#[inline]
fn sub_assign(&mut self, rhs: u32) {
*self = self.saturating_sub(rhs);
}
}
impl_bytemuck_transparent!(FrameIndex, u32);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basics_zero_new_get_next_prev() {
assert_eq!(FrameIndex::ZERO.get(), 0);
assert_eq!(FrameIndex::new(10).get(), 10);
assert_eq!(FrameIndex::new(10).next(), FrameIndex::new(11));
assert_eq!(FrameIndex::new(10).prev(), FrameIndex::new(9));
}
#[test]
fn next_prev_saturate_at_bounds() {
assert_eq!(FrameIndex::new(u32::MAX).next(), FrameIndex::new(u32::MAX));
assert_eq!(FrameIndex::ZERO.prev(), FrameIndex::ZERO);
}
#[test]
fn offset_from_reports_signed_difference() {
assert_eq!(FrameIndex::new(10).offset_from(FrameIndex::new(4)), 6);
assert_eq!(FrameIndex::new(4).offset_from(FrameIndex::new(10)), -6);
assert_eq!(FrameIndex::new(8).offset_from(FrameIndex::new(8)), 0);
}
#[test]
fn saturating_checked_wrapping_add_sub_work() {
let x = FrameIndex::new(u32::MAX - 1);
assert_eq!(x.saturating_add(10), FrameIndex::new(u32::MAX));
assert_eq!(x.checked_add(1), Some(FrameIndex::new(u32::MAX)));
assert_eq!(x.checked_add(2), None);
assert_eq!(x.wrapping_add(2), FrameIndex::new(0));
let y = FrameIndex::new(1);
assert_eq!(y.saturating_sub(10), FrameIndex::ZERO);
assert_eq!(y.checked_sub(1), Some(FrameIndex::ZERO));
assert_eq!(y.checked_sub(2), None);
assert_eq!(y.wrapping_sub(2), FrameIndex::new(u32::MAX));
}
#[test]
fn add_sub_and_assign_use_saturating_behavior() {
let mut i = FrameIndex::new(u32::MAX - 1);
i += 10;
assert_eq!(i, FrameIndex::new(u32::MAX));
i -= u32::MAX;
assert_eq!(i, FrameIndex::ZERO);
assert_eq!(FrameIndex::new(5) + 10, FrameIndex::new(15));
assert_eq!(FrameIndex::new(5) - 10, FrameIndex::ZERO);
}
#[test]
fn conversion_to_and_from_time_matches_rate() {
let rate = FrameRate::from_fps(30.0_f64);
let frame = FrameIndex::new(45);
let time = frame.to_time(rate);
let frame2 = FrameIndex::from_time(time, rate);
assert_eq!(time, Time::from_seconds(1.5));
assert_eq!(frame2, frame);
}
#[cfg(feature = "bytemuck")]
#[test]
fn bytemuck_traits_are_implemented() {
fn assert_impl<
T: bytemuck::Pod + bytemuck::Zeroable + bytemuck::TransparentWrapper<u32>,
>() {
}
assert_impl::<FrameIndex>();
}
#[cfg(feature = "bytemuck")]
#[test]
fn bytemuck_roundtrip_works() {
let value = FrameIndex::new(1234);
let bytes = bytemuck::bytes_of(&value);
let decoded = *bytemuck::from_bytes::<FrameIndex>(bytes);
assert_eq!(decoded, value);
}
#[cfg(feature = "zerocopy")]
#[test]
fn zerocopy_traits_are_implemented() {
fn assert_impl<
T: zerocopy::FromBytes + zerocopy::IntoBytes + zerocopy::KnownLayout + zerocopy::Immutable,
>() {
}
assert_impl::<FrameIndex>();
}
#[cfg(feature = "zerocopy")]
#[test]
fn zerocopy_roundtrip_works() {
let value = FrameIndex::new(5678);
let bytes = <FrameIndex as zerocopy::IntoBytes>::as_bytes(&value);
let decoded = <FrameIndex as zerocopy::FromBytes>::ref_from_bytes(bytes)
.expect("FrameIndex bytes should decode");
assert_eq!(*decoded, value);
}
}