Skip to main content

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(w) => w,
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(w) => w,
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 raf_val: JsValue = Reflect::get(&window_value, &JsValue::from_str(REQUEST_ANIMATION_FRAME))
66        .unwrap_or(JsValue::UNDEFINED);
67    if raf_val.is_undefined() {
68        let queue_microtask_val: JsValue =
69            Reflect::get(&window_value, &JsValue::from_str(QUEUE_MICROTASK))
70                .unwrap_or(JsValue::UNDEFINED);
71        if queue_microtask_val.is_undefined() {
72            SCHEDULED.store(false, Ordering::Relaxed);
73            return;
74        }
75        let queue_microtask: Function = queue_microtask_val.into();
76        let _ = queue_microtask.call1(&JsValue::NULL, &dispatch_fn);
77        return;
78    }
79    let raf: Function = raf_val.into();
80    let _ = raf.call1(&JsValue::NULL, &dispatch_fn);
81}
82
83/// Batches signal updates within a closure, deferring DOM synchronization until completion.
84///
85/// Saves the current `SUPPRESS_SCHEDULE` flag, sets it to `true`,
86/// executes the closure, and restores the previous flag value.
87/// This prevents `schedule_signal_update` from queuing microtasks
88/// during the closure execution, allowing multiple signal mutations
89/// to be applied before triggering a single DOM update cycle.
90///
91/// # Arguments
92///
93/// - `F`- The closure to execute with batched updates.
94///
95/// # Returns
96///
97/// - `R`- The result of the closure execution.
98pub fn batch_updates<F, R>(callback: F) -> R
99where
100    F: FnOnce() -> R,
101{
102    let previous: bool = SUPPRESS_SCHEDULE.load(Ordering::Relaxed);
103    SUPPRESS_SCHEDULE.store(true, Ordering::Relaxed);
104    let result: R = callback();
105    SUPPRESS_SCHEDULE.store(previous, Ordering::Relaxed);
106    result
107}
108
109/// Subscribes an attribute signal to the global signal update dispatch cycle.
110///
111/// Creates a callback that re-computes the attribute value and sets
112/// it on the signal whenever a signal update cycle runs. The callback
113/// is registered in the signal update registry using the signal's
114/// inner address as the key.
115///
116/// # Arguments
117///
118/// - `Signal<String>`- The attribute signal to subscribe.
119/// - `F`- A closure that computes the current attribute value string.
120pub(crate) fn subscribe_attr_signal<F>(attr_signal: Signal<String>, compute: F)
121where
122    F: Fn() -> String + 'static,
123{
124    let signal_key: usize = attr_signal.get_inner_addr();
125    let callback: Box<dyn FnMut()> = Box::new(move || {
126        let new_value: String = compute();
127        attr_signal.set_silent(new_value);
128    });
129    register_attr_signal_listener(signal_key, callback);
130}
131
132/// Converts a bool signal into a reactive `Signal<String>` attribute value.
133///
134/// Creates a `Signal<String>` initialized with the bool's string
135/// representation, then subscribes to the source signal so that
136/// whenever the bool changes, the string signal is updated accordingly.
137///
138/// # Arguments
139///
140/// - `Signal<bool>`- The source boolean signal.
141///
142/// # Returns
143///
144/// - `AttributeValue`- An `AttributeValue::Signal` wrapping the derived string signal.
145pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
146    let initial: String = source.get().to_string();
147    let string_signal: Signal<String> = Signal::create(initial);
148    let string_signal_clone: Signal<String> = string_signal;
149    source.replace_subscribe({
150        let source_inner: Signal<bool> = source;
151        move || {
152            let new_value: String = source_inner.get().to_string();
153            string_signal_clone.set_silent(new_value);
154        }
155    });
156    AttributeValue::Signal(string_signal)
157}
158
159/// Returns a mutable reference to the current hook context.
160///
161/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
162///
163/// # Returns
164///
165/// - `&'static mut Option<HookContextRc>`: A mutable reference to the global hook context.
166#[allow(static_mut_refs)]
167pub(crate) fn current_hook_context_mut() -> &'static mut Option<HookContextRc> {
168    unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
169}
170
171/// Returns a shared reference to the current hook context.
172///
173/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
174///
175/// # Returns
176///
177/// - `&'static Option<HookContextRc>`: A shared reference to the global hook context.
178#[allow(static_mut_refs)]
179pub(crate) fn current_hook_context() -> &'static Option<HookContextRc> {
180    unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
181}