fenrix_core/
lib.rs

1use std::any::{Any, TypeId};
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::rc::Rc;
5
6// The internal state of a signal.
7struct Signal<T> {
8    value: T,
9    subscribers: Vec<Rc<dyn Fn()>>,
10}
11
12type AnySignal = Rc<dyn Any>;
13
14// The context for a single component instance, managing its hooks.
15#[derive(Default)]
16struct ComponentContext {
17    states: Vec<AnySignal>,
18    effects: Vec<()>, // Use a unit type to simply mark that an effect has been created.
19    state_index: usize,
20    effect_index: usize,
21}
22
23// A thread-local stack to manage nested component renders.
24thread_local! {
25    static CONTEXT_STACK: RefCell<Vec<ComponentContext>> = RefCell::new(Vec::new());
26    static CURRENT_EFFECT: RefCell<Option<Rc<dyn Fn()>>> = RefCell::new(None);
27}
28
29/// Provides a piece of state for a component.
30pub fn use_state<T: Clone + 'static>(
31    initial_value_fn: impl FnOnce() -> T,
32) -> (impl Fn() -> T + Clone, impl Fn(T) + Clone) {
33    CONTEXT_STACK.with(|stack| {
34        let mut stack = stack.borrow_mut();
35        let current_context = stack
36            .last_mut()
37            .expect("`use_state` can only be called inside a component.");
38
39        if let Some(any_signal) = current_context.states.get(current_context.state_index) {
40            current_context.state_index += 1;
41            let signal = any_signal
42                .clone()
43                .downcast::<RefCell<Signal<T>>>()
44                .expect("Mismatched state type in `use_state` hook.");
45            return create_signal_from_rc(signal);
46        }
47
48        let initial_value = initial_value_fn();
49        let signal = Rc::new(RefCell::new(Signal {
50            value: initial_value,
51            subscribers: Vec::new(),
52        }));
53
54        current_context.states.push(signal.clone() as AnySignal);
55        current_context.state_index += 1;
56        create_signal_from_rc(signal)
57    })
58}
59
60/// A reactive signal that holds a value.
61pub fn create_signal<T: Clone + 'static>(
62    initial_value: T,
63) -> (impl Fn() -> T + Clone, impl Fn(T) + Clone) {
64    let signal = Rc::new(RefCell::new(Signal {
65        value: initial_value,
66        subscribers: Vec::new(),
67    }));
68    create_signal_from_rc(signal)
69}
70
71// Helper to create getter/setter from a signal Rc.
72fn create_signal_from_rc<T: Clone + 'static>(
73    signal: Rc<RefCell<Signal<T>>>,
74) -> (impl Fn() -> T + Clone, impl Fn(T) + Clone) {
75    let getter = {
76        let signal = Rc::clone(&signal);
77        move || {
78            CURRENT_EFFECT.with(|e| {
79                if let Some(effect) = e.borrow().clone() {
80                    let mut s = signal.borrow_mut();
81                    if !s.subscribers.iter().any(|s_rc| Rc::ptr_eq(s_rc, &effect)) {
82                        s.subscribers.push(effect);
83                    }
84                }
85            });
86            signal.borrow().value.clone()
87        }
88    };
89
90    let setter = {
91        let signal = Rc::clone(&signal);
92        move |new_value: T| {
93            let subscribers = {
94                let mut s = signal.borrow_mut();
95                s.value = new_value;
96                s.subscribers.clone()
97            };
98            for effect in subscribers {
99                effect();
100            }
101        }
102    };
103
104    (getter, setter)
105}
106
107/// Creates an effect that runs once and re-runs when its dependencies change.
108pub fn use_effect(effect_fn: impl Fn() + 'static) {
109    CONTEXT_STACK.with(|stack| {
110        let mut stack = stack.borrow_mut();
111        let current_context = stack
112            .last_mut()
113            .expect("`use_effect` can only be called inside a component.");
114
115        if current_context.effects.get(current_context.effect_index).is_some() {
116            current_context.effect_index += 1;
117            return;
118        }
119
120        create_effect(effect_fn);
121        current_context.effects.push(());
122        current_context.effect_index += 1;
123    })
124}
125
126/// Creates an effect that re-runs when its dependencies change.
127pub fn create_effect(effect_fn: impl Fn() + 'static) {
128    let effect = Rc::new(move || {
129        effect_fn();
130    });
131
132    CURRENT_EFFECT.with(|e| e.borrow_mut().replace(effect.clone()));
133    effect();
134    CURRENT_EFFECT.with(|e| e.borrow_mut().take());
135}
136
137/// A helper function to be called by the `#[component]` macro.
138pub fn with_component_context<F, R>(f: F) -> R
139where
140    F: FnOnce() -> R,
141{
142    CONTEXT_STACK.with(|s| s.borrow_mut().push(ComponentContext::default()));
143    let result = f();
144    CONTEXT_STACK.with(|s| s.borrow_mut().pop());
145    result
146}
147
148// A container for dependency-injected services.
149#[derive(Default)]
150pub struct ServiceContainer {
151    services: HashMap<TypeId, Rc<dyn Any>>,
152}
153
154impl ServiceContainer {
155    pub fn new() -> Self {
156        Self::default()
157    }
158}
159
160thread_local! {
161    // A thread-local static container for services.
162    static SERVICE_CONTAINER: RefCell<ServiceContainer> = RefCell::new(ServiceContainer::new());
163}
164
165/// Provides a service to the application's global DI container.
166///
167/// The service is stored by its type, so only one instance of any
168/// given type can be provided.
169pub fn provide_service<T: 'static>(service: T) {
170    SERVICE_CONTAINER.with(|sc| {
171        sc.borrow_mut()
172            .services
173            .insert(TypeId::of::<T>(), Rc::new(service));
174    });
175}
176
177/// Injects a service from the global DI container.
178///
179/// This hook retrieves a shared reference (`Rc`) to a service that was
180/// previously provided via `provide_service`.
181///
182/// # Panics
183///
184/// This function will panic if the requested service (`T`) has not been
185/// provided, or if the stored service cannot be downcast to the requested type.
186pub fn inject<T: 'static>() -> Rc<T> {
187    SERVICE_CONTAINER.with(|sc| {
188        let sc = sc.borrow();
189        let service = sc
190            .services
191            .get(&TypeId::of::<T>())
192            .expect("Service not provided: Make sure to call `provide_service` before `inject`.");
193
194        service
195            .clone()
196            .downcast::<T>()
197            .expect("Failed to downcast service to the requested type.")
198    })
199}