use crate::context::use_context;
use ratatui::Frame;
use std::time::{Duration, Instant};
#[derive(Clone, Copy)]
pub struct FrameContext {
frame_ptr: *mut Frame<'static>,
pub count: u64,
pub delta: Duration,
pub timestamp: Instant,
}
unsafe impl Send for FrameContext {}
unsafe impl Sync for FrameContext {}
impl FrameContext {
pub unsafe fn new(frame: &mut Frame, count: u64, delta: Duration, timestamp: Instant) -> Self {
let frame_ptr = std::ptr::from_mut(frame).cast::<Frame<'static>>();
Self {
frame_ptr,
count,
delta,
timestamp,
}
}
pub unsafe fn from_raw_ptr(
frame_ptr: *mut Frame<'static>,
count: u64,
delta: Duration,
timestamp: Instant,
) -> Self {
Self {
frame_ptr,
count,
delta,
timestamp,
}
}
pub fn frame(&self) -> &Frame<'static> {
unsafe { &*self.frame_ptr }
}
#[allow(clippy::mut_from_ref)]
pub fn frame_mut(&mut self) -> &mut Frame<'static> {
unsafe { &mut *self.frame_ptr }
}
pub fn fps(&self) -> f64 {
if self.delta.as_secs_f64() > 0.0 {
1.0 / self.delta.as_secs_f64()
} else {
0.0
}
}
pub fn delta_secs(&self) -> f64 {
self.delta.as_secs_f64()
}
pub fn delta_millis(&self) -> u128 {
self.delta.as_millis()
}
pub fn is_first_frame(&self) -> bool {
self.count == 0
}
pub fn frame_info(&self) -> FrameInfo {
FrameInfo {
count: self.count,
delta: self.delta,
timestamp: self.timestamp,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FrameInfo {
pub count: u64,
pub delta: Duration,
pub timestamp: Instant,
}
impl FrameInfo {
pub fn new(count: u64, delta: Duration, timestamp: Instant) -> Self {
Self {
count,
delta,
timestamp,
}
}
pub fn fps(&self) -> f64 {
if self.delta.as_secs_f64() > 0.0 {
1.0 / self.delta.as_secs_f64()
} else {
0.0
}
}
pub fn delta_secs(&self) -> f64 {
self.delta.as_secs_f64()
}
pub fn delta_millis(&self) -> u128 {
self.delta.as_millis()
}
pub fn is_first_frame(&self) -> bool {
self.count == 0
}
}
pub fn use_frame() -> FrameContext {
use_context::<FrameContext>()
}
pub trait FrameExt {
fn frame_count(&self) -> u64;
fn delta_time(&self) -> Duration;
fn frame_timestamp(&self) -> Instant;
fn fps(&self) -> f64;
fn delta_secs(&self) -> f64;
fn delta_millis(&self) -> u128;
fn is_first_frame(&self) -> bool;
fn frame_info(&self) -> FrameInfo;
}
impl FrameExt for Frame<'_> {
fn frame_count(&self) -> u64 {
use_frame().count
}
fn delta_time(&self) -> Duration {
use_frame().delta
}
fn frame_timestamp(&self) -> Instant {
use_frame().timestamp
}
fn fps(&self) -> f64 {
use_frame().fps()
}
fn delta_secs(&self) -> f64 {
use_frame().delta_secs()
}
fn delta_millis(&self) -> u128 {
use_frame().delta_millis()
}
fn is_first_frame(&self) -> bool {
use_frame().is_first_frame()
}
fn frame_info(&self) -> FrameInfo {
use_frame().frame_info()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::with_component_id;
use std::time::{Duration, Instant};
#[test]
fn test_frame_info_creation() {
let now = Instant::now();
let frame = FrameInfo::new(42, Duration::from_millis(16), now);
assert_eq!(frame.count, 42);
assert_eq!(frame.delta, Duration::from_millis(16));
assert_eq!(frame.timestamp, now);
}
#[test]
fn test_frame_info_fps() {
let frame = FrameInfo::new(1, Duration::from_millis(16), Instant::now());
let fps = frame.fps();
assert!((fps - 62.5).abs() < 0.1);
}
#[test]
fn test_frame_info_delta_conversions() {
let frame = FrameInfo::new(1, Duration::from_millis(16), Instant::now());
assert_eq!(frame.delta_millis(), 16);
assert!((frame.delta_secs() - 0.016).abs() < 0.001);
}
#[test]
fn test_frame_info_is_first_frame() {
let frame0 = FrameInfo::new(0, Duration::from_millis(16), Instant::now());
let frame1 = FrameInfo::new(1, Duration::from_millis(16), Instant::now());
assert!(frame0.is_first_frame());
assert!(!frame1.is_first_frame());
}
#[test]
fn test_frame_context_methods() {
let now = Instant::now();
let mock_ptr = std::ptr::null_mut();
let frame_ctx = FrameContext {
frame_ptr: mock_ptr,
count: 100,
delta: Duration::from_millis(16),
timestamp: now,
};
assert_eq!(frame_ctx.count, 100);
assert_eq!(frame_ctx.delta, Duration::from_millis(16));
assert_eq!(frame_ctx.timestamp, now);
let fps = frame_ctx.fps();
assert!((fps - 62.5).abs() < 0.1);
assert_eq!(frame_ctx.delta_millis(), 16);
assert!((frame_ctx.delta_secs() - 0.016).abs() < 0.001);
assert!(!frame_ctx.is_first_frame());
}
#[test]
fn test_frame_context_first_frame() {
let mock_ptr = std::ptr::null_mut();
let frame_ctx = FrameContext {
frame_ptr: mock_ptr,
count: 0,
delta: Duration::from_millis(16),
timestamp: Instant::now(),
};
assert!(frame_ctx.is_first_frame());
}
#[test]
fn test_frame_context_fps_calculation() {
let mock_ptr = std::ptr::null_mut();
let frame_60fps = FrameContext {
frame_ptr: mock_ptr,
count: 1,
delta: Duration::from_micros(16667),
timestamp: Instant::now(),
};
let fps = frame_60fps.fps();
assert!((fps - 60.0).abs() < 1.0);
}
#[test]
#[should_panic(expected = "Context value for type")]
fn test_use_frame_without_context_panics() {
with_component_id("ComponentWithoutContext", |_ctx| {
let _frame = use_frame();
});
}
}