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}