extern crate alloc;
use alloc::collections::{BTreeMap, VecDeque};
use alloc::rc::{Rc, Weak};
use alloc::vec::Vec;
use core::cell::RefCell;
use crate::ecs::{Entity, World};
use crate::ui::dirty::Dirty;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Subscriber {
Widget(Entity),
Effect(EffectId),
Computed(ComputedId),
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct EffectId(usize);
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct ComputedId(usize);
struct Reactive {
scope: Option<Subscriber>,
world: *mut World,
dirty_widgets: VecDeque<Entity>,
dirty_effects: VecDeque<EffectId>,
effects: BTreeMap<EffectId, Rc<RefCell<EffectInner>>>,
computeds: BTreeMap<ComputedId, Weak<dyn ComputedNode>>,
}
impl Reactive {
const fn new() -> Self {
Reactive {
scope: None,
world: core::ptr::null_mut(),
dirty_widgets: VecDeque::new(),
dirty_effects: VecDeque::new(),
effects: BTreeMap::new(),
computeds: BTreeMap::new(),
}
}
}
#[cfg(feature = "std")]
std::thread_local! {
static RT: RefCell<Reactive> = const { RefCell::new(Reactive::new()) };
}
fn with_reactive<R>(f: impl FnOnce(&mut Reactive) -> R) -> R {
#[cfg(feature = "std")]
{
RT.with(|rt| f(&mut rt.borrow_mut()))
}
#[cfg(not(feature = "std"))]
{
static mut RT: Reactive = Reactive::new();
critical_section::with(|_| {
#[allow(static_mut_refs)]
unsafe {
f(&mut RT)
}
})
}
}
fn try_with_reactive<R>(f: impl FnOnce(&mut Reactive) -> R) -> Option<R> {
#[cfg(feature = "std")]
{
RT.try_with(|rt| f(&mut rt.borrow_mut())).ok()
}
#[cfg(not(feature = "std"))]
{
Some(with_reactive(f))
}
}
fn current_scope() -> Option<Subscriber> {
with_reactive(|r| r.scope)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn with_scope<R>(scope: Subscriber, f: impl FnOnce() -> R) -> R {
let prev = with_reactive(|r| r.scope.replace(scope));
let out = f();
with_reactive(|r| r.scope = prev);
out
}
fn enqueue_widget(entity: Entity) {
with_reactive(|r| r.dirty_widgets.push_back(entity));
}
fn enqueue_effect(id: EffectId) {
with_reactive(|r| r.dirty_effects.push_back(id));
}
struct WorldGuard {
prev: *mut World,
}
impl WorldGuard {
fn enter(world: &mut World) -> Self {
let prev = with_reactive(|r| core::mem::replace(&mut r.world, world as *mut World));
WorldGuard { prev }
}
}
impl Drop for WorldGuard {
fn drop(&mut self) {
let prev = self.prev;
with_reactive(|r| r.world = prev);
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn with_world_scope<R>(world: &mut World, f: impl FnOnce() -> R) -> R {
let _guard = WorldGuard::enter(world);
f()
}
pub fn with_world<R>(f: impl FnOnce(&mut World) -> R) -> Option<R> {
let ptr = with_reactive(|r| r.world);
if ptr.is_null() {
return None;
}
Some(f(unsafe { &mut *ptr }))
}
const FLUSH_MAX_PASSES: u32 = 32;
pub fn flush_signal_dirty(world: &mut World) {
let _guard = WorldGuard::enter(world);
reclaim_dead_effects(world);
for _ in 0..FLUSH_MAX_PASSES {
let effects: Vec<EffectId> = with_reactive(|r| r.dirty_effects.drain(..).collect());
for id in &effects {
run_effect(*id);
}
let widgets: Vec<Entity> = with_reactive(|r| r.dirty_widgets.drain(..).collect());
for entity in &widgets {
if world.is_alive(*entity) {
world.insert(*entity, Dirty);
}
}
let settled = with_reactive(|r| r.dirty_effects.is_empty() && r.dirty_widgets.is_empty());
if effects.is_empty() && widgets.is_empty() && settled {
return;
}
}
debug_assert!(
false,
"reactive flush did not settle in {FLUSH_MAX_PASSES} passes (cycle?)"
);
}
fn reclaim_dead_effects(world: &World) {
let dead: Vec<EffectId> = with_reactive(|r| {
r.effects
.iter()
.filter(|(_, e)| e.borrow().owner_entity.is_some_and(|o| !world.is_alive(o)))
.map(|(id, _)| *id)
.collect()
});
for id in dead {
with_reactive(|r| r.effects.remove(&id));
}
with_reactive(|r| r.computeds.retain(|_, w| w.strong_count() > 0));
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn cleanup_effects_for(entity: Entity) {
let bound: Vec<EffectId> = with_reactive(|r| {
r.effects
.iter()
.filter(|(_, e)| e.borrow().owner_entity == Some(entity))
.map(|(id, _)| *id)
.collect()
});
for id in bound {
with_reactive(|r| r.effects.remove(&id));
}
}
struct SignalInner<T> {
value: T,
subscribers: Vec<Subscriber>,
}
pub struct Signal<T: 'static> {
inner: Rc<RefCell<SignalInner<T>>>,
}
impl<T: 'static> Clone for Signal<T> {
fn clone(&self) -> Self {
Signal {
inner: Rc::clone(&self.inner),
}
}
}
impl<T: 'static> Signal<T> {
pub fn new(initial: T) -> Self {
Signal {
inner: Rc::new(RefCell::new(SignalInner {
value: initial,
subscribers: Vec::new(),
})),
}
}
fn track(&self) {
if let Some(sub) = current_scope() {
let mut inner = self.inner.borrow_mut();
if !inner.subscribers.contains(&sub) {
inner.subscribers.push(sub);
}
}
}
pub fn get(&self) -> T
where
T: Clone,
{
self.track();
self.inner.borrow().value.clone()
}
pub fn get_untracked(&self) -> T
where
T: Clone,
{
self.inner.borrow().value.clone()
}
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
self.track();
f(&self.inner.borrow().value)
}
pub fn set(&self, value: T) {
{
let mut inner = self.inner.borrow_mut();
inner.value = value;
}
self.notify();
}
pub fn update(&self, f: impl FnOnce(&mut T)) {
{
let mut inner = self.inner.borrow_mut();
f(&mut inner.value);
}
self.notify();
}
fn notify(&self) {
let subs: Vec<Subscriber> = self.inner.borrow().subscribers.clone();
for sub in subs {
propagate(sub);
}
}
}
fn propagate(sub: Subscriber) {
match sub {
Subscriber::Widget(entity) => enqueue_widget(entity),
Subscriber::Effect(id) => enqueue_effect(id),
Subscriber::Computed(id) => mark_computed_dirty(id),
}
}
fn mark_computed_dirty(id: ComputedId) {
let node = with_reactive(|r| r.computeds.get(&id).and_then(Weak::upgrade));
let Some(node) = node else {
try_with_reactive(|r| r.computeds.remove(&id));
return;
};
let already_dirty = node.mark_dirty_take_was_dirty();
if already_dirty {
return;
}
for sub in node.subscribers() {
propagate(sub);
}
}
struct EffectInner {
run: Rc<dyn Fn()>,
owner_entity: Option<Entity>,
}
pub struct Effect {
id: EffectId,
}
impl Effect {
pub fn new(f: impl Fn() + 'static) -> Effect {
Self::spawn(f, None)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn owner(&self) -> Option<Entity> {
with_reactive(|r| {
r.effects
.get(&self.id)
.and_then(|e| e.borrow().owner_entity)
})
}
fn spawn(f: impl Fn() + 'static, owner_entity: Option<Entity>) -> Effect {
let inner = Rc::new(RefCell::new(EffectInner {
run: Rc::new(f),
owner_entity,
}));
let id = EffectId(Rc::as_ptr(&inner) as *const () as usize);
with_reactive(|r| {
r.effects.insert(id, inner);
});
run_effect(id);
Effect { id }
}
pub fn dispose(self) {}
}
impl Drop for Effect {
fn drop(&mut self) {
try_with_reactive(|r| {
r.effects.remove(&self.id);
});
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn effect_with_widget(entity: Entity, f: impl Fn() + 'static) {
core::mem::forget(Effect::spawn(f, Some(entity)));
}
fn run_effect(id: EffectId) {
let run = with_reactive(|r| r.effects.get(&id).map(|e| Rc::clone(&e.borrow().run)));
if let Some(run) = run {
with_scope(Subscriber::Effect(id), || run());
}
}
trait ComputedNode {
fn mark_dirty_take_was_dirty(&self) -> bool;
fn subscribers(&self) -> Vec<Subscriber>;
}
struct ComputedInner<T> {
value: Option<T>,
compute: alloc::boxed::Box<dyn Fn() -> T>,
subscribers: Vec<Subscriber>,
dirty: bool,
}
impl<T> ComputedNode for RefCell<ComputedInner<T>> {
fn mark_dirty_take_was_dirty(&self) -> bool {
let was = self.borrow().dirty;
self.borrow_mut().dirty = true;
was
}
fn subscribers(&self) -> Vec<Subscriber> {
self.borrow().subscribers.clone()
}
}
pub struct Computed<T: 'static> {
inner: Rc<RefCell<ComputedInner<T>>>,
}
impl<T: 'static> Clone for Computed<T> {
fn clone(&self) -> Self {
Computed {
inner: Rc::clone(&self.inner),
}
}
}
impl<T: 'static> Computed<T> {
pub fn new(f: impl Fn() -> T + 'static) -> Self {
let inner = Rc::new(RefCell::new(ComputedInner {
value: None,
compute: alloc::boxed::Box::new(f),
subscribers: Vec::new(),
dirty: true,
}));
let id = ComputedId(Rc::as_ptr(&inner) as *const () as usize);
let node: Rc<dyn ComputedNode> = inner.clone();
with_reactive(|r| {
r.computeds.insert(id, Rc::downgrade(&node));
});
Computed { inner }
}
fn id(&self) -> ComputedId {
ComputedId(Rc::as_ptr(&self.inner) as *const () as usize)
}
pub fn get(&self) -> T
where
T: Clone,
{
let id = self.id();
if let Some(sub) = current_scope() {
let mut inner = self.inner.borrow_mut();
if !inner.subscribers.contains(&sub) {
inner.subscribers.push(sub);
}
}
if self.inner.borrow().dirty {
let value = with_scope(Subscriber::Computed(id), || (self.inner.borrow().compute)());
let mut inner = self.inner.borrow_mut();
inner.value = Some(value);
inner.dirty = false;
}
self.inner
.borrow()
.value
.clone()
.expect("computed value populated after recompute")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reset() {
with_reactive(|r| {
r.scope = None;
r.dirty_widgets.clear();
r.dirty_effects.clear();
r.effects.clear();
r.computeds.clear();
});
}
fn entity(id: u32) -> Entity {
Entity { id, generation: 0 }
}
fn drain_effects() {
loop {
let batch: Vec<EffectId> = with_reactive(|r| r.dirty_effects.drain(..).collect());
if batch.is_empty() {
break;
}
for id in batch {
run_effect(id);
}
}
}
#[test]
fn get_set_update_roundtrip() {
let s = Signal::new(1i32);
assert_eq!(s.get(), 1);
s.set(5);
assert_eq!(s.get(), 5);
s.update(|n| *n += 3);
assert_eq!(s.get(), 8);
}
#[test]
fn with_reads_without_clone() {
let s = Signal::new(alloc::string::String::from("hi"));
let len = s.with(|v| v.len());
assert_eq!(len, 2);
}
#[test]
fn clone_shares_state() {
let a = Signal::new(0i32);
let b = a.clone();
a.set(42);
assert_eq!(b.get(), 42);
}
#[test]
fn read_in_scope_subscribes_and_set_enqueues() {
reset();
let s = Signal::new(0i32);
let w = entity(1);
with_scope(Subscriber::Widget(w), || {
let _ = s.get();
});
s.set(1);
with_reactive(|r| {
assert_eq!(r.dirty_widgets.len(), 1);
assert_eq!(r.dirty_widgets[0], w);
r.dirty_widgets.clear();
});
}
#[test]
fn get_untracked_does_not_subscribe() {
reset();
let s = Signal::new(0i32);
let w = entity(2);
with_scope(Subscriber::Widget(w), || {
let _ = s.get_untracked();
});
s.set(1);
with_reactive(|r| {
assert!(r.dirty_widgets.is_empty());
});
}
#[test]
fn repeated_reads_dedup_subscriber() {
reset();
let s = Signal::new(0i32);
let w = entity(3);
with_scope(Subscriber::Widget(w), || {
let _ = s.get();
let _ = s.get();
let _ = s.get();
});
assert_eq!(s.inner.borrow().subscribers.len(), 1);
s.set(1);
with_reactive(|r| {
assert_eq!(r.dirty_widgets.len(), 1);
r.dirty_widgets.clear();
});
}
#[test]
fn read_outside_scope_no_subscribe() {
reset();
let s = Signal::new(0i32);
let _ = s.get();
s.set(1);
with_reactive(|r| assert!(r.dirty_widgets.is_empty()));
}
#[test]
fn nested_scope_restores_previous() {
let outer = entity(10);
let inner = entity(11);
let s_outer = Signal::new(0i32);
let s_inner = Signal::new(0i32);
with_scope(Subscriber::Widget(outer), || {
let _ = s_outer.get();
with_scope(Subscriber::Widget(inner), || {
let _ = s_inner.get();
});
let _ = s_outer.get();
});
assert_eq!(
s_outer.inner.borrow().subscribers,
alloc::vec![Subscriber::Widget(outer)]
);
assert_eq!(
s_inner.inner.borrow().subscribers,
alloc::vec![Subscriber::Widget(inner)]
);
}
#[test]
fn effect_runs_once_on_creation() {
reset();
let runs = Rc::new(RefCell::new(0));
let r = Rc::clone(&runs);
let _e = Effect::new(move || *r.borrow_mut() += 1);
assert_eq!(*runs.borrow(), 1);
}
#[test]
fn effect_reruns_on_dependency_change() {
reset();
let s = Signal::new(0i32);
let seen = Rc::new(RefCell::new(alloc::vec::Vec::<i32>::new()));
let (sc, seenc) = (s.clone(), Rc::clone(&seen));
let _e = Effect::new(move || seenc.borrow_mut().push(sc.get()));
assert_eq!(*seen.borrow(), alloc::vec![0]);
s.set(7);
drain_effects();
assert_eq!(*seen.borrow(), alloc::vec![0, 7]);
}
#[test]
fn disposed_effect_stops_rerunning() {
reset();
let s = Signal::new(0i32);
let runs = Rc::new(RefCell::new(0));
let (sc, rc) = (s.clone(), Rc::clone(&runs));
let e = Effect::new(move || {
let _ = sc.get();
*rc.borrow_mut() += 1;
});
assert_eq!(*runs.borrow(), 1);
e.dispose();
s.set(1);
drain_effects();
assert_eq!(*runs.borrow(), 1, "disposed effect must not re-run");
}
#[test]
fn spawn_records_owner_entity() {
reset();
let w = entity(99);
let owned = Effect::spawn(|| {}, Some(w));
assert_eq!(owned.owner(), Some(w));
let standalone = Effect::new(|| {});
assert_eq!(standalone.owner(), None);
}
#[test]
fn effect_setting_signal_during_flush_does_not_deadlock() {
reset();
let trigger = Signal::new(0i32);
let target = Signal::new(0i32);
let (tc, gc) = (trigger.clone(), target.clone());
let _e = Effect::new(move || {
let v = tc.get();
if v > 0 {
gc.set(v * 2);
}
});
trigger.set(5);
drain_effects();
assert_eq!(target.get_untracked(), 10);
}
#[test]
fn computed_derives_and_recomputes() {
reset();
let n = Signal::new(2i32);
let nc = n.clone();
let doubled = Computed::new(move || nc.get() * 2);
assert_eq!(doubled.get(), 4);
n.set(5);
assert_eq!(doubled.get(), 10);
}
#[test]
fn computed_is_lazy_until_get() {
reset();
let n = Signal::new(1i32);
let calls = Rc::new(RefCell::new(0));
let (nc, cc) = (n.clone(), Rc::clone(&calls));
let c = Computed::new(move || {
*cc.borrow_mut() += 1;
nc.get()
});
assert_eq!(*calls.borrow(), 0, "no compute before first get");
let _ = c.get();
assert_eq!(*calls.borrow(), 1);
let _ = c.get();
assert_eq!(*calls.borrow(), 1, "clean re-get does not recompute");
n.set(2);
let _ = c.get();
assert_eq!(*calls.borrow(), 2, "recompute only after a source change");
}
#[test]
fn computed_source_change_dirties_subscribing_widget() {
reset();
let n = Signal::new(0i32);
let nc = n.clone();
let c = Computed::new(move || nc.get() + 1);
let w = entity(7);
with_scope(Subscriber::Widget(w), || {
let _ = c.get();
});
n.set(9);
with_reactive(|r| {
assert!(
r.dirty_widgets.contains(&w),
"source change cascades to widget via computed"
);
r.dirty_widgets.clear();
});
}
#[test]
fn chained_computeds_propagate() {
reset();
let n = Signal::new(1i32);
let nc = n.clone();
let a = Computed::new(move || nc.get() + 1);
let ac = a.clone();
let b = Computed::new(move || ac.get() * 10);
assert_eq!(b.get(), 20);
n.set(4);
assert_eq!(b.get(), 50, "change flows source -> a -> b");
}
fn effect_count() -> usize {
with_reactive(|r| r.effects.len())
}
#[test]
fn despawned_widget_effect_is_reclaimed_on_flush() {
reset();
let mut world = World::new();
let e = world.spawn_empty();
let s = Signal::new(0i32);
let sc = s.clone();
with_world_scope(&mut world, || {
effect_with_widget(e, move || {
let _ = sc.get();
})
});
assert_eq!(effect_count(), 1);
world.despawn(e);
flush_signal_dirty(&mut world);
assert_eq!(effect_count(), 0, "sweep drops the dead-owner effect");
}
#[test]
fn live_widget_effect_survives_flush() {
reset();
let mut world = World::new();
let e = world.spawn_empty();
let s = Signal::new(0i32);
let sc = s.clone();
with_world_scope(&mut world, || {
effect_with_widget(e, move || {
let _ = sc.get();
})
});
flush_signal_dirty(&mut world);
assert_eq!(effect_count(), 1, "live owner keeps its effect");
}
#[test]
fn dispose_unregisters_standalone_effect() {
reset();
let eff = Effect::new(|| {});
assert_eq!(effect_count(), 1);
eff.dispose();
assert_eq!(effect_count(), 0);
}
#[test]
fn self_feeding_effect_terminates_within_cap() {
reset();
let mut world = World::new();
let s = Signal::new(0i32);
let sc = s.clone();
core::mem::forget(Effect::new(move || {
let v = sc.get();
if v < 5 {
sc.set(v + 1);
}
}));
s.set(1);
flush_signal_dirty(&mut world); }
#[test]
fn nested_world_scope_restores_outer_pointer() {
reset();
let mut outer = World::new();
with_world_scope(&mut outer, || {
let mut inner = World::new();
with_world_scope(&mut inner, || {
assert!(with_world(|_| ()).is_some(), "inner scope sees a world");
});
assert!(
with_world(|_| ()).is_some(),
"outer scope still reachable after inner drops",
);
});
assert!(with_world(|_| ()).is_none(), "no world outside any scope");
}
}