euv_core/reactive/schedule/fn.rs
1use crate::*;
2
3/// Ensures the `window.__euv_dispatch` callback is registered.
4///
5/// Creates a `Closure` that resets the `SCHEDULED` flag and directly
6/// invokes `dispatch_signal_update_callbacks`, then stores it on the
7/// `window` object so it can be invoked via `requestAnimationFrame`.
8///
9fn ensure_dispatch_callback() {
10 let window_value: Window = match window() {
11 Some(window_instance) => window_instance,
12 None => return,
13 };
14 let key: JsValue = JsValue::from_str(EUV_DISPATCH);
15 if Reflect::get(&window_value, &key)
16 .unwrap_or(JsValue::UNDEFINED)
17 .is_undefined()
18 {
19 let closure: closure::Closure<dyn FnMut()> = closure::Closure::wrap(Box::new(|| {
20 SCHEDULED.store(false, Ordering::Relaxed);
21 dispatch_signal_update_callbacks();
22 }));
23 let _ = Reflect::set(&window_value, &key, closure.as_ref());
24 closure.forget();
25 }
26}
27
28/// Schedules a deferred signal update event via `requestAnimationFrame`.
29///
30/// If a schedule is already pending (`SCHEDULED` is true) or updates
31/// are suppressed (`SUPPRESS_SCHEDULE` is true), this is a no-op.
32/// Otherwise, sets `SCHEDULED` to true and queues the
33/// `window.__euv_dispatch` callback via `requestAnimationFrame` on WASM
34/// targets. This ensures that no matter how many signal updates occur
35/// within a single animation frame, only one dispatch cycle runs,
36/// preventing CPU spikes during rapid input events (e.g., slider dragging).
37///
38/// On non-WASM targets, resets `SCHEDULED` immediately since there is
39/// no event loop to schedule on.
40pub fn schedule_signal_update() {
41 if SCHEDULED.load(Ordering::Relaxed) || SUPPRESS_SCHEDULE.load(Ordering::Relaxed) {
42 return;
43 }
44 SCHEDULED.store(true, Ordering::Relaxed);
45 mark_all_slots_dirty();
46 let window_option: Option<Window> = window();
47 if window_option.is_none() {
48 SCHEDULED.store(false, Ordering::Relaxed);
49 return;
50 }
51 ensure_dispatch_callback();
52 let window_value: Window = match window_option {
53 Some(window_instance) => window_instance,
54 None => {
55 SCHEDULED.store(false, Ordering::Relaxed);
56 return;
57 }
58 };
59 let dispatch_fn: JsValue =
60 Reflect::get(&window_value, &JsValue::from_str(EUV_DISPATCH)).unwrap_or(JsValue::UNDEFINED);
61 if dispatch_fn.is_undefined() {
62 SCHEDULED.store(false, Ordering::Relaxed);
63 return;
64 }
65 let request_animation_frame_value: JsValue =
66 Reflect::get(&window_value, &JsValue::from_str(REQUEST_ANIMATION_FRAME))
67 .unwrap_or(JsValue::UNDEFINED);
68 if request_animation_frame_value.is_undefined() {
69 let queue_microtask_value: JsValue =
70 Reflect::get(&window_value, &JsValue::from_str(QUEUE_MICROTASK))
71 .unwrap_or(JsValue::UNDEFINED);
72 if queue_microtask_value.is_undefined() {
73 SCHEDULED.store(false, Ordering::Relaxed);
74 return;
75 }
76 let queue_microtask: Function = queue_microtask_value.into();
77 let _ = queue_microtask.call1(&JsValue::NULL, &dispatch_fn);
78 return;
79 }
80 let request_animation_frame: Function = request_animation_frame_value.into();
81 let _ = request_animation_frame.call1(&JsValue::NULL, &dispatch_fn);
82}
83
84/// Batches signal updates within a closure, deferring DOM synchronization until completion.
85///
86/// Saves the current `SUPPRESS_SCHEDULE` flag, sets it to `true`,
87/// executes the closure, and restores the previous flag value.
88/// This prevents `schedule_signal_update` from queuing microtasks
89/// during the closure execution, allowing multiple signal mutations
90/// to be applied before triggering a single DOM update cycle.
91///
92/// When the outermost `batch_updates` call completes (i.e., the previous
93/// suppress flag was `false`), a single `schedule_signal_update()` is
94/// invoked to ensure that any signal mutations performed inside the
95/// closure are reflected in the DOM. This is critical for `watch!`
96/// initialisation, where `Console::log` calls mutate the console signal
97/// inside the batched block and must still trigger DynamicNode re-renders.
98///
99/// # Arguments
100///
101/// - `FnOnce() -> R` - The closure to execute with batched updates.
102///
103/// # Returns
104///
105/// - `R` - The result of the closure execution.
106pub fn batch_updates<F, R>(callback: F) -> R
107where
108 F: FnOnce() -> R,
109{
110 let was_outermost: bool = !SUPPRESS_SCHEDULE.load(Ordering::Relaxed);
111 SUPPRESS_SCHEDULE.store(true, Ordering::Relaxed);
112 let result: R = callback();
113 if was_outermost {
114 SUPPRESS_SCHEDULE.store(false, Ordering::Relaxed);
115 schedule_signal_update();
116 } else {
117 SUPPRESS_SCHEDULE.store(false, Ordering::Relaxed);
118 }
119 result
120}
121
122/// Subscribes an attribute signal to the global signal update dispatch cycle.
123///
124/// Creates a callback that re-computes the attribute value and sets
125/// it on the signal whenever a signal update cycle runs. The callback
126/// is registered in the signal update registry using the signal's
127/// inner address as the key.
128///
129/// # Arguments
130///
131/// - `Signal<String>` - The attribute signal to subscribe.
132/// - `Fn() -> String + 'static` - A closure that computes the current attribute value string.
133pub(crate) fn subscribe_attr_signal<F>(attr_signal: Signal<String>, compute: F)
134where
135 F: Fn() -> String + 'static,
136{
137 register_attr_signal_listener(
138 attr_signal.get_inner(),
139 Box::new(move || {
140 attr_signal.set_silent(compute());
141 }),
142 );
143}
144
145/// Converts a bool signal into a reactive `Signal<String>` attribute value.
146///
147/// Creates a `Signal<String>` initialized with the bool's string
148/// representation, then subscribes to the source signal so that
149/// whenever the bool changes, the string signal is updated accordingly.
150///
151/// # Arguments
152///
153/// - `Signal<bool>` - The source boolean signal.
154///
155/// # Returns
156///
157/// - `AttributeValue` - An `AttributeValue::Signal` wrapping the derived string signal.
158pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
159 let string_signal: Signal<String> = Signal::create(source.get().to_string());
160 let string_signal_clone: Signal<String> = string_signal;
161 source.replace_subscribe({
162 let source_inner: Signal<bool> = source;
163 move || {
164 string_signal_clone.set_silent(source_inner.get().to_string());
165 }
166 });
167 AttributeValue::Signal(string_signal)
168}
169
170/// Returns a mutable reference to the current hook context.
171///
172/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
173///
174/// # Returns
175///
176/// - `&'static mut Option<HookContextRc>`: A mutable reference to the global hook context.
177#[allow(static_mut_refs)]
178pub(crate) fn current_hook_context_mut() -> &'static mut Option<HookContextRc> {
179 unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
180}
181
182/// Returns a shared reference to the current hook context.
183///
184/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
185///
186/// # Returns
187///
188/// - `&'static Option<HookContextRc>`: A shared reference to the global hook context.
189#[allow(static_mut_refs)]
190pub(crate) fn current_hook_context() -> &'static Option<HookContextRc> {
191 unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
192}