use crossterm::event::Event;
use ratatui::Frame;
use ratatui::layout::Rect;
use std::ops::Deref;
use std::time::{Duration, Instant};
use super::context::{try_use_context, use_context};
use super::effect_event::use_effect_event;
use super::event::use_event;
use super::state::use_state;
#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)]
pub struct ComponentArea(pub Rect);
impl Deref for ComponentArea {
type Target = Rect;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub fn use_area() -> ComponentArea {
use_context::<ComponentArea>()
}
pub fn try_use_area() -> Option<ComponentArea> {
try_use_context::<ComponentArea>()
}
#[derive(Clone, Copy, Debug)]
pub struct FrameInfo {
pub count: u64,
pub delta: Duration,
pub timestamp: Instant,
}
impl Default for FrameInfo {
fn default() -> Self {
Self {
count: 0,
delta: Duration::ZERO,
timestamp: Instant::now(),
}
}
}
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
}
}
#[derive(Clone, Copy)]
pub struct FrameContext {
frame_ptr: *mut Frame<'static>,
pub info: FrameInfo,
}
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,
info: FrameInfo::new(count, delta, timestamp),
}
}
pub unsafe fn from_raw_ptr(
frame_ptr: *mut Frame<'static>,
count: u64,
delta: Duration,
timestamp: Instant,
) -> Self {
Self {
frame_ptr,
info: FrameInfo::new(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 count(&self) -> u64 {
self.info.count
}
pub fn delta(&self) -> Duration {
self.info.delta
}
pub fn timestamp(&self) -> Instant {
self.info.timestamp
}
pub fn fps(&self) -> f64 {
self.info.fps()
}
pub fn delta_secs(&self) -> f64 {
self.info.delta_secs()
}
pub fn delta_millis(&self) -> u128 {
self.info.delta_millis()
}
pub fn is_first_frame(&self) -> bool {
self.info.is_first_frame()
}
pub fn frame_info(&self) -> FrameInfo {
self.info
}
}
pub fn use_frame() -> FrameContext {
use_context::<FrameContext>()
}
pub fn try_use_frame() -> Option<FrameContext> {
try_use_context::<FrameContext>()
}
pub fn use_frame_info() -> FrameInfo {
try_use_context::<FrameContext>()
.map(|ctx| ctx.frame_info())
.unwrap_or_default()
}
pub fn use_on_resize<F>(callback: F)
where
F: Fn((u16, u16)) + Send + Sync + 'static,
{
let stable_handler = use_effect_event(move |dimensions: (u16, u16)| {
callback(dimensions);
});
if let Some(Event::Resize(width, height)) = use_event() {
stable_handler.call((width, height));
}
}
pub fn use_resize() -> (u16, u16) {
let (size, set_size) = use_state(|| (0u16, 0u16));
use_on_resize({
move |(width, height)| {
set_size.set((width, height));
}
});
size
}
pub fn use_media_query<F>(predicate: F) -> bool
where
F: Fn((u16, u16)) -> bool + Send + Sync + 'static,
{
let (matches, set_matches) = use_state(|| false);
use_on_resize({
move |(width, height)| {
let result = predicate((width, height));
set_matches.set(result);
}
});
matches
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context_stack::{clear_context_stack, push_context};
use crate::event::{clear_current_event, set_current_event};
use crate::fiber::FiberId;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree, with_fiber_tree_mut};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
with_fiber_tree_mut(|tree| {
tree.end_render();
});
clear_fiber_tree();
clear_current_event();
clear_context_stack();
crate::scheduler::batch::clear_state_batch();
}
#[test]
fn test_component_area_deref() {
let area = ComponentArea(Rect::new(10, 20, 100, 50));
assert_eq!(area.width, 100);
assert_eq!(area.height, 50);
assert_eq!(area.x, 10);
assert_eq!(area.y, 20);
}
#[test]
fn test_component_area_default() {
let area = ComponentArea::default();
assert_eq!(area.width, 0);
assert_eq!(area.height, 0);
}
#[test]
fn test_use_area_with_context() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let test_area = ComponentArea(Rect::new(10, 20, 100, 50));
push_context(fiber_id, test_area);
let area = use_area();
assert_eq!(area.width, 100);
assert_eq!(area.height, 50);
cleanup_test();
}
#[test]
fn test_try_use_area_without_context() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let area = try_use_area();
assert!(area.is_none());
cleanup_test();
}
#[test]
fn test_frame_info_creation() {
let now = Instant::now();
let info = FrameInfo::new(42, Duration::from_millis(16), now);
assert_eq!(info.count, 42);
assert_eq!(info.delta, Duration::from_millis(16));
assert_eq!(info.timestamp, now);
}
#[test]
fn test_frame_info_fps() {
let info = FrameInfo::new(1, Duration::from_millis(16), Instant::now());
let fps = info.fps();
assert!((fps - 62.5).abs() < 0.1);
}
#[test]
fn test_frame_info_delta_conversions() {
let info = FrameInfo::new(1, Duration::from_millis(16), Instant::now());
assert_eq!(info.delta_millis(), 16);
assert!((info.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_info_default() {
let info = FrameInfo::default();
assert_eq!(info.count, 0);
assert_eq!(info.delta, Duration::ZERO);
assert!(info.is_first_frame());
}
#[test]
fn test_use_on_resize_receives_event() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let call_count_clone = call_count.clone();
let event = Event::Resize(120, 40);
set_current_event(Some(Arc::new(event)));
use_on_resize(move |(width, height)| {
assert_eq!(width, 120);
assert_eq!(height, 40);
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 1);
cleanup_test();
}
#[test]
fn test_use_on_resize_ignores_non_resize_events() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
let call_count = Arc::new(AtomicI32::new(0));
let call_count_clone = call_count.clone();
let event = Event::Key(crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('a'),
crossterm::event::KeyModifiers::NONE,
));
set_current_event(Some(Arc::new(event)));
use_on_resize(move |_| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 0);
cleanup_test();
}
#[test]
fn test_use_resize_default() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
clear_current_event();
let (width, height) = use_resize();
assert_eq!(width, 0);
assert_eq!(height, 0);
cleanup_test();
}
#[test]
fn test_use_resize_updates_on_event() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let event = Event::Resize(120, 40);
set_current_event(Some(Arc::new(event)));
let (width, height) = use_resize();
assert_eq!(width, 0);
assert_eq!(height, 0);
with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
tree.begin_render(fiber_id);
});
clear_current_event();
let (width, height) = use_resize();
assert_eq!(width, 120);
assert_eq!(height, 40);
cleanup_test();
}
#[test]
fn test_use_media_query_default() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let _fiber_id = setup_test_fiber();
clear_current_event();
let is_narrow = use_media_query(|(w, _)| w < 80);
assert!(!is_narrow);
cleanup_test();
}
#[test]
fn test_use_media_query_evaluates_predicate() {
let _lock = TEST_MUTEX.lock();
cleanup_test();
let fiber_id = setup_test_fiber();
let event = Event::Resize(60, 40);
set_current_event(Some(Arc::new(event)));
let is_narrow = use_media_query(|(w, _)| w < 80);
assert!(!is_narrow);
with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
tree.begin_render(fiber_id);
});
clear_current_event();
let is_narrow = use_media_query(|(w, _)| w < 80);
assert!(is_narrow);
cleanup_test();
}
}