use std::marker::PhantomData;
use crate::fiber::FiberId;
use crate::fiber_tree::with_current_fiber;
use crate::scheduler::batch::{StateUpdate, StateUpdateKind, queue_update};
#[derive(Debug)]
pub struct StateSetter<T> {
pub(crate) fiber_id: FiberId,
pub(crate) hook_index: usize,
pub(crate) _marker: PhantomData<T>,
}
impl<T> Clone for StateSetter<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for StateSetter<T> {}
impl<T: Clone + Send + 'static> StateSetter<T> {
pub fn set(&self, new_value: T) {
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::Value(Box::new(new_value)),
},
);
}
pub fn update<F>(&self, updater: F)
where
F: FnOnce(&T) -> T + Send + 'static,
{
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::Updater(Box::new(move |any| {
let current = any.downcast_ref::<T>().expect("State type mismatch");
Box::new(updater(current))
})),
},
);
}
}
impl<T: Clone + Send + PartialEq + 'static> StateSetter<T> {
pub fn set_if_changed(&self, new_value: T) {
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::ValueIfChanged {
value: Box::new(new_value),
eq_check: Box::new(|old, new| {
let old = old.downcast_ref::<T>().expect("State type mismatch");
let new = new.downcast_ref::<T>().expect("State type mismatch");
old == new
}),
},
},
);
}
pub fn update_if_changed<F>(&self, updater: F)
where
F: FnOnce(&T) -> T + Send + 'static,
{
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::UpdaterIfChanged {
updater: Box::new(move |any| {
let current = any.downcast_ref::<T>().expect("State type mismatch");
Box::new(updater(current))
}),
eq_check: Box::new(|old, new| {
let old = old.downcast_ref::<T>().expect("State type mismatch");
let new = new.downcast_ref::<T>().expect("State type mismatch");
old == new
}),
},
},
);
}
}
pub fn use_state<T, F>(initializer: F) -> (T, StateSetter<T>)
where
T: Clone + Send + 'static,
F: FnOnce() -> T,
{
with_current_fiber(|fiber| {
fiber.track_hook_call("use_state");
let hook_index = fiber.next_hook_index();
let value = fiber.get_or_init_hook(hook_index, initializer);
let setter = StateSetter {
fiber_id: fiber.id,
hook_index,
_marker: PhantomData,
};
(value, setter)
})
.expect("use_state must be called within a component render context")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
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() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
}
#[test]
fn test_use_state_initial_value() {
let _fiber_id = setup_test_fiber();
let (value, _setter) = use_state(|| 42);
assert_eq!(value, 42);
cleanup_test();
}
#[test]
fn test_use_state_returns_same_value_on_rerender() {
let fiber_id = setup_test_fiber();
let (value1, _setter1) = use_state(|| 100);
assert_eq!(value1, 100);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let (value2, _setter2) = use_state(|| 999);
assert_eq!(value2, 100);
cleanup_test();
}
#[test]
fn test_state_setter_set_queues_update() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 0);
setter.set(42);
let has_updates =
crate::scheduler::batch::with_state_batch(|batch| batch.has_pending_updates());
assert!(has_updates);
let is_dirty =
crate::scheduler::batch::with_state_batch(|batch| batch.is_fiber_dirty(fiber_id));
assert!(is_dirty);
cleanup_test();
}
#[test]
fn test_state_setter_update_queues_functional_update() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 10);
setter.update(|n| n + 5);
let has_updates =
crate::scheduler::batch::with_state_batch(|batch| batch.has_pending_updates());
assert!(has_updates);
let is_dirty =
crate::scheduler::batch::with_state_batch(|batch| batch.is_fiber_dirty(fiber_id));
assert!(is_dirty);
cleanup_test();
}
#[test]
fn test_multiple_state_hooks() {
let _fiber_id = setup_test_fiber();
let (count, _set_count) = use_state(|| 0);
let (name, _set_name) = use_state(|| "Alice".to_string());
let (active, _set_active) = use_state(|| true);
assert_eq!(count, 0);
assert_eq!(name, "Alice");
assert!(active);
cleanup_test();
}
#[test]
fn test_state_setter_is_copy() {
let _fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 0);
let setter_copy = setter;
let _setter_copy2 = setter_copy;
let _setter_copy3 = setter;
cleanup_test();
}
#[test]
fn test_batched_updates_applied_correctly() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 0);
setter.set(10);
setter.update(|n| n + 5);
setter.update(|n| n * 2);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert!(dirty.contains(&fiber_id));
let fiber = tree.get(fiber_id).unwrap();
let final_value = fiber.get_hook::<i32>(0);
assert_eq!(final_value, Some(30));
});
cleanup_test();
}
#[test]
fn test_functional_updates_receive_latest_state() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 0);
for _ in 0..5 {
setter.update(|n| n + 1);
}
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
crate::scheduler::batch::with_state_batch_mut(|batch| {
batch.end_batch(tree);
});
let fiber = tree.get(fiber_id).unwrap();
let final_value = fiber.get_hook::<i32>(0);
assert_eq!(final_value, Some(5));
});
cleanup_test();
}
#[test]
fn test_state_setter_debug() {
let _fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 0);
let debug_str = format!("{:?}", setter);
assert!(debug_str.contains("StateSetter"));
cleanup_test();
}
#[test]
#[should_panic(expected = "use_state must be called within a component render context")]
fn test_use_state_panics_outside_render() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
let _ = use_state(|| 0);
}
#[test]
fn test_set_if_changed_skips_equal_value() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 42);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.mark_clean(fiber_id);
tree.begin_render(fiber_id);
});
crate::scheduler::batch::clear_state_batch();
setter.set_if_changed(42);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert!(!dirty.contains(&fiber_id));
assert!(!tree.get(fiber_id).unwrap().dirty);
});
cleanup_test();
}
#[test]
fn test_set_if_changed_updates_different_value() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 42);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.mark_clean(fiber_id);
tree.begin_render(fiber_id);
});
crate::scheduler::batch::clear_state_batch();
setter.set_if_changed(100);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert!(dirty.contains(&fiber_id));
assert!(tree.get(fiber_id).unwrap().dirty);
assert_eq!(tree.get(fiber_id).unwrap().get_hook::<i32>(0), Some(100));
});
cleanup_test();
}
#[test]
fn test_update_if_changed_skips_equal_result() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 5);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.mark_clean(fiber_id);
tree.begin_render(fiber_id);
});
crate::scheduler::batch::clear_state_batch();
setter.update_if_changed(|n| (*n).max(3));
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert!(!dirty.contains(&fiber_id));
assert!(!tree.get(fiber_id).unwrap().dirty);
assert_eq!(tree.get(fiber_id).unwrap().get_hook::<i32>(0), Some(5));
});
cleanup_test();
}
#[test]
fn test_update_if_changed_updates_different_result() {
let fiber_id = setup_test_fiber();
let (_value, setter) = use_state(|| 5);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.mark_clean(fiber_id);
tree.begin_render(fiber_id);
});
crate::scheduler::batch::clear_state_batch();
setter.update_if_changed(|n| (*n).max(10));
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
let dirty =
crate::scheduler::batch::with_state_batch_mut(|batch| batch.end_batch(tree));
assert!(dirty.contains(&fiber_id));
assert!(tree.get(fiber_id).unwrap().dirty);
assert_eq!(tree.get(fiber_id).unwrap().get_hook::<i32>(0), Some(10));
});
cleanup_test();
}
}