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 the currently active `HookContext`.
71///
72/// When called outside a `with_hook_context` scope, returns a reference
73/// to the default empty context.
74///
75/// # Returns
76///
77/// - `HookContext`: The currently active hook context.
78fn get_current_hook_context() -> HookContext {
79    unsafe { HookContext::from_inner(CURRENT_HOOK_CONTEXT) }
80}
81
82/// Runs a closure with the given `HookContext` set as the active context.
83///
84/// This is called by the renderer before invoking a `DynamicNode`'s
85/// render function, enabling `use_signal` and other hooks to access
86/// and persist state across re-renders.
87///
88/// # Arguments
89///
90/// - `HookContext`: The hook context to set as active.
91/// - `F`: The closure to execute with the active context.
92///
93/// # Returns
94///
95/// - `R`: The return value of the closure.
96pub fn with_hook_context<F, R>(context: HookContext, f: F) -> R
97where
98    F: FnOnce() -> R,
99{
100    // SAFETY: WASM is single-threaded; no data race on CURRENT_HOOK_CONTEXT.
101    let previous: *mut HookContextInner = unsafe { CURRENT_HOOK_CONTEXT };
102    // SAFETY: Same as above.
103    unsafe {
104        CURRENT_HOOK_CONTEXT = context.inner;
105    }
106    let result: R = f();
107    // SAFETY: Same as above.
108    unsafe {
109        CURRENT_HOOK_CONTEXT = previous;
110    }
111    result
112}
113
114/// Creates a new `HookContext` allocated via `Box::leak`.
115///
116/// The allocated memory lives for the remainder of the program and will
117/// never be freed. This is acceptable for WASM single-threaded contexts
118/// where `DynamicNode` instances persist for the application lifetime.
119///
120/// # Returns
121///
122/// - `HookContext`: A handle to the newly allocated hook context.
123pub fn create_hook_context() -> HookContext {
124    let ctx: Box<HookContextInner> = Box::default();
125    HookContext::from_inner(Box::leak(ctx) as *mut HookContextInner)
126}
127
128/// Creates a new reactive signal with the given initial value.
129///
130/// When called inside a `DynamicNode` render function (within a
131/// `with_hook_context` scope), the signal state is persisted across
132/// re-renders by storing it in the active `HookContext`. Subsequent
133/// re-renders return the same signal handle, preserving its current value.
134///
135/// When called outside a hook context, a fresh signal is created each time.
136///
137/// # Arguments
138///
139/// - `FnOnce() -> T`: A closure that returns the initial value of the signal.
140///
141/// # Returns
142///
143/// - `Signal<T>`: A mutable handle to the newly created or persisted reactive signal.
144pub fn use_signal<T, F>(init: F) -> Signal<T>
145where
146    T: Clone + PartialEq + 'static,
147    F: FnOnce() -> T,
148{
149    let mut ctx: HookContext = get_current_hook_context();
150    let index: usize = ctx.get_hook_index();
151    ctx.set_hook_index(index + 1_usize);
152    if index < ctx.get_hooks().len()
153        && let Some(existing) = ctx.get_hooks()[index].downcast_ref::<Signal<T>>()
154    {
155        return *existing;
156    }
157    let signal: Signal<T> = {
158        let boxed: Box<SignalInner<T>> = Box::new(SignalInner::new(init()));
159        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<T>)
160    };
161    if index < ctx.get_hooks().len() {
162        ctx.get_mut_hooks()[index] = Box::new(signal);
163    } else {
164        ctx.get_mut_hooks().push(Box::new(signal));
165    }
166    signal
167}
168
169/// Converts a bool signal into a reactive `Signal<String>` that
170/// yields `"true"` or `"false"`, enabling boolean attributes like `checked` to
171/// reactively update the DOM.
172///
173/// # Arguments
174///
175/// - `Signal<bool>`: The source boolean signal to convert.
176///
177/// # Returns
178///
179/// - `AttributeValue`: A signal-backed attribute value that reactively mirrors the boolean as a string.
180pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
181    let initial: String = source.get().to_string();
182    let string_signal: Signal<String> = {
183        let inner: SignalInner<String> = SignalInner::new(initial);
184        let boxed: Box<SignalInner<String>> = Box::new(inner);
185        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
186    };
187    let string_signal_clone: Signal<String> = string_signal;
188    source.subscribe({
189        let source_inner: Signal<bool> = source;
190        move || {
191            let new_value: String = source_inner.get().to_string();
192            string_signal_clone.set(new_value);
193        }
194    });
195    AttributeValue::Signal(string_signal)
196}