Skip to main content

euv_core/reactive/hook/
fn.rs

1use crate::*;
2
3/// Returns the currently active `HookContext`.
4///
5/// When called outside a `with_hook_context` scope, returns a reference
6/// to the default empty context.
7///
8/// # Returns
9///
10/// - `HookContext` - The currently active hook context.
11pub fn get_current_hook_context() -> HookContext {
12    let address: usize = unsafe { CURRENT_HOOK_CONTEXT };
13    if address == 0 {
14        init_current_hook_context();
15    }
16    let current: usize = unsafe { CURRENT_HOOK_CONTEXT };
17    current.into()
18}
19
20/// Runs a closure with the given `HookContext` set as the active context.
21///
22/// This is called by the renderer before invoking a `DynamicNode`'s
23/// render function, enabling `use_signal` and other hooks to access
24/// and persist state across re-renders.
25///
26/// # Arguments
27///
28/// - `HookContext` - The hook context to set as active.
29/// - `FnOnce() -> R` - The closure to execute with the active context.
30///
31/// # Returns
32///
33/// - `R` - The return value of the closure.
34pub fn with_hook_context<F, R>(context: HookContext, f: F) -> R
35where
36    F: FnOnce() -> R,
37{
38    let previous: usize = unsafe { CURRENT_HOOK_CONTEXT };
39    unsafe {
40        CURRENT_HOOK_CONTEXT = Into::<usize>::into(context);
41    }
42    let result: R = f();
43    unsafe {
44        CURRENT_HOOK_CONTEXT = previous;
45    }
46    result
47}
48
49/// Creates a new `HookContext` allocated via `Box::leak`.
50///
51/// The allocated memory lives for the remainder of the program and will
52/// never be freed. This is acceptable for WASM single-threaded contexts
53/// where `DynamicNode` instances persist for the application lifetime.
54///
55/// # Returns
56///
57/// - `HookContext` - A handle to the newly allocated hook context.
58pub fn create_hook_context() -> HookContext {
59    let ctx: Box<HookContextInner> = Box::default();
60    let ptr: *mut HookContextInner = Box::leak(ctx) as *mut HookContextInner;
61    let address: usize = ptr as usize;
62    address.into()
63}
64
65/// Creates a new reactive signal with the given initial value.
66///
67/// When called inside a `DynamicNode` render function (within a
68/// `with_hook_context` scope), the signal state is persisted across
69/// re-renders by storing it in the active `HookContext`. Subsequent
70/// re-renders return the same signal handle, preserving its current value.
71///
72/// When called outside a hook context, a fresh signal is created each time.
73///
74/// # Arguments
75///
76/// - `FnOnce() -> T` - A closure that returns the initial value of the signal.
77///
78/// # Returns
79///
80/// - `Signal<T>` - A mutable handle to the newly created or persisted reactive signal.
81pub fn use_signal<T, F>(init: F) -> Signal<T>
82where
83    T: Clone + PartialEq + 'static,
84    F: FnOnce() -> T,
85{
86    let ctx: HookContext = get_current_hook_context();
87    let ctx_inner: &mut HookContextInner = ctx.leak_mut();
88    let index: usize = ctx_inner.get_hook_index();
89    ctx_inner.set_hook_index(index + 1);
90    if index < ctx_inner.get_hooks().len()
91        && let Some(existing) = ctx_inner.get_hooks()[index].downcast_ref::<Signal<T>>()
92    {
93        return *existing;
94    }
95    let signal: Signal<T> = Signal::new(init());
96    let cleanup_signal: Signal<T> = signal;
97    ctx_inner
98        .get_mut_cleanups()
99        .push(Box::new(move || cleanup_signal.clear_listeners()));
100    if index < ctx_inner.get_hooks().len() {
101        ctx_inner.get_mut_hooks()[index] = Box::new(signal);
102    } else {
103        ctx_inner.get_mut_hooks().push(Box::new(signal));
104    }
105    signal
106}