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}