use std::cell::RefCell;
use std::sync::Arc;
use crossterm::event::Event;
use crate::context_stack::ContextStack;
use crate::event::EventState;
use crate::fiber_tree::FiberTree;
use crate::scheduler::batch::StateBatch;
use crate::scheduler::effect_queue::EffectQueue;
thread_local! {
static RENDER_CONTEXT: RefCell<Option<RenderContext>> = const { RefCell::new(None) };
}
pub struct RenderContext {
pub fiber_tree: FiberTree,
pub state_batch: StateBatch,
pub effect_queue: EffectQueue,
pub context_stack: ContextStack,
pub event_state: EventState,
}
impl RenderContext {
pub fn new() -> Self {
Self {
fiber_tree: FiberTree::new(),
state_batch: StateBatch::new(),
effect_queue: EffectQueue::new(),
context_stack: ContextStack::new(),
event_state: EventState::new(),
}
}
pub fn prepare_for_render(&mut self) {
self.fiber_tree.prepare_for_render();
}
pub fn set_event(&mut self, event: Option<Arc<Event>>) {
self.event_state.event = event;
self.event_state.reset_propagation();
}
pub fn clear_event(&mut self) {
self.event_state.event = None;
}
pub fn begin_batch(&mut self) {
self.state_batch.begin_batch();
}
pub fn end_batch(&mut self) -> std::collections::HashSet<crate::fiber::FiberId> {
self.state_batch.end_batch(&mut self.fiber_tree)
}
pub fn flush_effects(&mut self) {
self.effect_queue.flush(&mut self.fiber_tree);
}
pub async fn flush_async_effects(&mut self) {
self.effect_queue.flush_async(&mut self.fiber_tree).await;
}
pub fn mark_unseen_for_unmount(&mut self) {
self.fiber_tree.mark_unseen_for_unmount();
}
pub fn process_unmounts(&mut self) -> Vec<crate::fiber::FiberId> {
for &fiber_id in &self.fiber_tree.pending_unmount {
self.context_stack.pop_for_fiber(fiber_id);
}
self.fiber_tree.process_unmounts()
}
pub fn clear(&mut self) {
self.fiber_tree = FiberTree::new();
self.state_batch.clear();
self.effect_queue.clear();
self.context_stack.clear();
self.event_state = EventState::new();
}
}
impl Default for RenderContext {
fn default() -> Self {
Self::new()
}
}
pub fn init_render_context() {
RENDER_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = Some(RenderContext::new());
});
}
pub fn is_render_context_initialized() -> bool {
RENDER_CONTEXT.with(|ctx| ctx.borrow().is_some())
}
pub fn with_render_context<R, F: FnOnce(&RenderContext) -> R>(f: F) -> Option<R> {
RENDER_CONTEXT.with(|ctx| ctx.borrow().as_ref().map(f))
}
pub fn with_render_context_mut<R, F: FnOnce(&mut RenderContext) -> R>(f: F) -> Option<R> {
RENDER_CONTEXT.with(|ctx| ctx.borrow_mut().as_mut().map(f))
}
pub fn clear_render_context() {
RENDER_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = None;
});
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() {
clear_render_context();
init_render_context();
}
fn teardown() {
clear_render_context();
}
#[test]
fn test_render_context_creation() {
let ctx = RenderContext::new();
assert!(ctx.fiber_tree.fibers.is_empty());
assert!(!ctx.state_batch.is_batching());
assert!(!ctx.effect_queue.has_pending());
assert!(ctx.event_state.event.is_none());
}
#[test]
fn test_init_and_clear_render_context() {
clear_render_context();
assert!(!is_render_context_initialized());
init_render_context();
assert!(is_render_context_initialized());
clear_render_context();
assert!(!is_render_context_initialized());
}
#[test]
fn test_with_render_context() {
setup();
let result = with_render_context(|ctx| ctx.fiber_tree.fibers.len());
assert_eq!(result, Some(0));
teardown();
}
#[test]
fn test_with_render_context_mut() {
setup();
with_render_context_mut(|ctx| {
ctx.fiber_tree.mount(None, None);
});
let count = with_render_context(|ctx| ctx.fiber_tree.fibers.len());
assert_eq!(count, Some(1));
teardown();
}
#[test]
fn test_set_and_clear_event() {
setup();
let event = Event::FocusGained;
with_render_context_mut(|ctx| {
ctx.set_event(Some(Arc::new(event)));
});
let has_event = with_render_context(|ctx| ctx.event_state.event.is_some());
assert_eq!(has_event, Some(true));
with_render_context_mut(|ctx| {
ctx.clear_event();
});
let has_event = with_render_context(|ctx| ctx.event_state.event.is_some());
assert_eq!(has_event, Some(false));
teardown();
}
#[test]
fn test_begin_and_end_batch() {
setup();
with_render_context_mut(|ctx| {
assert!(!ctx.state_batch.is_batching());
ctx.begin_batch();
assert!(ctx.state_batch.is_batching());
});
with_render_context_mut(|ctx| {
let dirty = ctx.end_batch();
assert!(dirty.is_empty());
assert!(!ctx.state_batch.is_batching());
});
teardown();
}
#[test]
fn test_prepare_for_render() {
setup();
with_render_context_mut(|ctx| {
let fiber_id = ctx.fiber_tree.mount(None, None);
ctx.fiber_tree.begin_render(fiber_id);
if let Some(fiber) = ctx.fiber_tree.get_mut(fiber_id) {
fiber.next_hook_index();
fiber.next_hook_index();
}
ctx.fiber_tree.end_render();
assert_eq!(ctx.fiber_tree.get(fiber_id).unwrap().hook_index, 2);
ctx.prepare_for_render();
assert_eq!(ctx.fiber_tree.get(fiber_id).unwrap().hook_index, 0);
});
teardown();
}
#[test]
fn test_mark_unseen_for_unmount() {
setup();
with_render_context_mut(|ctx| {
let fiber_id = ctx.fiber_tree.get_or_create_fiber_by_component_id(42);
ctx.fiber_tree.mark_unseen_for_unmount();
assert!(!ctx.fiber_tree.pending_unmount.contains(&fiber_id));
ctx.fiber_tree.mark_unseen_for_unmount();
assert!(ctx.fiber_tree.pending_unmount.contains(&fiber_id));
});
teardown();
}
#[test]
fn test_process_unmounts_cleans_up_context() {
setup();
with_render_context_mut(|ctx| {
let fiber_id = ctx.fiber_tree.mount(None, None);
ctx.context_stack.push(fiber_id, "test-value".to_string());
assert_eq!(
ctx.context_stack.get::<String>(),
Some("test-value".to_string())
);
ctx.fiber_tree.schedule_unmount(fiber_id);
ctx.process_unmounts();
assert_eq!(ctx.context_stack.get::<String>(), None);
});
teardown();
}
#[test]
fn test_clear_resets_all_state() {
setup();
with_render_context_mut(|ctx| {
ctx.fiber_tree.mount(None, None);
ctx.state_batch.begin_batch();
ctx.context_stack.push(crate::fiber::FiberId(1), 42i32);
ctx.clear();
assert!(ctx.fiber_tree.fibers.is_empty());
assert!(!ctx.state_batch.is_batching());
assert!(!ctx.context_stack.has::<i32>());
});
teardown();
}
#[test]
fn test_fallback_to_legacy_fiber_tree() {
clear_render_context();
crate::fiber_tree::set_fiber_tree(crate::fiber_tree::FiberTree::new());
let count = crate::fiber_tree::with_fiber_tree(|tree| tree.fibers.len());
assert_eq!(count, Some(0));
crate::fiber_tree::clear_fiber_tree();
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
use std::collections::HashMap;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_fiber_lifecycle_consistency(
component_ids in prop::collection::vec(1u64..10000, 1..20),
render_passes in 1usize..5
) {
clear_render_context();
init_render_context();
let mut created_fibers: HashMap<u64, crate::fiber::FiberId> = HashMap::new();
for _pass in 0..render_passes {
for &component_id in &component_ids {
with_render_context_mut(|ctx| {
let fiber_id = ctx.fiber_tree.get_or_create_fiber_by_component_id(component_id);
if let Some(&existing_fiber_id) = created_fibers.get(&component_id) {
prop_assert_eq!(fiber_id, existing_fiber_id,
"Component ID {} should always map to same fiber", component_id);
} else {
created_fibers.insert(component_id, fiber_id);
}
prop_assert!(ctx.fiber_tree.fibers.contains_key(&fiber_id),
"Fiber {} should exist in tree", fiber_id.0);
ctx.fiber_tree.begin_render(fiber_id);
prop_assert_eq!(ctx.fiber_tree.current_fiber(), Some(fiber_id),
"Current fiber should be {} during render", fiber_id.0);
ctx.fiber_tree.end_render();
prop_assert!(ctx.fiber_tree.current_fiber().is_none(),
"Current fiber should be None after end_render");
Ok(())
}).unwrap()?;
}
with_render_context_mut(|ctx| {
ctx.fiber_tree.mark_unseen_for_unmount();
});
}
clear_render_context();
}
#[test]
fn prop_unseen_fibers_scheduled_for_unmount_via_context(
initial_ids in prop::collection::vec(1u64..10000, 1..10),
surviving_ids in prop::collection::vec(1u64..10000, 0..5)
) {
clear_render_context();
init_render_context();
let mut fiber_map: HashMap<u64, crate::fiber::FiberId> = HashMap::new();
with_render_context_mut(|ctx| {
for &id in &initial_ids {
let fiber_id = ctx.fiber_tree.get_or_create_fiber_by_component_id(id);
fiber_map.insert(id, fiber_id);
}
ctx.fiber_tree.mark_unseen_for_unmount();
});
with_render_context_mut(|ctx| {
for &id in &surviving_ids {
ctx.fiber_tree.get_or_create_fiber_by_component_id(id);
}
ctx.fiber_tree.mark_unseen_for_unmount();
});
with_render_context(|ctx| {
for (&component_id, &fiber_id) in &fiber_map {
let should_survive = surviving_ids.contains(&component_id);
let is_pending_unmount = ctx.fiber_tree.pending_unmount.contains(&fiber_id);
if !should_survive {
prop_assert!(is_pending_unmount,
"Fiber for component {} should be scheduled for unmount", component_id);
}
}
Ok(())
}).unwrap()?;
clear_render_context();
}
#[test]
fn prop_context_cleanup_on_unmount_via_context(
fiber_count in 1usize..10,
context_values in prop::collection::vec(1i32..1000, 1..10)
) {
clear_render_context();
init_render_context();
let mut fiber_ids = Vec::new();
with_render_context_mut(|ctx| {
for i in 0..fiber_count {
let fiber_id = ctx.fiber_tree.mount(None, None);
fiber_ids.push(fiber_id);
if i < context_values.len() {
ctx.context_stack.push(fiber_id, context_values[i]);
}
}
});
with_render_context(|ctx| {
if !context_values.is_empty() {
prop_assert!(ctx.context_stack.has::<i32>(),
"Context should have i32 values");
}
Ok(())
}).unwrap()?;
with_render_context_mut(|ctx| {
for &fiber_id in &fiber_ids {
ctx.fiber_tree.schedule_unmount(fiber_id);
}
ctx.process_unmounts();
});
with_render_context(|ctx| {
prop_assert!(!ctx.context_stack.has::<i32>(),
"Context should be empty after unmount");
Ok(())
}).unwrap()?;
clear_render_context();
}
#[test]
fn prop_state_batching_via_context(
update_count in 1usize..10
) {
use crate::scheduler::batch::{StateUpdate, StateUpdateKind};
clear_render_context();
init_render_context();
let fiber_id = with_render_context_mut(|ctx| {
let fiber_id = ctx.fiber_tree.mount(None, None);
ctx.fiber_tree.get_mut(fiber_id).unwrap().set_hook(0, 0i32);
fiber_id
}).unwrap();
with_render_context_mut(|ctx| {
ctx.begin_batch();
for i in 0..update_count {
ctx.state_batch.queue_update(
fiber_id,
StateUpdate {
hook_index: 0,
update: StateUpdateKind::Value(Box::new(i as i32)),
},
);
}
prop_assert!(ctx.state_batch.is_batching(),
"Batching should be active");
let dirty = ctx.end_batch();
prop_assert!(dirty.contains(&fiber_id),
"Fiber should be marked dirty");
let final_value = ctx.fiber_tree.get(fiber_id).unwrap().get_hook::<i32>(0);
prop_assert_eq!(final_value, Some((update_count - 1) as i32),
"Final value should be the last update");
Ok(())
}).unwrap()?;
clear_render_context();
}
#[test]
fn prop_event_available_to_all_fibers_via_context(
fiber_count in 2usize..10
) {
clear_render_context();
init_render_context();
let event = Event::FocusGained;
with_render_context_mut(|ctx| {
ctx.set_event(Some(Arc::new(event)));
});
let fiber_ids: Vec<_> = with_render_context_mut(|ctx| {
(0..fiber_count).map(|_| {
ctx.fiber_tree.mount(None, None)
}).collect()
}).unwrap_or_default();
for &fiber_id in &fiber_ids {
let result = with_render_context_mut(|ctx| {
ctx.fiber_tree.begin_render(fiber_id);
let event = ctx.event_state.event.clone();
ctx.fiber_tree.end_render();
event
});
prop_assert!(result.flatten().is_some(),
"Fiber {:?} should be able to read the event", fiber_id);
}
if let Some(&fiber_id) = fiber_ids.first() {
for read_num in 0..3 {
let result = with_render_context_mut(|ctx| {
ctx.fiber_tree.begin_render(fiber_id);
let event = ctx.event_state.event.clone();
ctx.fiber_tree.end_render();
event
});
prop_assert!(result.flatten().is_some(),
"Fiber should read event on attempt {}", read_num);
}
}
if fiber_ids.len() >= 2 {
let stopping_fiber = fiber_ids[0];
let other_fiber = fiber_ids[1];
with_render_context_mut(|ctx| {
ctx.fiber_tree.begin_render(stopping_fiber);
ctx.event_state.propagation_stopped = true;
ctx.event_state.stopped_by_fiber = Some(stopping_fiber);
ctx.fiber_tree.end_render();
});
let stopping_result = with_render_context_mut(|ctx| {
ctx.fiber_tree.begin_render(stopping_fiber);
let can_read = !ctx.event_state.propagation_stopped
|| ctx.event_state.stopped_by_fiber == Some(stopping_fiber);
let event = if can_read { ctx.event_state.event.clone() } else { None };
ctx.fiber_tree.end_render();
event
});
prop_assert!(stopping_result.flatten().is_some(),
"Stopping fiber should still read event");
let other_result = with_render_context_mut(|ctx| {
ctx.fiber_tree.begin_render(other_fiber);
let can_read = !ctx.event_state.propagation_stopped
|| ctx.event_state.stopped_by_fiber == Some(other_fiber);
let event = if can_read { ctx.event_state.event.clone() } else { None };
ctx.fiber_tree.end_render();
event
});
prop_assert!(other_result.flatten().is_none(),
"Other fiber should NOT read event after stop_propagation");
}
clear_render_context();
}
}
}