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}