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