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/// When `SUPPRESS_SCHEDULE` is `true`, this function is a no-op so that
29/// internal operations (such as `watch!` initialisation) can perform
30/// signal mutations without triggering premature DOM re-renders.
31///
32/// # Panics
33///
34/// Panics if `Promise::new()` or `Event::new()` fails.
35pub(crate) fn schedule_signal_update() {
36    if SCHEDULED.load(Ordering::Relaxed) || SUPPRESS_SCHEDULE.load(Ordering::Relaxed) {
37        return;
38    }
39    SCHEDULED.store(true, Ordering::Relaxed);
40    #[cfg(target_arch = "wasm32")]
41    {
42        let win: Option<Window> = window();
43        if win.is_none() {
44            SCHEDULED.store(false, Ordering::Relaxed);
45            return;
46        }
47        let promise: js_sys::Promise = js_sys::Promise::resolve(&wasm_bindgen::JsValue::NULL);
48        let closure: wasm_bindgen::closure::Closure<dyn FnMut(wasm_bindgen::JsValue)> =
49            wasm_bindgen::closure::Closure::wrap(Box::new(move |_value: wasm_bindgen::JsValue| {
50                SCHEDULED.store(false, Ordering::Relaxed);
51                dispatch_signal_update();
52            }));
53        let _ = promise.then(&closure);
54        closure.forget();
55    }
56    #[cfg(not(target_arch = "wasm32"))]
57    {
58        SCHEDULED.store(false, Ordering::Relaxed);
59    }
60}
61
62/// Executes a closure with signal update scheduling suppressed.
63///
64/// Any `schedule_signal_update()` calls that occur within the closure
65/// (including those triggered by `Signal::set()`) are silently ignored.
66/// After the closure returns, the suppress flag is restored to its
67/// previous value.
68///
69/// This is used internally by `watch!` to prevent its initial body
70/// execution from triggering unnecessary DynamicNode re-renders.
71///
72/// # Arguments
73///
74/// - `FnOnce() -> R` - The closure to execute with suppressed scheduling.
75///
76/// # Returns
77///
78/// - `R` - The return value of the closure.
79pub fn with_suppressed_updates<F, R>(f: F) -> R
80where
81    F: FnOnce() -> R,
82{
83    let previous: bool = SUPPRESS_SCHEDULE.load(Ordering::Relaxed);
84    SUPPRESS_SCHEDULE.store(true, Ordering::Relaxed);
85    let result: R = f();
86    SUPPRESS_SCHEDULE.store(previous, Ordering::Relaxed);
87    result
88}
89
90/// Subscribes an attribute signal to the global `__euv_signal_update__` event so that
91/// whenever any signal changes, the attribute value is recomputed and the attribute
92/// signal is updated. This enables reactive `if` conditions inside any HTML attribute,
93/// including `style`, `class`, and others.
94///
95/// Works identically to the DOM-level `if {expr} { children }` mechanism: both
96/// re-evaluate their condition expressions when any signal dispatches the global
97/// update event, then apply only the minimal diff to the DOM.
98///
99/// # Arguments
100///
101/// - `Signal<String>` - The attribute signal to update when signals change.
102/// - `Fn() -> String + 'static` - A closure that recomputes the attribute value string.
103///
104/// # Panics
105///
106/// Panics if `window()` is unavailable on the current platform.
107pub fn subscribe_attr_signal<F>(attr_signal: Signal<String>, compute: F)
108where
109    F: Fn() -> String + 'static,
110{
111    #[cfg(target_arch = "wasm32")]
112    {
113        let signal_key: usize = attr_signal.inner as usize;
114        let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
115            let new_value: String = compute();
116            attr_signal.set(new_value);
117        }));
118        register_attr_signal_listener(signal_key, closure);
119    }
120    #[cfg(not(target_arch = "wasm32"))]
121    {
122        let _ = attr_signal;
123        let _ = compute;
124    }
125}
126
127/// Returns the currently active `HookContext`.
128///
129/// When called outside a `with_hook_context` scope, returns a reference
130/// to the default empty context.
131///
132/// # Returns
133///
134/// - `HookContext` - The currently active hook context.
135fn get_current_hook_context() -> HookContext {
136    unsafe { HookContext::from_inner(CURRENT_HOOK_CONTEXT) }
137}
138
139/// Runs a closure with the given `HookContext` set as the active context.
140///
141/// This is called by the renderer before invoking a `DynamicNode`'s
142/// render function, enabling `use_signal` and other hooks to access
143/// and persist state across re-renders.
144///
145/// # Arguments
146///
147/// - `HookContext` - The hook context to set as active.
148/// - `FnOnce() -> R` - The closure to execute with the active context.
149///
150/// # Returns
151///
152/// - `R` - The return value of the closure.
153pub fn with_hook_context<F, R>(context: HookContext, f: F) -> R
154where
155    F: FnOnce() -> R,
156{
157    let previous: *mut HookContextInner = unsafe { CURRENT_HOOK_CONTEXT };
158    unsafe {
159        CURRENT_HOOK_CONTEXT = context.inner;
160    }
161    let result: R = f();
162    unsafe {
163        CURRENT_HOOK_CONTEXT = previous;
164    }
165    result
166}
167
168/// Creates a new `HookContext` allocated via `Box::leak`.
169///
170/// The allocated memory lives for the remainder of the program and will
171/// never be freed. This is acceptable for WASM single-threaded contexts
172/// where `DynamicNode` instances persist for the application lifetime.
173///
174/// # Returns
175///
176/// - `HookContext` - A handle to the newly allocated hook context.
177pub fn create_hook_context() -> HookContext {
178    let ctx: Box<HookContextInner> = Box::default();
179    HookContext::from_inner(Box::leak(ctx) as *mut HookContextInner)
180}
181
182/// Creates a new reactive signal with the given initial value.
183///
184/// When called inside a `DynamicNode` render function (within a
185/// `with_hook_context` scope), the signal state is persisted across
186/// re-renders by storing it in the active `HookContext`. Subsequent
187/// re-renders return the same signal handle, preserving its current value.
188///
189/// When called outside a hook context, a fresh signal is created each time.
190///
191/// # Arguments
192///
193/// - `FnOnce() -> T` - A closure that returns the initial value of the signal.
194///
195/// # Returns
196///
197/// - `Signal<T>` - A mutable handle to the newly created or persisted reactive signal.
198pub fn use_signal<T, F>(init: F) -> Signal<T>
199where
200    T: Clone + PartialEq + 'static,
201    F: FnOnce() -> T,
202{
203    let mut ctx: HookContext = get_current_hook_context();
204    let index: usize = ctx.get_hook_index();
205    ctx.set_hook_index(index + 1_usize);
206    if index < ctx.get_hooks().len()
207        && let Some(existing) = ctx.get_hooks()[index].downcast_ref::<Signal<T>>()
208    {
209        return *existing;
210    }
211    let signal: Signal<T> = {
212        let boxed: Box<SignalInner<T>> = Box::new(SignalInner::new(init()));
213        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<T>)
214    };
215    let cleanup_signal: Signal<T> = signal;
216    ctx.get_mut_cleanups()
217        .push(Box::new(move || cleanup_signal.clear_listeners()));
218    if index < ctx.get_hooks().len() {
219        ctx.get_mut_hooks()[index] = Box::new(signal);
220    } else {
221        ctx.get_mut_hooks().push(Box::new(signal));
222    }
223    signal
224}
225
226/// Converts a bool signal into a reactive `Signal<String>` that
227/// yields `"true"` or `"false"`, enabling boolean attributes like `checked` to
228/// reactively update the DOM.
229///
230/// # Arguments
231///
232/// - `Signal<bool>` - The source boolean signal to convert.
233///
234/// # Returns
235///
236/// - `AttributeValue` - A signal-backed attribute value that reactively mirrors the boolean as a string.
237pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
238    let initial: String = source.get().to_string();
239    let string_signal: Signal<String> = {
240        let inner: SignalInner<String> = SignalInner::new(initial);
241        let boxed: Box<SignalInner<String>> = Box::new(inner);
242        Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
243    };
244    let string_signal_clone: Signal<String> = string_signal;
245    source.subscribe({
246        let source_inner: Signal<bool> = source;
247        move || {
248            let new_value: String = source_inner.get().to_string();
249            string_signal_clone.set(new_value);
250        }
251    });
252    AttributeValue::Signal(string_signal)
253}