Skip to main content

euv_core/reactive/
fn.rs

1use crate::*;
2
3/// Dispatches the global `__euv_signal_update__` event on the window.
4///
5/// This triggers any `DynamicNode` listeners that are subscribed via
6/// `window.addEventListener("__euv_signal_update__", ...)` so that
7/// dynamic virtual DOM nodes can re-render when signal values change.
8///
9/// # Panics
10///
11/// Panics if `Event::new("__euv_signal_update__")` fails, which should
12/// never happen for a valid event type string.
13#[cfg(target_arch = "wasm32")]
14pub(crate) fn dispatch_signal_update() {
15    if let Some(win) = window() {
16        let event: Event = Event::new("__euv_signal_update__").unwrap();
17        let _ = win.dispatch_event(&event);
18    }
19}
20
21/// Schedules a deferred `__euv_signal_update__` event via a microtask.
22///
23/// Batches multiple signal updates within the same synchronous tick into
24/// a single dispatch that runs after all local listeners have completed,
25/// preventing DynamicNode re-renders from interfering with in-flight
26/// signal updates.
27///
28/// # Panics
29///
30/// Panics if `Promise::new()` or `Event::new()` fails.
31pub(crate) fn schedule_signal_update() {
32    if SCHEDULED.load(Ordering::Relaxed) {
33        return;
34    }
35    SCHEDULED.store(true, Ordering::Relaxed);
36    #[cfg(target_arch = "wasm32")]
37    {
38        let win: Option<Window> = window();
39        if win.is_none() {
40            SCHEDULED.store(false, Ordering::Relaxed);
41            return;
42        }
43        let promise: js_sys::Promise = js_sys::Promise::resolve(&wasm_bindgen::JsValue::NULL);
44        let closure: wasm_bindgen::closure::Closure<dyn FnMut(wasm_bindgen::JsValue)> =
45            wasm_bindgen::closure::Closure::wrap(Box::new(move |_value: wasm_bindgen::JsValue| {
46                SCHEDULED.store(false, Ordering::Relaxed);
47                dispatch_signal_update();
48            }));
49        let _ = promise.then(&closure);
50        closure.forget();
51    }
52    #[cfg(not(target_arch = "wasm32"))]
53    {
54        SCHEDULED.store(false, Ordering::Relaxed);
55    }
56}
57
58/// Dispatches the global `__euv_signal_update__` event on the window.
59///
60/// This is the public entry point for manually triggering a signal update
61/// cycle, which causes all `DynamicNode` instances to re-render.
62///
63/// # Panics
64///
65/// Panics if `Event::new("__euv_signal_update__")` fails.
66pub fn trigger_update() {
67    schedule_signal_update();
68}
69
70/// Returns a mutable reference to the currently active `HookContext`.
71///
72/// When called outside a `with_hook_context` scope, returns a reference
73/// to the default empty context.
74fn get_current_hook_context() -> HookContext {
75    unsafe { HookContext::from_inner(CURRENT_HOOK_CONTEXT) }
76}
77
78/// Runs a closure with the given `HookContext` set as the active context.
79///
80/// This is called by the renderer before invoking a `DynamicNode`'s
81/// render function, enabling `use_signal` and other hooks to access
82/// and persist state across re-renders.
83///
84/// # Arguments
85///
86/// - `HookContext`: The hook context to set as active.
87/// - `F`: The closure to execute with the active context.
88///
89/// # Returns
90///
91/// - `R`: The return value of the closure.
92pub fn with_hook_context<F, R>(context: HookContext, f: F) -> R
93where
94    F: FnOnce() -> R,
95{
96    // SAFETY: WASM is single-threaded; no data race on CURRENT_HOOK_CONTEXT.
97    let previous: *mut HookContextInner = unsafe { CURRENT_HOOK_CONTEXT };
98    // SAFETY: Same as above.
99    unsafe {
100        CURRENT_HOOK_CONTEXT = context.inner;
101    }
102    let result: R = f();
103    // SAFETY: Same as above.
104    unsafe {
105        CURRENT_HOOK_CONTEXT = previous;
106    }
107    result
108}
109
110/// Creates a new `HookContext` allocated via `Box::leak`.
111///
112/// The allocated memory lives for the remainder of the program and will
113/// never be freed. This is acceptable for WASM single-threaded contexts
114/// where `DynamicNode` instances persist for the application lifetime.
115///
116/// # Returns
117///
118/// - `HookContext`: A handle to the newly allocated hook context.
119pub fn create_hook_context() -> HookContext {
120    let ctx: Box<HookContextInner> = Box::default();
121    HookContext::from_inner(Box::leak(ctx) as *mut HookContextInner)
122}
123
124/// Creates a new reactive signal with the given initial value.
125///
126/// When called inside a `DynamicNode` render function (within a
127/// `with_hook_context` scope), the signal state is persisted across
128/// re-renders by storing it in the active `HookContext`. Subsequent
129/// re-renders return the same signal handle, preserving its current value.
130///
131/// When called outside a hook context, a fresh signal is created each time.
132///
133/// # Arguments
134///
135/// - `FnOnce() -> T`: A closure that returns the initial value of the signal.
136///
137/// # Returns
138///
139/// - `Signal<T>`: A mutable handle to the newly created or persisted reactive signal.
140pub fn use_signal<T, F>(init: F) -> Signal<T>
141where
142    T: Clone + PartialEq + 'static,
143    F: FnOnce() -> T,
144{
145    let mut ctx: HookContext = get_current_hook_context();
146    let index: usize = ctx.get_hook_index();
147    ctx.set_hook_index(index + 1_usize);
148    if index < ctx.get_hooks().len()
149        && let Some(existing) = ctx.get_hooks()[index].downcast_ref::<Signal<T>>()
150    {
151        return *existing;
152    }
153    let signal: Signal<T> = {
154        let boxed: Box<SignalInner<T>> = Box::new(SignalInner::new(init()));
155        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<T>)
156    };
157    if index < ctx.get_hooks().len() {
158        ctx.get_mut_hooks()[index] = Box::new(signal);
159    } else {
160        ctx.get_mut_hooks().push(Box::new(signal));
161    }
162    signal
163}
164
165/// Converts a bool signal into a reactive `Signal<String>` that
166/// yields `"true"` or `"false"`, enabling boolean attributes like `checked` to
167/// reactively update the DOM.
168pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
169    let initial: String = source.get().to_string();
170    let string_signal: Signal<String> = {
171        let inner: SignalInner<String> = SignalInner::new(initial);
172        let boxed: Box<SignalInner<String>> = Box::new(inner);
173        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
174    };
175    let string_signal_clone: Signal<String> = string_signal;
176    source.subscribe({
177        let source_inner: Signal<bool> = source;
178        move || {
179            let new_value: String = source_inner.get().to_string();
180            string_signal_clone.set(new_value);
181        }
182    });
183    AttributeValue::Signal(string_signal)
184}