Skip to main content

euv_core/reactive/
impl.rs

1use crate::*;
2
3/// Implementation of SignalInner construction.
4impl<T> SignalInner<T>
5where
6    T: Clone,
7{
8    /// Creates a new signal inner with the given initial value and no listeners.
9    pub fn new(value: T) -> Self {
10        let inner: SignalInner<T> = SignalInner {
11            value,
12            listeners: Vec::new(),
13            alive: true,
14        };
15        inner
16    }
17}
18
19/// Implementation of reactive signal operations.
20impl<T> Signal<T>
21where
22    T: Clone + PartialEq,
23{
24    /// Creates a new `Signal` with the given initial value.
25    ///
26    /// The inner state is allocated via `Box::leak` and lives for the
27    /// remainder of the program. This is safe in single-threaded WASM
28    /// contexts where no concurrent access can occur.
29    ///
30    /// # Arguments
31    ///
32    /// - `T` - The initial value of the signal.
33    ///
34    /// # Returns
35    ///
36    /// - `Signal<T>` - A handle to the newly created reactive signal.
37    pub fn new(value: T) -> Self {
38        let boxed: Box<SignalInner<T>> = Box::new(SignalInner::new(value));
39        Signal {
40            inner: Box::leak(boxed) as *mut SignalInner<T>,
41        }
42    }
43
44    /// Creates a new `Signal` from an existing raw pointer.
45    ///
46    /// # Safety
47    ///
48    /// The caller must ensure the pointer was allocated via `Box::leak`
49    /// and remains valid for the entire program lifetime.
50    pub(crate) fn from_inner(inner: *mut SignalInner<T>) -> Self {
51        Signal { inner }
52    }
53
54    /// Returns a mutable reference to the inner signal state.
55    ///
56    /// # Safety
57    ///
58    /// The caller must ensure no other references to the inner state exist.
59    /// In single-threaded WASM this is always safe.
60    #[allow(clippy::mut_from_ref)]
61    fn get_inner_mut(&self) -> &mut SignalInner<T> {
62        unsafe { &mut *self.inner }
63    }
64
65    /// Returns the current value of the signal.
66    ///
67    /// # Returns
68    ///
69    /// - `T` - The current value of the signal.
70    pub fn get(&self) -> T {
71        self.get_inner_mut().get_value().clone()
72    }
73
74    /// Attempts to return the current value of the signal without panicking.
75    ///
76    /// # Returns
77    ///
78    /// - `Some(T)` - The current value if the borrow succeeds.
79    /// - `None` - If the inner value is already mutably borrowed.
80    pub fn try_get(&self) -> Option<T> {
81        Some(self.get_inner_mut().get_value().clone())
82    }
83
84    /// Subscribes a callback to be invoked when the signal changes.
85    ///
86    /// # Arguments
87    ///
88    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
89    pub fn subscribe<F>(&self, callback: F)
90    where
91        F: FnMut() + 'static,
92    {
93        self.get_inner_mut()
94            .get_mut_listeners()
95            .push(Rc::new(RefCell::new(callback)));
96    }
97
98    /// Replaces all listeners with a single new callback.
99    ///
100    /// Unlike `subscribe`, which appends a listener, this method clears any
101    /// existing listeners first and then adds the new one. This prevents
102    /// listener accumulation across re-renders: each signal is guaranteed
103    /// to have at most one active listener at any time, eliminating
104    /// cascading `set()` calls that would otherwise grow exponentially.
105    ///
106    /// # Arguments
107    ///
108    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
109    pub fn replace_subscribe<F>(&self, callback: F)
110    where
111        F: FnMut() + 'static,
112    {
113        let listeners: &mut Vec<Rc<RefCell<dyn FnMut()>>> =
114            self.get_inner_mut().get_mut_listeners();
115        listeners.clear();
116        listeners.push(Rc::new(RefCell::new(callback)));
117    }
118
119    /// Removes all subscribed listeners from this signal and marks it as
120    /// inactive. After calling this method, subsequent `set()` and
121    /// `try_set()` calls become complete no-ops: the value is not updated,
122    /// no listeners are invoked, and `schedule_signal_update()` is not
123    /// called. This is used during hook context cleanup when a `match`
124    /// arm switch discards old signals, ensuring that stale `setInterval`
125    /// closures referencing these signals become entirely harmless.
126    pub fn clear_listeners(&self) {
127        let inner: &mut SignalInner<T> = self.get_inner_mut();
128        inner.set_alive(false);
129        inner.get_mut_listeners().clear();
130    }
131
132    /// Sets the value of the signal and notifies listeners.
133    ///
134    /// If the signal has been marked as inactive (via `clear_listeners()`),
135    /// this method is a complete no-op: the value is not updated, no
136    /// listeners are invoked, and no global update is scheduled.
137    ///
138    /// If the new value is equal to the current value, no update is performed
139    /// and no listeners are notified. This prevents unnecessary re-renders and
140    /// avoids cascading no-op updates through intermediate signal chains.
141    ///
142    /// # Arguments
143    ///
144    /// - `T` - The new value to assign to the signal.
145    pub fn set(&self, value: T) {
146        let inner: &mut SignalInner<T> = self.get_inner_mut();
147        if !inner.get_alive() {
148            return;
149        }
150        if inner.get_value() == &value {
151            return;
152        }
153        let listeners: ListenerList = {
154            inner.set_value(value);
155            inner.get_listeners().iter().map(Rc::clone).collect()
156        };
157        for listener in &listeners {
158            let mut borrowed: RefMut<dyn FnMut()> = listener.borrow_mut();
159            borrowed();
160        }
161        schedule_signal_update();
162    }
163
164    /// Sets the value of the signal and notifies listeners without scheduling
165    /// a global DOM update dispatch.
166    ///
167    /// This is identical to `set` except it does not call
168    /// `schedule_signal_update()`, meaning no `__euv_signal_update__` event
169    /// will be dispatched. Use this for internal bookkeeping signals whose
170    /// changes should not trigger DynamicNode re-renders.
171    ///
172    /// # When to use
173    ///
174    /// Prefer `set` in almost all cases. Only use `set_silent` when the
175    /// signal change is guaranteed not to affect any DynamicNode output
176    /// (e.g., internal guard flags, derived-value caches already at the
177    /// correct value, or within a `with_suppressed_updates` block where
178    /// the caller takes responsibility for batching the update).
179    ///
180    /// If the new value is equal to the current value, no update is performed
181    /// and no listeners are notified.
182    ///
183    /// If the signal has been marked as inactive (via `clear_listeners()`),
184    /// this method is a complete no-op.
185    pub fn set_silent(&self, value: T) {
186        let inner: &mut SignalInner<T> = self.get_inner_mut();
187        if !inner.get_alive() {
188            return;
189        }
190        if inner.get_value() == &value {
191            return;
192        }
193        let listeners: ListenerList = {
194            inner.set_value(value);
195            inner.get_listeners().iter().map(Rc::clone).collect()
196        };
197        for listener in &listeners {
198            let mut borrowed: RefMut<dyn FnMut()> = listener.borrow_mut();
199            borrowed();
200        }
201    }
202
203    /// Attempts to set the value of the signal and notify listeners without panicking.
204    ///
205    /// If the new value is equal to the current value, no update is performed.
206    ///
207    /// # Arguments
208    ///
209    /// - `T` - The new value to assign to the signal.
210    ///
211    /// # Returns
212    ///
213    /// - `bool` - `true` if the value was successfully updated and listeners were notified, `false` if unchanged or inactive.
214    pub fn try_set(&self, value: T) -> bool {
215        let inner: &mut SignalInner<T> = self.get_inner_mut();
216        if !inner.get_alive() {
217            return false;
218        }
219        if inner.get_value() == &value {
220            return false;
221        }
222        let listeners: ListenerList = {
223            inner.set_value(value);
224            inner.get_listeners().iter().map(Rc::clone).collect()
225        };
226        for listener in &listeners {
227            listener.borrow_mut()();
228        }
229        schedule_signal_update();
230        true
231    }
232}
233
234/// Prevents direct dereference of a signal to enforce explicit API usage.
235impl<T> Deref for Signal<T>
236where
237    T: Clone + PartialEq,
238{
239    type Target = T;
240
241    fn deref(&self) -> &Self::Target {
242        panic!("Signal does not support direct dereference; use .get() instead");
243    }
244}
245
246/// Prevents direct mutable dereference of a signal to enforce explicit API usage.
247impl<T> DerefMut for Signal<T>
248where
249    T: Clone + PartialEq,
250{
251    fn deref_mut(&mut self) -> &mut Self::Target {
252        panic!("Signal does not support direct dereference; use .set() instead");
253    }
254}
255
256/// Clones the signal, sharing the same inner state.
257impl<T> Clone for Signal<T>
258where
259    T: Clone + PartialEq,
260{
261    fn clone(&self) -> Self {
262        *self
263    }
264}
265
266/// Copies the signal, sharing the same inner state.
267///
268/// A `Signal` is just a raw pointer; copying it is a trivial bitwise copy.
269impl<T> Copy for Signal<T> where T: Clone + PartialEq {}
270
271/// Converts a static `String` into a text attribute value.
272impl IntoReactiveValue for String {
273    fn into_reactive_value(self) -> AttributeValue {
274        AttributeValue::Text(self)
275    }
276}
277
278/// Converts a string slice into a text attribute value.
279impl IntoReactiveValue for &str {
280    fn into_reactive_value(self) -> AttributeValue {
281        AttributeValue::Text(self.to_string())
282    }
283}
284
285/// Converts a string signal into a reactive attribute value.
286impl IntoReactiveValue for Signal<String> {
287    fn into_reactive_value(self) -> AttributeValue {
288        AttributeValue::Signal(self)
289    }
290}
291
292/// Converts a mutable bool signal into a reactive attribute value.
293///
294/// The signal is mapped to a `Signal<String>` that yields `"true"` or `"false"`,
295/// enabling boolean attributes like `checked` to reactively update the DOM.
296impl IntoReactiveValue for Signal<bool> {
297    fn into_reactive_value(self) -> AttributeValue {
298        bool_signal_to_string_attribute_value(self)
299    }
300}
301
302/// Converts a CSS class reference into an attribute value.
303impl IntoReactiveValue for CssClass {
304    fn into_reactive_value(self) -> AttributeValue {
305        AttributeValue::Css(self)
306    }
307}
308
309/// Converts a reference to a CSS class into an attribute value by cloning.
310impl IntoReactiveValue for &'static CssClass {
311    fn into_reactive_value(self) -> AttributeValue {
312        AttributeValue::Css(self.clone())
313    }
314}
315
316/// Converts a `String` into its own value for reactive string storage.
317impl IntoReactiveString for String {
318    fn into_reactive_string(self) -> String {
319        self
320    }
321}
322
323/// Converts a string slice into an owned string for reactive string storage.
324impl IntoReactiveString for &str {
325    fn into_reactive_string(self) -> String {
326        self.to_string()
327    }
328}
329
330/// Converts a `CssClass` into its class name for reactive string storage.
331impl IntoReactiveString for CssClass {
332    fn into_reactive_string(self) -> String {
333        self.get_name().to_string()
334    }
335}
336
337/// Converts a reference to a `CssClass` into its class name for reactive string storage.
338impl IntoReactiveString for &'static CssClass {
339    fn into_reactive_string(self) -> String {
340        self.get_name().to_string()
341    }
342}
343
344/// Converts a `bool` into `"true"` or `"false"` for reactive string storage.
345impl IntoReactiveString for bool {
346    fn into_reactive_string(self) -> String {
347        self.to_string()
348    }
349}
350
351/// Converts an `i32` into a string for reactive string storage.
352impl IntoReactiveString for i32 {
353    fn into_reactive_string(self) -> String {
354        self.to_string()
355    }
356}
357
358/// Converts a `u32` into a string for reactive string storage.
359impl IntoReactiveString for u32 {
360    fn into_reactive_string(self) -> String {
361        self.to_string()
362    }
363}
364
365/// Converts a `f64` into a string for reactive string storage.
366impl IntoReactiveString for f64 {
367    fn into_reactive_string(self) -> String {
368        self.to_string()
369    }
370}
371
372/// Converts a string signal into a reactive string by resolving its current value.
373impl IntoReactiveString for Signal<String> {
374    fn into_reactive_string(self) -> String {
375        self.get()
376    }
377}
378
379/// Converts a bool signal into a reactive string by resolving its current value.
380impl IntoReactiveString for Signal<bool> {
381    fn into_reactive_string(self) -> String {
382        self.get().to_string()
383    }
384}
385
386/// Converts a closure into a callback attribute value.
387impl<F> IntoCallbackAttribute for F
388where
389    F: FnMut(NativeEvent) + 'static,
390{
391    fn into_callback_attribute(self) -> AttributeValue {
392        AttributeValue::Event(NativeEventHandler::new(
393            NativeEventName::Other("callback".to_string()),
394            self,
395        ))
396    }
397}
398
399/// Converts an owned event handler into a callback attribute value.
400impl IntoCallbackAttribute for NativeEventHandler {
401    fn into_callback_attribute(self) -> AttributeValue {
402        AttributeValue::Event(self)
403    }
404}
405
406/// Converts an optional event handler into a callback attribute value.
407impl IntoCallbackAttribute for Option<NativeEventHandler> {
408    fn into_callback_attribute(self) -> AttributeValue {
409        match self {
410            Some(handler) => AttributeValue::Event(handler),
411            None => AttributeValue::Text(String::new()),
412        }
413    }
414}
415
416/// Implementation of hook context lifecycle and hook index management.
417impl HookContext {
418    /// Creates a new `HookContext` from an existing raw pointer.
419    ///
420    /// # Safety
421    ///
422    /// The caller must ensure the pointer was allocated via `Box::leak`
423    /// and remains valid for the entire program lifetime.
424    pub fn from_inner(inner: *mut HookContextInner) -> Self {
425        HookContext { inner }
426    }
427
428    /// Returns a mutable reference to the inner hook context state.
429    ///
430    /// # Safety
431    ///
432    /// The caller must ensure no other references to the inner state exist.
433    /// In single-threaded WASM this is always safe.
434    #[allow(clippy::mut_from_ref)]
435    fn get_inner_mut(&self) -> &mut HookContextInner {
436        unsafe { &mut *self.inner }
437    }
438
439    /// Returns the current hook index.
440    pub fn get_hook_index(&self) -> usize {
441        self.get_inner_mut().get_hook_index()
442    }
443
444    /// Sets the hook index.
445    ///
446    /// # Arguments
447    ///
448    /// - `usize` - The new hook index value.
449    pub fn set_hook_index(&mut self, index: usize) {
450        self.get_inner_mut().set_hook_index(index);
451    }
452
453    /// Returns a reference to the hooks storage.
454    pub fn get_hooks(&self) -> &Vec<Box<dyn Any>> {
455        self.get_inner_mut().get_hooks()
456    }
457
458    /// Returns a mutable reference to the hooks storage.
459    pub fn get_mut_hooks(&mut self) -> &mut Vec<Box<dyn Any>> {
460        self.get_inner_mut().get_mut_hooks()
461    }
462
463    /// Returns a mutable reference to the cleanup closures storage.
464    pub fn get_mut_cleanups(&mut self) -> &mut Vec<Box<dyn FnOnce()>> {
465        self.get_inner_mut().get_mut_cleanups()
466    }
467
468    /// Resets the hook index for a new render cycle.
469    pub fn reset_hook_index(&mut self) {
470        self.set_hook_index(0_usize);
471    }
472
473    /// Notifies the hook context that a match arm is being entered.
474    /// Toggles the `arm_changed` flag; if it differs from the previous value,
475    /// the hooks array is cleared to prevent signal leakage between arms.
476    pub fn set_arm_changed(&mut self, changed: bool) {
477        let inner: &mut HookContextInner = self.get_inner_mut();
478        if inner.get_arm_changed() != changed {
479            let cleanups: Vec<Box<dyn FnOnce()>> = std::mem::take(inner.get_mut_cleanups());
480            for cleanup in cleanups {
481                cleanup();
482            }
483            inner.get_mut_hooks().clear();
484            inner.set_arm_changed(changed);
485        }
486        self.reset_hook_index();
487    }
488}
489
490/// Clones the hook context, sharing the same inner state.
491impl Clone for HookContext {
492    fn clone(&self) -> Self {
493        *self
494    }
495}
496
497/// Copies the hook context, sharing the same inner state.
498///
499/// A `HookContext` is just a raw pointer; copying it is a trivial bitwise copy.
500impl Copy for HookContext {}
501
502/// Provides a default empty hook context.
503impl Default for HookContext {
504    fn default() -> Self {
505        let boxed: Box<HookContextInner> = Box::default();
506        HookContext::from_inner(Box::leak(boxed) as *mut HookContextInner)
507    }
508}
509
510/// Implementation of HookContextInner construction.
511impl HookContextInner {
512    /// Creates a new empty hook context inner.
513    pub const fn new() -> Self {
514        HookContextInner {
515            hooks: Vec::new(),
516            arm_changed: false,
517            hook_index: 0_usize,
518            cleanups: Vec::new(),
519        }
520    }
521}
522
523/// Provides a default empty hook context inner.
524impl Default for HookContextInner {
525    fn default() -> Self {
526        Self::new()
527    }
528}
529
530/// SAFETY: `HookContextCell` is only used in single-threaded WASM contexts.
531/// Concurrent access from multiple threads would be undefined behavior.
532unsafe impl Sync for HookContextCell {}
533
534/// Implementation of SignalCell construction and access.
535impl<T> SignalCell<T>
536where
537    T: Clone + PartialEq,
538{
539    /// Creates a new empty `SignalCell` with no signal stored.
540    ///
541    /// # Returns
542    ///
543    /// - `SignalCell<T>` - An empty cell ready to hold a signal.
544    pub const fn new() -> Self {
545        SignalCell {
546            inner: UnsafeCell::new(None),
547        }
548    }
549
550    /// Stores a signal into the cell.
551    ///
552    /// # Arguments
553    ///
554    /// - `Signal<T>` - The signal to store.
555    ///
556    /// # Panics
557    ///
558    /// Panics if a signal has already been stored.
559    pub fn set(&self, signal: Signal<T>) {
560        unsafe {
561            let ptr: &mut Option<Signal<T>> = &mut *self.inner.get();
562            if ptr.is_some() {
563                panic!("SignalCell::set called on an already-initialized cell");
564            }
565            *ptr = Some(signal);
566        }
567    }
568
569    /// Returns the signal stored in the cell.
570    ///
571    /// # Returns
572    ///
573    /// - `Signal<T>` - The stored signal.
574    ///
575    /// # Panics
576    ///
577    /// Panics if no signal has been stored via `set`.
578    pub fn get(&self) -> Signal<T> {
579        unsafe {
580            let ptr: &Option<Signal<T>> = &*self.inner.get();
581            match ptr {
582                Some(signal) => *signal,
583                None => panic!("SignalCell::get called on an uninitialized cell"),
584            }
585        }
586    }
587}
588
589/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
590/// Concurrent access from multiple threads would be undefined behavior.
591unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq {}
592
593/// Provides a default empty `SignalCell`.
594impl<T> Default for SignalCell<T>
595where
596    T: Clone + PartialEq,
597{
598    fn default() -> Self {
599        Self::new()
600    }
601}