Skip to main content

teksilo_core/
signal.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Unified reactivity primitives for Teksilo.
5//!
6//! `Signal<T>` is the single reactive type. `Prop<T>` is the widget
7//! property type for static values and signal bindings. `ObserverHandle`
8//! is an RAII guard — dropping it removes the observer callback.
9
10use std::cell::{Cell, Ref, RefCell};
11use std::rc::{Rc, Weak};
12
13use crate::binding::{Binding, BindingLevel, BindingRegistry};
14use crate::widget_id::WidgetId;
15
16// ---------------------------------------------------------------------------
17// Feedback-loop guard (debug-only)
18// ---------------------------------------------------------------------------
19//
20// The snapshot-and-release notification model lets an observer freely
21// re-enter `set` on the same (or another) signal. That flexibility also means
22// an *accidental* feedback loop — signal A's observer writes B, B's observer
23// writes A, neither guarded by an equality check — recurses without bound and
24// blows the stack with no actionable diagnostic. In debug builds we track the
25// per-thread notification depth and panic with a pointer at the likely cause
26// once it crosses a limit set far above any legitimate synchronous cascade.
27// Release builds carry no counter and no check.
28#[cfg(debug_assertions)]
29const SIGNAL_NOTIFY_DEPTH_LIMIT: u32 = 256;
30
31#[cfg(debug_assertions)]
32thread_local! {
33    static SIGNAL_NOTIFY_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
34}
35
36/// RAII increment of the per-thread Signal-notification depth (debug only).
37#[cfg(debug_assertions)]
38struct NotifyDepthGuard;
39
40#[cfg(debug_assertions)]
41impl NotifyDepthGuard {
42    fn enter() -> Self {
43        SIGNAL_NOTIFY_DEPTH.with(|d| {
44            let next = d.get() + 1;
45            assert!(
46                next <= SIGNAL_NOTIFY_DEPTH_LIMIT,
47                "Signal notification nested {next} deep (limit {SIGNAL_NOTIFY_DEPTH_LIMIT}) — \
48                 almost certainly an unbounded feedback loop between observers (e.g. signal A's \
49                 observer sets B and B's observer sets A). Break the cycle: guard the write with \
50                 an equality check (`if sig.get() != v {{ sig.set(v) }}`), or drop one edge with \
51                 a WeakSignal."
52            );
53            d.set(next);
54        });
55        NotifyDepthGuard
56    }
57}
58
59#[cfg(debug_assertions)]
60impl Drop for NotifyDepthGuard {
61    fn drop(&mut self) {
62        SIGNAL_NOTIFY_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
63    }
64}
65
66// ---------------------------------------------------------------------------
67// ObserverHandle — RAII guard for observer cleanup
68// ---------------------------------------------------------------------------
69
70/// RAII guard for an observer callback. Dropping the handle removes the
71/// callback from the signal, preventing memory leaks.
72pub struct ObserverHandle {
73    /// Reference to the signal (keeps it alive while the handle exists).
74    _signal: Rc<dyn std::any::Any>,
75    observer_id: u64,
76    remover: Rc<dyn Fn(u64)>,
77}
78
79impl ObserverHandle {
80    /// Create a new observer handle.
81    ///
82    /// - `keeper`: an `Rc` that keeps the observed source alive while this handle exists.
83    /// - `observer_id`: the ID identifying this observer.
84    /// - `remover`: called with `observer_id` when the handle is dropped, to unregister the callback.
85    pub fn new(keeper: Rc<dyn std::any::Any>, observer_id: u64, remover: Rc<dyn Fn(u64)>) -> Self {
86        Self {
87            _signal: keeper,
88            observer_id,
89            remover,
90        }
91    }
92
93    /// Explicitly detach the observer without dropping the handle.
94    pub fn detach(self) {
95        // Drop runs automatically, which calls remover
96        drop(self);
97    }
98}
99
100impl Drop for ObserverHandle {
101    fn drop(&mut self) {
102        (self.remover)(self.observer_id);
103    }
104}
105
106impl std::fmt::Debug for ObserverHandle {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("ObserverHandle")
109            .field("observer_id", &self.observer_id)
110            .finish()
111    }
112}
113
114// ---------------------------------------------------------------------------
115// Signal internals
116// ---------------------------------------------------------------------------
117
118struct ObserverEntry<T> {
119    id: u64,
120    callback: Rc<dyn Fn(&T)>,
121}
122
123struct MutableInner<T> {
124    value: T,
125    /// Monotonic change counter, advanced by every write (`try_set`, and
126    /// arming an animation). **Never reset.**
127    ///
128    /// This replaced a plain `dirty: bool` because a boolean is
129    /// *consumer-shaped state stored on the producer*: one flag, but
130    /// potentially many independent consumers. Each open window owns its
131    /// own [`crate::binding::BindingRegistry`], and that registry's flush
132    /// pass both read AND cleared the flag — so with two windows bound to
133    /// one signal, whichever tree reconciled first consumed the flag and
134    /// every other window silently skipped its otherwise-correct binding.
135    /// Not a delayed rebuild: a permanently missed one, until an unrelated
136    /// later write raced a different window into observing it first (which
137    /// window lost was decided by `HashMap` iteration order).
138    ///
139    /// A counter moves the per-consumer half of that state to the
140    /// consumer: nothing here is consumed, and each registry remembers the
141    /// generation it last acted on (`BindingGroup::last_seen`). N readers
142    /// are then trivially independent.
143    generation: u64,
144    observers: Vec<ObserverEntry<T>>,
145    next_observer_id: u64,
146    /// Drop guards attached to this signal via `attach_keepalive`. Used
147    /// by adapters (e.g., `LocalizedString::to_signal`) that observe an
148    /// external source and need their `ObserverHandle` to live exactly
149    /// as long as the signal it updates — when the last `Signal<T>`
150    /// clone drops, this `Vec` drops, which drops every stored handle,
151    /// which detaches their callbacks from the source. Without this,
152    /// such adapters would have to `mem::forget` their handles and
153    /// leak both the observer entry on the source and the target
154    /// signal it kept alive through a strong `Rc` clone.
155    keepalive: Vec<Box<dyn std::any::Any>>,
156}
157
158/// Animation-specific state, only for `Signal<f32>`.
159struct AnimationState {
160    pending: Option<crate::animation::AnimationRequest>,
161    target: Option<f32>,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
165pub enum SignalAccessError {
166    #[error("signal is read-only")]
167    ReadOnly,
168    #[error("signal does not support animation")]
169    AnimationUnsupported,
170}
171
172/// Weak reference to a mutable signal. Produced by `Signal::downgrade`.
173///
174/// Unlike a `Signal<T>` clone (which is an `Rc`), a `WeakSignal<T>`
175/// does not extend the lifetime of the underlying `MutableInner<T>`.
176/// Use this inside observer callbacks that should not keep the
177/// observed-target signal alive — otherwise the strong `Rc` captured
178/// by the closure forms a reference cycle with the inner that holds
179/// the observer, and neither gets freed.
180pub struct WeakSignal<T> {
181    inner: Weak<RefCell<MutableInner<T>>>,
182    animation: Option<Weak<RefCell<AnimationState>>>,
183}
184
185impl<T> Clone for WeakSignal<T> {
186    fn clone(&self) -> Self {
187        Self {
188            inner: self.inner.clone(),
189            animation: self.animation.clone(),
190        }
191    }
192}
193
194impl<T: 'static> WeakSignal<T> {
195    /// Try to upgrade the weak reference into a live `Signal<T>`.
196    /// Returns `None` if the target signal has already been dropped.
197    pub fn upgrade(&self) -> Option<Signal<T>> {
198        let inner = self.inner.upgrade()?;
199        // If the original was animated, preserve that — but if the
200        // animation state was freed independently we degrade to a
201        // non-animated signal rather than failing the upgrade.
202        let animation = self.animation.as_ref().and_then(|weak| weak.upgrade());
203        Some(Signal {
204            kind: SignalKind::Mutable { inner, animation },
205        })
206    }
207}
208
209pub(crate) struct WeakAnimatedSignal {
210    inner: Weak<RefCell<MutableInner<f32>>>,
211    animation: Weak<RefCell<AnimationState>>,
212}
213
214impl WeakAnimatedSignal {
215    pub(crate) fn upgrade(&self) -> Option<Signal<f32>> {
216        Some(Signal {
217            kind: SignalKind::Mutable {
218                inner: self.inner.upgrade()?,
219                animation: Some(self.animation.upgrade()?),
220            },
221        })
222    }
223
224    pub(crate) fn same_signal(&self, signal: &Signal<f32>) -> bool {
225        match &signal.kind {
226            SignalKind::Mutable { inner, .. } => self.inner.as_ptr() == Rc::as_ptr(inner),
227            SignalKind::Derived { .. } => false,
228        }
229    }
230}
231
232/// One upstream source a [`SignalKind::Derived`] signal depends on.
233///
234/// A single-source derived signal (the typical `map` case) carries one
235/// entry; multi-source derived signals (`zip`, `zip3`, `and`, `or`)
236/// carry one per observed mutable root. Change-tracking walks the whole
237/// vec — a consumer is stale if *any* entry's generation moved past what
238/// that consumer last acted on.
239///
240/// **Every `generation` closure in the system is monotonically
241/// non-decreasing**, whether it reads a mutable root's counter directly
242/// or is a composite built by [`coalesced_source`]. Callers rely on that:
243/// it is what lets [`Signal::generation`] fold a multi-source derived
244/// signal down to a single `u64` by summing, with no risk that one
245/// source's advance is cancelled by another's retreat.
246#[derive(Clone)]
247struct DerivedSource {
248    /// Current generation of this upstream. Compare against a remembered
249    /// value to decide staleness; never "clear" it.
250    generation: Rc<dyn Fn() -> u64>,
251    /// Stable identity of the upstream mutable root — used by
252    /// [`BindingRegistry`] to dedup repeated `bind_to` calls.
253    source_id: usize,
254}
255
256/// Fold an arbitrary — and possibly *changing* — set of upstream
257/// generations into ONE monotone counter, presented as a single
258/// [`DerivedSource`].
259///
260/// `inputs` is polled on demand and returns the current generation of
261/// every upstream the composite currently depends on. Whenever that
262/// vector differs from the one seen at the previous poll (in length or
263/// in any element), the composite's own counter advances by one.
264///
265/// Summing the inputs would be enough for a *fixed* set of monotone
266/// upstreams, but not for [`Signal::flat_map`], whose selected inner
267/// signal is re-chosen on every poll: switching inners makes its
268/// generation term jump arbitrarily, including downwards, and a drop
269/// that exactly cancelled an increase elsewhere would hide a real
270/// change. Re-deriving an own counter from "did the input vector change
271/// at all" is immune to that, and keeps this source monotone for
272/// everyone downstream.
273///
274/// Crucially the memo is **never consumed**: the first registry to poll
275/// advances the counter, and every registry polling afterwards reads the
276/// same, already-advanced value and compares it against its OWN
277/// last-seen. That is what makes one composite safe to share between N
278/// independently-reconciled `WidgetTree`s — the property the whole
279/// generation scheme exists to provide.
280fn coalesced_source(inputs: Rc<dyn Fn() -> Vec<u64>>) -> DerivedSource {
281    // The token anchors a unique heap address used as `source_id`; the
282    // closure below owns it, so the address stays valid — and therefore
283    // unambiguous — for exactly as long as this source is reachable.
284    let token: Rc<()> = Rc::new(());
285    let source_id = Rc::as_ptr(&token) as usize;
286    let state: Rc<(Cell<u64>, RefCell<Option<Vec<u64>>>)> =
287        Rc::new((Cell::new(0), RefCell::new(None)));
288    DerivedSource {
289        generation: Rc::new(move || {
290            let _keep = &token;
291            let now = inputs();
292            let (own, seen) = &*state;
293            let mut seen = seen.borrow_mut();
294            if seen.as_deref() != Some(now.as_slice()) {
295                *seen = Some(now);
296                own.set(own.get().wrapping_add(1));
297            }
298            own.get()
299        }),
300        source_id,
301    }
302}
303
304enum SignalKind<T> {
305    Mutable {
306        inner: Rc<RefCell<MutableInner<T>>>,
307        animation: Option<Rc<RefCell<AnimationState>>>,
308    },
309    Derived {
310        compute: Rc<dyn Fn() -> T>,
311        /// The upstream mutable roots this derived signal depends on.
312        /// Typically one entry; `zip`/`zip3`/`and`/`or` produce many.
313        /// Deduped by `source_id` at construction.
314        sources: Vec<DerivedSource>,
315    },
316}
317
318// ---------------------------------------------------------------------------
319// Signal<T>
320// ---------------------------------------------------------------------------
321
322/// A reactive value. Created via `Signal::new(value)` for mutable signals
323/// or `signal.map(f)` for derived (read-only) signals.
324pub struct Signal<T> {
325    kind: SignalKind<T>,
326}
327
328impl<T: 'static> Signal<T> {
329    /// Create a mutable signal with an initial value.
330    pub fn new(value: T) -> Self {
331        Self {
332            kind: SignalKind::Mutable {
333                inner: Rc::new(RefCell::new(MutableInner {
334                    value,
335                    generation: 0,
336                    observers: Vec::new(),
337                    next_observer_id: 1,
338                    keepalive: Vec::new(),
339                })),
340                animation: None,
341            },
342        }
343    }
344
345    /// Attach an arbitrary drop guard that lives exactly as long as the
346    /// signal does — the guard is dropped when the last `Signal<T>`
347    /// clone is freed (i.e., when `MutableInner<T>` is freed). Intended
348    /// for adapters that observe an external source and want their
349    /// `ObserverHandle` to auto-unsubscribe when the signal they're
350    /// driving becomes unreachable.
351    ///
352    /// On a derived (read-only) signal this is a no-op; the adapter
353    /// pattern only makes sense for mutable signals.
354    pub fn attach_keepalive<G: 'static>(&self, guard: G) {
355        if let SignalKind::Mutable { inner, .. } = &self.kind {
356            inner.borrow_mut().keepalive.push(Box::new(guard));
357        }
358    }
359
360    /// Get a weak reference to this signal. Callbacks registered on an
361    /// external source should capture the `WeakSignal` instead of a
362    /// strong `Signal<T>` clone — otherwise the callback's strong `Rc`
363    /// keeps the inner alive indefinitely, creating a reference cycle.
364    ///
365    /// Returns `None` for derived (read-only) signals, which have no
366    /// shared inner to downgrade.
367    pub fn downgrade(&self) -> Option<WeakSignal<T>> {
368        match &self.kind {
369            SignalKind::Mutable { inner, animation } => Some(WeakSignal {
370                inner: Rc::downgrade(inner),
371                animation: animation.as_ref().map(Rc::downgrade),
372            }),
373            SignalKind::Derived { .. } => None,
374        }
375    }
376
377    /// Register an observer callback. Returns an `ObserverHandle` — dropping
378    /// the handle removes the callback.
379    pub fn observe(&self, f: impl Fn(&T) + 'static) -> ObserverHandle {
380        self.try_observe(f)
381            .expect("observe() is only supported on mutable signals")
382    }
383
384    pub fn try_observe(
385        &self,
386        f: impl Fn(&T) + 'static,
387    ) -> Result<ObserverHandle, SignalAccessError> {
388        match &self.kind {
389            SignalKind::Mutable { inner, .. } => {
390                let mut guard = inner.borrow_mut();
391                let id = guard.next_observer_id;
392                guard.next_observer_id += 1;
393                guard.observers.push(ObserverEntry {
394                    id,
395                    callback: Rc::new(f),
396                });
397                Ok(ObserverHandle {
398                    _signal: inner.clone(),
399                    observer_id: id,
400                    remover: {
401                        let inner = inner.clone();
402                        Rc::new(move |observer_id| {
403                            inner.borrow_mut().observers.retain(|e| e.id != observer_id);
404                        })
405                    },
406                })
407            }
408            SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
409        }
410    }
411
412    /// Number of active observers on this signal. Derived signals always return 0.
413    pub fn observer_count(&self) -> usize {
414        match &self.kind {
415            SignalKind::Mutable { inner, .. } => inner.borrow().observers.len(),
416            SignalKind::Derived { .. } => 0,
417        }
418    }
419
420    /// Whether two Signal handles point to the same underlying value.
421    pub fn same(a: &Self, b: &Self) -> bool {
422        match (&a.kind, &b.kind) {
423            (SignalKind::Mutable { inner: a, .. }, SignalKind::Mutable { inner: b, .. }) => {
424                Rc::ptr_eq(a, b)
425            }
426            _ => false,
427        }
428    }
429}
430
431impl<T: Clone + 'static> Signal<T> {
432    /// Set a new value. Marks the signal as dirty and notifies observers.
433    /// Panics if called on a derived (read-only) signal.
434    ///
435    /// Observers may freely re-enter `set`/`try_set`/`observe`, or drop their
436    /// `ObserverHandle`, on this same signal from within their callback — see
437    /// [`try_set`](Self::try_set).
438    pub fn set(&self, value: T) {
439        self.try_set(value)
440            .expect("cannot set() on a derived Signal — it is read-only");
441    }
442
443    /// Set a new value only if it differs from the current one, returning
444    /// whether it changed.
445    ///
446    /// [`set`](Self::set) has no equality check by design — it writes and fans
447    /// out to every observer unconditionally. That is the right default for a
448    /// signal carrying a value whose identity matters, but it makes a
449    /// *republish* of an unchanged value cost a full observer walk. On a
450    /// per-frame path that is pure waste: the text editors' scroll-metric step
451    /// republishes four signals every tick and measured ~5% of frame CPU in
452    /// `set<f32>` before its call sites were guarded by hand.
453    ///
454    /// This is also exactly the guard the [`try_set`](Self::try_set) docs
455    /// prescribe for reactive writes that might cycle, so reach for this rather
456    /// than open-coding `if sig.get() != v { sig.set(v) }` — it is the same
457    /// thing, named, and it cannot be forgotten at one call site out of four.
458    ///
459    /// Equality is `PartialEq`, not an epsilon. For floats that is deliberate:
460    /// a tolerance like `f32::EPSILON` is the machine epsilon *near 1.0*, so
461    /// past a magnitude of about 1.0 the smallest representable step already
462    /// exceeds it and the comparison silently degrades into exact inequality
463    /// anyway — while near zero it would suppress writes that genuinely
464    /// changed. A caller that truly wants a tolerance wants a domain-specific
465    /// one, and should say so at its own call site.
466    ///
467    /// Panics on a derived (read-only) signal, like [`set`](Self::set).
468    pub fn set_if_changed(&self, value: T) -> bool
469    where
470        T: PartialEq,
471    {
472        if self.get() == value {
473            return false;
474        }
475        self.set(value);
476        true
477    }
478
479    /// Fallible [`set`](Self::set): returns [`SignalAccessError::ReadOnly`]
480    /// for a derived signal instead of panicking.
481    ///
482    /// The new value and the observer callbacks are snapshotted while the
483    /// inner `RefCell` is borrowed, then **all** borrows are released before
484    /// any callback runs. An observer is therefore free to re-enter
485    /// `set`/`try_set`/`observe`, or drop an `ObserverHandle`, on this same
486    /// signal without a `RefCell` borrow conflict — mirroring the
487    /// mutate-then-notify discipline used across `teksilo-data`. Each
488    /// notification delivers the value as written by *that* call; observer
489    /// additions or removals made during a callback take effect only on
490    /// subsequent notifications.
491    ///
492    /// # Feedback loops
493    ///
494    /// Re-entrancy is supported, but a write cascade that never settles — A's
495    /// observer writes B and B's observer writes A, with no equality guard — is
496    /// an unbounded recursion that will overflow the stack. Guard reactive
497    /// writes that may cycle (`if sig.get() != v { sig.set(v) }`) or break one
498    /// edge with a [`WeakSignal`]. In debug builds a depth guard turns a runaway
499    /// loop into a diagnostic panic instead of a silent stack overflow; release
500    /// builds carry no such check.
501    pub fn try_set(&self, value: T) -> Result<(), SignalAccessError> {
502        match &self.kind {
503            SignalKind::Mutable { inner, .. } => {
504                let (snapshot, callbacks) = {
505                    let mut guard = inner.borrow_mut();
506                    guard.value = value;
507                    guard.generation = guard.generation.wrapping_add(1);
508                    let callbacks: Vec<_> =
509                        guard.observers.iter().map(|e| e.callback.clone()).collect();
510                    (guard.value.clone(), callbacks)
511                };
512                // Debug-only: a re-entrant observer bumps this depth; an
513                // unbounded feedback loop trips the limit and panics with a
514                // diagnostic rather than overflowing the stack.
515                #[cfg(debug_assertions)]
516                let _depth = NotifyDepthGuard::enter();
517                for cb in &callbacks {
518                    cb(&snapshot);
519                }
520                Ok(())
521            }
522            SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
523        }
524    }
525
526    /// Read the current value (cloned).
527    pub fn get(&self) -> T {
528        match &self.kind {
529            SignalKind::Mutable { inner, .. } => inner.borrow().value.clone(),
530            SignalKind::Derived { compute, .. } => compute(),
531        }
532    }
533
534    /// Read the current value by reference (only for mutable signals).
535    /// Panics on derived signals.
536    pub fn get_ref(&self) -> Ref<'_, T> {
537        self.try_get_ref()
538            .expect("get_ref() is only supported on mutable signals")
539    }
540
541    pub fn try_get_ref(&self) -> Result<Ref<'_, T>, SignalAccessError> {
542        match &self.kind {
543            SignalKind::Mutable { inner, .. } => Ok(Ref::map(inner.borrow(), |guard| &guard.value)),
544            SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
545        }
546    }
547
548    /// Create a derived (read-only) signal whose value is computed from
549    /// this signal. The closure runs lazily when the derived signal is read.
550    pub fn map<U: Clone + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Signal<U> {
551        let compute = self.as_compute();
552        let sources = self.as_sources();
553        Signal {
554            kind: SignalKind::Derived {
555                compute: Rc::new(move || f(&compute())),
556                sources,
557            },
558        }
559    }
560
561    /// Zip this signal with another, producing a derived signal that
562    /// observes both upstream sources. The resulting signal is marked
563    /// dirty whenever *either* source flips, so widgets binding to it
564    /// correctly re-render on any input change.
565    ///
566    /// Combine with [`Signal::map`] for n-ary predicates:
567    ///
568    /// ```
569    /// # use teksilo_core::Signal;
570    /// # let focus = Signal::new(false);
571    /// # let readonly = Signal::new(false);
572    /// let composite = focus.zip(&readonly).map(|(f, r)| *f && !*r);
573    /// ```
574    pub fn zip<U: Clone + 'static>(&self, other: &Signal<U>) -> Signal<(T, U)> {
575        let a = self.as_compute();
576        let b = other.as_compute();
577        let mut sources = self.as_sources();
578        merge_sources(&mut sources, other.as_sources());
579        Signal {
580            kind: SignalKind::Derived {
581                compute: Rc::new(move || (a(), b())),
582                sources,
583            },
584        }
585    }
586
587    /// Zip three signals. See [`Signal::zip`].
588    pub fn zip3<U: Clone + 'static, V: Clone + 'static>(
589        &self,
590        b: &Signal<U>,
591        c: &Signal<V>,
592    ) -> Signal<(T, U, V)> {
593        let fa = self.as_compute();
594        let fb = b.as_compute();
595        let fc = c.as_compute();
596        let mut sources = self.as_sources();
597        merge_sources(&mut sources, b.as_sources());
598        merge_sources(&mut sources, c.as_sources());
599        Signal {
600            kind: SignalKind::Derived {
601                compute: Rc::new(move || (fa(), fb(), fc())),
602                sources,
603            },
604        }
605    }
606
607    /// Like [`map`](Self::map), but the resulting derived signal
608    /// presents a **single** combined `DerivedSource` to binding
609    /// registrations instead of one per upstream root.
610    ///
611    /// For animation-driven multi-axis signals — e.g. a
612    /// `pan_x.zip3(pan_y, zoom).zip(rotation)` view transform that
613    /// flips all four sources on every animation tick — this collapses
614    /// the per-tick binding work from O(N) to O(1) without changing
615    /// dirty-tracking semantics: the composite source is dirty when
616    /// any underlying source is, and clearing the composite clears
617    /// every underlying source.
618    ///
619    /// Use only when the derived signal's value depends on **all**
620    /// upstream sources being read together (a compose function);
621    /// when only one upstream changes per frame, [`map`](Self::map) is
622    /// equivalent and cheaper.
623    pub fn map_coalesced<U: Clone + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Signal<U> {
624        let compute = self.as_compute();
625        let underlying = self.as_sources();
626        if underlying.len() <= 1 {
627            // One source already — no coalescing benefit; delegate
628            // to plain map to avoid an extra indirection.
629            return self.map(f);
630        }
631        let coalesced = coalesced_source(Rc::new(move || {
632            underlying.iter().map(|s| (s.generation)()).collect()
633        }));
634        Signal {
635            kind: SignalKind::Derived {
636                compute: Rc::new(move || f(&compute())),
637                sources: vec![coalesced],
638            },
639        }
640    }
641
642    /// Switch / bind: derive a signal whose value **and** dirty-tracking
643    /// follow the inner `Signal<U>` selected by `f` from this signal's
644    /// current value. When *this* signal changes, `f` re-selects a
645    /// (possibly different) inner signal and the result follows that one
646    /// instead — the classic reactive "switchLatest" combinator.
647    ///
648    /// Unlike [`map`](Self::map), the result depends on an inner source
649    /// that is chosen dynamically, so it exposes a **single composite
650    /// `DerivedSource`** whose dirty/clear evaluate the currently-selected
651    /// inner each time they are polled. Binding registration stays O(1)
652    /// regardless of how many distinct inner signals `f` may return.
653    ///
654    /// Typical use — track the *active* item's reactive flag out of a set:
655    ///
656    /// ```
657    /// # use teksilo_core::Signal;
658    /// # let current_step: Signal<usize> = Signal::new(0);
659    /// # let completion: Vec<Signal<bool>> = vec![Signal::new(true), Signal::new(false)];
660    /// // disable Next until the currently-shown step's gate is satisfied
661    /// let gate = current_step.flat_map(move |i| completion[*i].clone());
662    /// // ctx.enabled_when(next_id, gate);
663    /// # let _ = gate;
664    /// ```
665    pub fn flat_map<U: Clone + 'static>(&self, f: impl Fn(&T) -> Signal<U> + 'static) -> Signal<U> {
666        let f: Rc<dyn Fn(&T) -> Signal<U>> = Rc::new(f);
667        let outer_compute = self.as_compute();
668        let outer_sources = self.as_sources();
669
670        // compute: select the inner signal from the outer value, read it.
671        let compute: Rc<dyn Fn() -> U> = {
672            let f = f.clone();
673            let outer_compute = outer_compute.clone();
674            Rc::new(move || f(&outer_compute()).get())
675        };
676
677        // One composite source, whose inputs are the outer sources' own
678        // generations PLUS the generations of whichever inner signal is
679        // selected right now. The inner half is spliced in as its
680        // individual leaf generations rather than as one folded `u64`:
681        // the selected inner can change identity between polls, so the
682        // *shape* of the input vector is itself information — a switch
683        // from a 1-source inner to a 2-source one is a change even if
684        // the numbers happen to line up. `coalesced_source` turns the
685        // whole vector back into a monotone counter.
686        let composite = coalesced_source({
687            let f = f.clone();
688            let outer_compute = outer_compute.clone();
689            Rc::new(move || {
690                let mut gens: Vec<u64> = outer_sources.iter().map(|s| (s.generation)()).collect();
691                gens.extend(
692                    f(&outer_compute())
693                        .as_sources()
694                        .iter()
695                        .map(|s| (s.generation)()),
696                );
697                gens
698            })
699        });
700
701        Signal {
702            kind: SignalKind::Derived {
703                compute,
704                sources: vec![composite],
705            },
706        }
707    }
708
709    /// Borrow a compute closure that reads this signal's current value.
710    /// For mutable signals this clones the inner cell's value; for
711    /// derived signals it clones the parent compute `Rc`.
712    fn as_compute(&self) -> Rc<dyn Fn() -> T> {
713        match &self.kind {
714            SignalKind::Mutable { inner, .. } => {
715                let source = inner.clone();
716                Rc::new(move || source.borrow().value.clone())
717            }
718            SignalKind::Derived { compute, .. } => compute.clone(),
719        }
720    }
721
722    /// Bind this signal to a widget at the given dirty-tracking level.
723    ///
724    /// For a mutable or single-source derived signal this registers one
725    /// binding. For a multi-source derived signal (built via `zip` /
726    /// `zip3` / `and` / `or`) this registers one binding per observed
727    /// mutable root so dirty flips on *any* source correctly re-render
728    /// the widget.
729    ///
730    /// Idempotent per `(widget_id, source_id, bucket)` tuple: duplicate
731    /// calls collapse in the [`BindingRegistry`], promoting the level
732    /// if the incoming one has higher priority.
733    pub fn bind_to(&self, widget_id: WidgetId, registry: &BindingRegistry, level: BindingLevel) {
734        for src in self.as_sources() {
735            registry.register(Binding {
736                widget_id,
737                level,
738                generation: src.generation,
739                source_id: src.source_id,
740            });
741        }
742    }
743
744    /// Materialise this signal's upstream sources as a `Vec`. Mutable
745    /// signals yield one entry anchored on their inner `Rc`; derived
746    /// signals clone their existing sources vec.
747    fn as_sources(&self) -> Vec<DerivedSource> {
748        match &self.kind {
749            SignalKind::Mutable { inner, .. } => {
750                let gen_src = inner.clone();
751                let source_id = Rc::as_ptr(inner) as *const () as usize;
752                vec![DerivedSource {
753                    // The closure owns an `Rc` clone of the inner, so the
754                    // address used as `source_id` cannot be recycled by a
755                    // different signal while this source is reachable.
756                    generation: Rc::new(move || gen_src.borrow().generation),
757                    source_id,
758                }]
759            }
760            SignalKind::Derived { sources, .. } => sources.clone(),
761        }
762    }
763}
764
765/// Extend `dst` with entries from `incoming`, deduping by `source_id`
766/// so the same mutable root is never registered twice in a combined
767/// derived signal (e.g. `a.zip(&a.map(...))`).
768fn merge_sources(dst: &mut Vec<DerivedSource>, incoming: Vec<DerivedSource>) {
769    for s in incoming {
770        if !dst.iter().any(|d| d.source_id == s.source_id) {
771            dst.push(s);
772        }
773    }
774}
775
776impl<T: 'static> Signal<T> {
777    pub fn is_mutable(&self) -> bool {
778        matches!(self.kind, SignalKind::Mutable { .. })
779    }
780
781    /// This signal's current change generation — a monotonically
782    /// non-decreasing counter advanced by every write. Two reads returning
783    /// the same value mean nothing changed in between; any difference
784    /// means something did.
785    ///
786    /// There is deliberately no way to *reset* it. Dirty tracking is
787    /// "compare against what I last acted on", and the remembered value
788    /// belongs to the consumer — see [`BindingRegistry`], which keeps
789    /// one per bound source. A resettable flag on the signal itself would
790    /// mean N consumers fighting over one slot, which is precisely the bug
791    /// this counter replaced (see `MutableInner::generation`).
792    ///
793    /// For a derived signal this folds every upstream into one number by
794    /// summing. That is exact rather than merely convenient: every
795    /// upstream generation is itself monotone, so a sum can only stay put
796    /// when all of them do.
797    pub fn generation(&self) -> u64 {
798        match &self.kind {
799            SignalKind::Mutable { inner, .. } => inner.borrow().generation,
800            SignalKind::Derived { sources, .. } => sources
801                .iter()
802                .map(|s| (s.generation)())
803                .fold(0u64, u64::wrapping_add),
804        }
805    }
806}
807
808// ---------------------------------------------------------------------------
809// Signal<bool> — boolean combinators for composite enabled_when predicates
810// ---------------------------------------------------------------------------
811
812impl Signal<bool> {
813    /// Logical AND of two boolean signals. The resulting derived signal
814    /// tracks both upstream sources and is marked dirty whenever either
815    /// changes (no short-circuit on the dirty side — semantic correctness
816    /// over micro-optimisation).
817    pub fn and(&self, other: &Signal<bool>) -> Signal<bool> {
818        self.zip(other).map(|(a, b)| *a && *b)
819    }
820
821    /// Logical OR of two boolean signals. Same dirty-tracking semantics
822    /// as [`Signal::and`].
823    pub fn or(&self, other: &Signal<bool>) -> Signal<bool> {
824        self.zip(other).map(|(a, b)| *a || *b)
825    }
826
827    /// Logical NOT of a boolean signal.
828    pub fn not(&self) -> Signal<bool> {
829        self.map(|b| !*b)
830    }
831}
832
833// ---------------------------------------------------------------------------
834// Signal<f32> — animation support
835// ---------------------------------------------------------------------------
836
837impl Signal<f32> {
838    /// Create a new `Signal<f32>` with animation support.
839    pub fn new_animated(value: f32) -> Self {
840        Self {
841            kind: SignalKind::Mutable {
842                inner: Rc::new(RefCell::new(MutableInner {
843                    value,
844                    generation: 0,
845                    observers: Vec::new(),
846                    next_observer_id: 1,
847                    keepalive: Vec::new(),
848                })),
849                animation: Some(Rc::new(RefCell::new(AnimationState {
850                    pending: None,
851                    target: None,
852                }))),
853            },
854        }
855    }
856
857    pub fn supports_animation(&self) -> bool {
858        matches!(
859            &self.kind,
860            SignalKind::Mutable {
861                animation: Some(_),
862                ..
863            }
864        )
865    }
866
867    pub(crate) fn weak_handle(&self) -> Option<WeakAnimatedSignal> {
868        match &self.kind {
869            SignalKind::Mutable {
870                inner,
871                animation: Some(animation),
872            } => Some(WeakAnimatedSignal {
873                inner: Rc::downgrade(inner),
874                animation: Rc::downgrade(animation),
875            }),
876            _ => None,
877        }
878    }
879
880    /// Animate to a target value over a duration with an easing curve.
881    pub fn animate_to(
882        &self,
883        target: f32,
884        duration: std::time::Duration,
885        easing: teksilo_tokens::Easing,
886    ) {
887        self.animate_to_with_frame_interval(target, duration, easing, None);
888    }
889
890    pub fn animate_to_with_frame_interval(
891        &self,
892        target: f32,
893        duration: std::time::Duration,
894        easing: teksilo_tokens::Easing,
895        frame_interval: Option<std::time::Duration>,
896    ) {
897        self.try_animate_to_with_frame_interval(target, duration, easing, frame_interval)
898            .unwrap_or_else(|err| match err {
899                SignalAccessError::ReadOnly => {
900                    panic!("animate_to is not supported on derived signals")
901                }
902                SignalAccessError::AnimationUnsupported => {
903                    panic!(
904                        "animate_to called on Signal<f32> without animation support; use Signal::new_animated()"
905                    )
906                }
907            });
908    }
909
910    pub fn try_animate_to(
911        &self,
912        target: f32,
913        duration: std::time::Duration,
914        easing: teksilo_tokens::Easing,
915    ) -> Result<(), SignalAccessError> {
916        self.try_animate_to_with_frame_interval(target, duration, easing, None)
917    }
918
919    pub fn try_animate_to_with_frame_interval(
920        &self,
921        target: f32,
922        duration: std::time::Duration,
923        easing: teksilo_tokens::Easing,
924        frame_interval: Option<std::time::Duration>,
925    ) -> Result<(), SignalAccessError> {
926        self.try_animate_with_options(crate::animation::AnimationRequest {
927            target,
928            duration,
929            easing,
930            frame_interval,
931            looping: false,
932            epsilon: 0.0,
933            max_duration: None,
934        })
935    }
936
937    /// Start a looping animation from the current value to `target`,
938    /// repeating with the given period. Runs until cancelled.
939    /// Frame updates are capped at `frame_interval` (default 60 Hz).
940    pub fn animate_looping(
941        &self,
942        target: f32,
943        period: std::time::Duration,
944        easing: teksilo_tokens::Easing,
945        frame_interval: Option<std::time::Duration>,
946    ) {
947        let _ = self.try_animate_with_options(crate::animation::AnimationRequest {
948            target,
949            duration: period,
950            easing,
951            frame_interval,
952            looping: true,
953            epsilon: 0.0,
954            max_duration: None,
955        });
956    }
957
958    /// Generic entry point that accepts a fully-built `AnimationRequest`.
959    /// Use this to set `epsilon` (pixel-stable quantization) or
960    /// `max_duration` (opt-in wall-clock cap) without having to thread
961    /// through every convenience wrapper.
962    pub fn try_animate_with_options(
963        &self,
964        request: crate::animation::AnimationRequest,
965    ) -> Result<(), SignalAccessError> {
966        match &self.kind {
967            SignalKind::Mutable {
968                inner,
969                animation: Some(animation),
970            } => {
971                let mut anim = animation.borrow_mut();
972                anim.target = Some(request.target);
973                anim.pending = Some(request);
974                drop(anim);
975                // Arming an animation is a change like any other write:
976                // bound widgets must reconcile so the tree picks the
977                // pending request up in `process_pending_animations`.
978                let mut guard = inner.borrow_mut();
979                guard.generation = guard.generation.wrapping_add(1);
980                Ok(())
981            }
982            SignalKind::Mutable {
983                animation: None, ..
984            } => Err(SignalAccessError::AnimationUnsupported),
985            SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
986        }
987    }
988
989    /// Returns the target value of the current or pending animation, if any.
990    pub fn animation_target(&self) -> Option<f32> {
991        match &self.kind {
992            SignalKind::Mutable { animation, .. } => {
993                animation.as_ref().and_then(|a| a.borrow().target)
994            }
995            _ => None,
996        }
997    }
998
999    /// Clear the animation target. Called by the animation scheduler when
1000    /// an animation completes.
1001    pub fn clear_animation_target(&self) {
1002        if let SignalKind::Mutable { animation, .. } = &self.kind
1003            && let Some(a) = animation
1004        {
1005            a.borrow_mut().target = None;
1006        }
1007    }
1008
1009    /// Take a pending animation request, if any.
1010    pub fn take_pending_animation(&self) -> Option<crate::animation::AnimationRequest> {
1011        match &self.kind {
1012            SignalKind::Mutable { animation, .. } => animation
1013                .as_ref()
1014                .and_then(|a| a.borrow_mut().pending.take()),
1015            _ => None,
1016        }
1017    }
1018
1019    /// Whether there is a pending animation request.
1020    pub fn has_pending_animation(&self) -> bool {
1021        match &self.kind {
1022            SignalKind::Mutable { animation, .. } => animation
1023                .as_ref()
1024                .is_some_and(|a| a.borrow().pending.is_some()),
1025            _ => false,
1026        }
1027    }
1028}
1029
1030// ---------------------------------------------------------------------------
1031// Clone, Debug
1032// ---------------------------------------------------------------------------
1033
1034impl<T> Clone for Signal<T> {
1035    fn clone(&self) -> Self {
1036        Self {
1037            kind: match &self.kind {
1038                SignalKind::Mutable { inner, animation } => SignalKind::Mutable {
1039                    inner: inner.clone(),
1040                    animation: animation.clone(),
1041                },
1042                SignalKind::Derived { compute, sources } => SignalKind::Derived {
1043                    compute: compute.clone(),
1044                    sources: sources.clone(),
1045                },
1046            },
1047        }
1048    }
1049}
1050
1051impl<T: std::fmt::Debug + 'static> std::fmt::Debug for Signal<T> {
1052    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1053        match &self.kind {
1054            SignalKind::Mutable { inner, .. } => f
1055                .debug_struct("Signal::Mutable")
1056                .field("value", &inner.borrow().value)
1057                .field("generation", &inner.borrow().generation)
1058                .finish(),
1059            SignalKind::Derived { .. } => f.write_str("Signal::Derived(..)"),
1060        }
1061    }
1062}
1063
1064// ---------------------------------------------------------------------------
1065// Prop<T> — widget property type
1066// ---------------------------------------------------------------------------
1067
1068/// A property value that is either static or bound to a reactive signal.
1069/// Widget property methods accept `impl Into<Prop<T>>` for flexibility.
1070pub enum Prop<T: Clone + 'static> {
1071    /// A fixed value, set once.
1072    Static(T),
1073    /// Bound to a signal; value read lazily on each use.
1074    Bound(Signal<T>),
1075}
1076
1077impl<T: Clone + 'static> Prop<T> {
1078    /// Resolve the current value.
1079    pub fn get(&self) -> T {
1080        match self {
1081            Prop::Static(v) => v.clone(),
1082            Prop::Bound(signal) => signal.get(),
1083        }
1084    }
1085
1086    /// Register dirty tracking for this prop if it is bound.
1087    pub fn register_if_bound(
1088        &self,
1089        widget_id: WidgetId,
1090        registry: &BindingRegistry,
1091        level: BindingLevel,
1092    ) {
1093        if let Prop::Bound(signal) = self {
1094            signal.bind_to(widget_id, registry, level);
1095        }
1096    }
1097
1098    /// Return the underlying signal if bound, or wrap a static value in a
1099    /// fresh, unshared signal. Use when an existing code path needs a
1100    /// `Signal<T>` (e.g. `ctx.effect(&signal, ...)`) but the widget field
1101    /// was widened from `Signal<T>` to `Prop<T>` — the derived signal
1102    /// preserves reactivity for the `Bound` case with minimal churn.
1103    pub fn as_signal(&self) -> Signal<T> {
1104        match self {
1105            Prop::Static(v) => Signal::new(v.clone()),
1106            Prop::Bound(signal) => signal.clone(),
1107        }
1108    }
1109}
1110
1111impl<T: Clone + 'static> Clone for Prop<T> {
1112    fn clone(&self) -> Self {
1113        match self {
1114            Prop::Static(v) => Prop::Static(v.clone()),
1115            Prop::Bound(s) => Prop::Bound(s.clone()),
1116        }
1117    }
1118}
1119
1120impl<T: Clone + std::fmt::Debug + 'static> std::fmt::Debug for Prop<T> {
1121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1122        match self {
1123            Prop::Static(v) => write!(f, "Prop::Static({:?})", v),
1124            Prop::Bound(_) => f.write_str("Prop::Bound(..)"),
1125        }
1126    }
1127}
1128
1129impl<T: Clone + 'static> From<T> for Prop<T> {
1130    fn from(value: T) -> Self {
1131        Prop::Static(value)
1132    }
1133}
1134
1135impl<T: Clone + 'static> From<Signal<T>> for Prop<T> {
1136    fn from(signal: Signal<T>) -> Self {
1137        Prop::Bound(signal)
1138    }
1139}
1140
1141// String ergonomics: let `impl Into<Prop<String>>` setters accept a borrowed
1142// string literal / `&String` the same way the old `impl Into<String>` setters
1143// did (the blanket `From<T>` only covers an owned `String`). Keeps call sites
1144// like `.name("x")` / `.suffix("x")` compiling after the widening.
1145impl From<&str> for Prop<String> {
1146    fn from(s: &str) -> Self {
1147        Prop::Static(s.to_owned())
1148    }
1149}
1150
1151impl From<&String> for Prop<String> {
1152    fn from(s: &String) -> Self {
1153        Prop::Static(s.clone())
1154    }
1155}
1156
1157// ---------------------------------------------------------------------------
1158// Tests
1159// ---------------------------------------------------------------------------
1160
1161#[cfg(test)]
1162mod tests {
1163    use super::*;
1164
1165    #[test]
1166    fn signal_get_set() {
1167        let s = Signal::new(42);
1168        assert_eq!(s.get(), 42);
1169        s.set(99);
1170        assert_eq!(s.get(), 99);
1171    }
1172
1173    #[test]
1174    fn generation_advances_on_every_write_and_never_resets() {
1175        let s = Signal::new(0);
1176        let start = s.generation();
1177        assert_eq!(s.generation(), start, "reading is not a change");
1178
1179        s.set(1);
1180        let after_one = s.generation();
1181        assert_ne!(after_one, start, "a write advances the generation");
1182
1183        s.set(1);
1184        let after_republish = s.generation();
1185        assert_ne!(
1186            after_republish, after_one,
1187            "`set` is unconditional — a republish of the same value is still \
1188             a write, and callers who want the equality guard use \
1189             `set_if_changed`"
1190        );
1191
1192        assert!(!s.set_if_changed(1), "value is unchanged");
1193        assert_eq!(
1194            s.generation(),
1195            after_republish,
1196            "`set_if_changed` with an identical value writes nothing at all"
1197        );
1198    }
1199
1200    /// The property every consumer relies on: staleness is "the
1201    /// generation moved since I last looked", and looking is free of
1202    /// consequence — so any number of independent consumers can each
1203    /// track the same signal without interfering.
1204    #[test]
1205    fn observing_the_generation_does_not_consume_it() {
1206        let s = Signal::new(0);
1207        let (mut seen_a, mut seen_b) = (s.generation(), s.generation());
1208
1209        s.set(1);
1210        assert_ne!(s.generation(), seen_a);
1211        seen_a = s.generation();
1212        assert_ne!(
1213            s.generation(),
1214            seen_b,
1215            "consumer A catching up must leave consumer B behind, not clean"
1216        );
1217        seen_b = s.generation();
1218
1219        assert_eq!(s.generation(), seen_a);
1220        assert_eq!(s.generation(), seen_b);
1221    }
1222
1223    #[test]
1224    fn signal_clone_shares() {
1225        let a = Signal::new(10);
1226        let b = a.clone();
1227        a.set(20);
1228        assert_eq!(b.get(), 20);
1229        assert!(Signal::same(&a, &b));
1230    }
1231
1232    #[test]
1233    fn signal_map_derived() {
1234        let text = Signal::new(String::from("hello"));
1235        let len = text.map(|t| t.len());
1236        assert_eq!(len.get(), 5);
1237        text.set(String::from("hi"));
1238        assert_eq!(len.get(), 2);
1239    }
1240
1241    #[test]
1242    fn signal_map_chained() {
1243        let s = Signal::new(5);
1244        let doubled = s.map(|v| v * 2);
1245        let as_string = doubled.map(|v| format!("{}", v));
1246        assert_eq!(as_string.get(), "10");
1247        s.set(7);
1248        assert_eq!(as_string.get(), "14");
1249    }
1250
1251    #[test]
1252    fn signal_derived_generation_tracks_source() {
1253        let s = Signal::new(0);
1254        let derived = s.map(|v| v + 1);
1255        let seen = derived.generation();
1256        s.set(5);
1257        assert_ne!(
1258            derived.generation(),
1259            seen,
1260            "the source's write shows through"
1261        );
1262        let seen = derived.generation();
1263        assert_eq!(
1264            derived.generation(),
1265            seen,
1266            "and settles with no further write"
1267        );
1268    }
1269
1270    #[test]
1271    fn flat_map_follows_selected_inner_value() {
1272        let a = Signal::new(10);
1273        let b = Signal::new(20);
1274        let which = Signal::new(0usize);
1275        let (a2, b2) = (a.clone(), b.clone());
1276        let out = which.flat_map(move |i| if *i == 0 { a2.clone() } else { b2.clone() });
1277
1278        assert_eq!(out.get(), 10); // follows a
1279        a.set(11);
1280        assert_eq!(out.get(), 11); // tracks a's value
1281        which.set(1);
1282        assert_eq!(out.get(), 20); // switched to b
1283        b.set(21);
1284        assert_eq!(out.get(), 21);
1285        a.set(999); // a is no longer selected — ignored
1286        assert_eq!(out.get(), 21);
1287    }
1288
1289    #[test]
1290    fn flat_map_generation_tracks_outer_and_current_inner() {
1291        let a = Signal::new(0);
1292        let b = Signal::new(0);
1293        let which = Signal::new(0usize);
1294        let (a2, b2) = (a.clone(), b.clone());
1295        let out = which.flat_map(move |i| if *i == 0 { a2.clone() } else { b2.clone() });
1296        let mut seen = out.generation();
1297
1298        // The current inner (a) flipping advances the result.
1299        a.set(5);
1300        assert_ne!(out.generation(), seen);
1301        seen = out.generation();
1302        assert_eq!(out.generation(), seen);
1303
1304        // The non-selected inner (b) flipping does NOT.
1305        b.set(7);
1306        assert_eq!(out.generation(), seen, "b is not selected");
1307
1308        // The outer selector flipping does.
1309        which.set(1);
1310        assert_ne!(out.generation(), seen);
1311        seen = out.generation();
1312
1313        // Now b is selected, so b flipping is tracked and a is ignored.
1314        b.set(8);
1315        assert_ne!(out.generation(), seen);
1316        seen = out.generation();
1317        a.set(9);
1318        assert_eq!(out.generation(), seen, "a is no longer selected");
1319    }
1320
1321    /// `flat_map` is why the composite source memoises a counter rather
1322    /// than summing its inputs. Switching the selected inner makes that
1323    /// term jump arbitrarily — here it jumps *down*, from a heavily
1324    /// written signal to a fresh one, by exactly as much as the outer
1325    /// selector's own write advanced. A sum would land on the same total
1326    /// and report "nothing changed" for a switch that changed everything.
1327    #[test]
1328    fn flat_map_survives_an_inner_switch_that_would_cancel_out_in_a_sum() {
1329        let hot = Signal::new(0_i32);
1330        let cold = Signal::new(0_i32);
1331        // `which` reaches generation 1 on the switch below, so drive
1332        // `hot` exactly one generation ahead of `cold`: switching from
1333        // hot to cold then costs -1 while `which` contributes +1.
1334        hot.set(1);
1335        let (hot2, cold2) = (hot.clone(), cold.clone());
1336        let which = Signal::new(0usize);
1337        let out = which.flat_map(move |i| if *i == 0 { hot2.clone() } else { cold2.clone() });
1338
1339        assert_eq!(out.get(), 1, "starts on `hot`");
1340        let seen = out.generation();
1341
1342        which.set(1);
1343
1344        assert_eq!(out.get(), 0, "the value really did change");
1345        assert_ne!(
1346            out.generation(),
1347            seen,
1348            "and the generation says so — a plain sum of (outer + inner) \
1349             would have been unchanged here"
1350        );
1351    }
1352
1353    /// The cross-window property at the level of a composite source: a
1354    /// `flat_map`'s memo advances once and is then read, unchanged, by
1355    /// every consumer polling afterwards. If the memo were consumed by
1356    /// the first reader (the way the old `clear_dirty` consumed a shared
1357    /// flag) the second window would never rebuild.
1358    #[test]
1359    fn a_composite_sources_generation_is_readable_by_every_consumer() {
1360        let inner = Signal::new(0_i32);
1361        let inner2 = inner.clone();
1362        let which = Signal::new(0usize);
1363        let out = which.flat_map(move |_| inner2.clone());
1364
1365        let (window_a, window_b) = (out.generation(), out.generation());
1366        inner.set(1);
1367
1368        let a_now = out.generation();
1369        assert_ne!(a_now, window_a, "window A notices");
1370        assert_ne!(
1371            out.generation(),
1372            window_b,
1373            "and window B still notices, after A already looked"
1374        );
1375        assert_eq!(out.generation(), a_now, "both see the SAME new generation");
1376    }
1377
1378    #[test]
1379    fn flat_map_binding_rerenders_on_inner_and_outer_change() {
1380        use crate::binding::{BindingLevel, BindingRegistry};
1381        use slotmap::KeyData;
1382        let fake_id: WidgetId = KeyData::from_ffi(1).into();
1383        let inner = Signal::new(false);
1384        let which = Signal::new(0usize);
1385        let inner2 = inner.clone();
1386        let gate = which.flat_map(move |_| inner2.clone());
1387
1388        let registry = BindingRegistry::new();
1389        gate.bind_to(fake_id, &registry, BindingLevel::Relayout);
1390        assert!(registry.flush_dirty().is_empty());
1391
1392        // A change to the currently-selected inner must dirty the bound widget.
1393        inner.set(true);
1394        let dirty = registry.flush_dirty();
1395        assert_eq!(dirty.len(), 1, "selected-inner change must re-render");
1396        assert_eq!(dirty[0].0, fake_id);
1397
1398        // A change to the outer selector must also dirty it.
1399        which.set(0);
1400        let dirty = registry.flush_dirty();
1401        assert_eq!(dirty.len(), 1, "outer-selector change must re-render");
1402    }
1403
1404    #[test]
1405    fn observer_called_on_set() {
1406        use std::cell::Cell;
1407        let s = Signal::new(0);
1408        let called = Rc::new(Cell::new(false));
1409        let c = called.clone();
1410        let _handle = s.observe(move |val| {
1411            assert_eq!(*val, 42);
1412            c.set(true);
1413        });
1414        s.set(42);
1415        assert!(called.get());
1416    }
1417
1418    /// The whole reason `set_if_changed` exists: `set` fans out to every
1419    /// observer even when the value is identical, which on a per-frame
1420    /// republish path is pure waste.
1421    #[test]
1422    fn set_if_changed_does_not_notify_when_the_value_is_identical() {
1423        use std::cell::Cell;
1424        let s = Signal::new(7);
1425        let calls = Rc::new(Cell::new(0));
1426        let c = calls.clone();
1427        let _handle = s.observe(move |_| c.set(c.get() + 1));
1428
1429        assert!(
1430            !s.set_if_changed(7),
1431            "an identical write must report no change"
1432        );
1433        assert_eq!(calls.get(), 0, "an identical write must not walk observers");
1434
1435        assert!(
1436            s.set_if_changed(8),
1437            "a differing write must report a change"
1438        );
1439        assert_eq!(calls.get(), 1, "a differing write must notify");
1440        assert_eq!(s.get(), 8);
1441    }
1442
1443    /// Guarding a write is what breaks an A→B→A observer cycle, which the
1444    /// `try_set` docs prescribe and which callers previously hand-rolled.
1445    #[test]
1446    fn set_if_changed_settles_a_two_signal_feedback_loop() {
1447        let a = Signal::new(0);
1448        let b = Signal::new(0);
1449        let _ha = {
1450            let b = b.clone();
1451            a.observe(move |v| {
1452                b.set_if_changed(*v);
1453            })
1454        };
1455        let _hb = {
1456            let a = a.clone();
1457            b.observe(move |v| {
1458                a.set_if_changed(*v);
1459            })
1460        };
1461        // Without the equality guard this recurses until the depth guard trips.
1462        a.set(5);
1463        assert_eq!(b.get(), 5);
1464        assert_eq!(a.get(), 5);
1465    }
1466
1467    #[test]
1468    fn observer_removed_on_handle_drop() {
1469        use std::cell::Cell;
1470        let s = Signal::new(0);
1471        let count = Rc::new(Cell::new(0));
1472        let c = count.clone();
1473        let handle = s.observe(move |_| {
1474            c.set(c.get() + 1);
1475        });
1476        s.set(1);
1477        assert_eq!(count.get(), 1);
1478        drop(handle);
1479        s.set(2);
1480        assert_eq!(count.get(), 1); // Not called again
1481    }
1482
1483    #[test]
1484    fn multiple_observers() {
1485        use std::cell::Cell;
1486        let s = Signal::new(0);
1487        let count = Rc::new(Cell::new(0));
1488        let c1 = count.clone();
1489        let c2 = count.clone();
1490        let _h1 = s.observe(move |_| c1.set(c1.get() + 1));
1491        let _h2 = s.observe(move |_| c2.set(c2.get() + 1));
1492        s.set(10);
1493        assert_eq!(count.get(), 2);
1494    }
1495
1496    #[test]
1497    fn binding_registry_integration() {
1498        use slotmap::KeyData;
1499        let fake_id: WidgetId = KeyData::from_ffi(1).into();
1500        let registry = BindingRegistry::new();
1501        let s = Signal::new(0);
1502        s.bind_to(fake_id, &registry, BindingLevel::RepaintOnly);
1503
1504        assert!(registry.flush_dirty().is_empty());
1505        s.set(42);
1506        let dirty = registry.flush_dirty();
1507        assert_eq!(dirty.len(), 1);
1508        assert_eq!(dirty[0].0, fake_id);
1509        assert_eq!(dirty[0].1, BindingLevel::RepaintOnly);
1510        assert!(registry.flush_dirty().is_empty());
1511    }
1512
1513    #[test]
1514    fn derived_binding_registry() {
1515        use slotmap::KeyData;
1516        let fake_id: WidgetId = KeyData::from_ffi(1).into();
1517        let registry = BindingRegistry::new();
1518        let s = Signal::new(0);
1519        let doubled = s.map(|v| v * 2);
1520        doubled.bind_to(fake_id, &registry, BindingLevel::Relayout);
1521
1522        assert!(registry.flush_dirty().is_empty());
1523        s.set(5);
1524        let dirty = registry.flush_dirty();
1525        assert_eq!(dirty.len(), 1);
1526        assert_eq!(dirty[0].1, BindingLevel::Relayout);
1527    }
1528
1529    #[test]
1530    fn get_ref_works() {
1531        let s = Signal::new(String::from("hello"));
1532        {
1533            let r = s.get_ref();
1534            assert_eq!(&*r, "hello");
1535        }
1536    }
1537
1538    #[test]
1539    #[should_panic(expected = "cannot set() on a derived Signal")]
1540    fn set_on_derived_panics() {
1541        let s = Signal::new(0);
1542        let d = s.map(|v| v + 1);
1543        d.set(99);
1544    }
1545
1546    #[test]
1547    fn prop_static() {
1548        let p: Prop<i32> = 42.into();
1549        assert_eq!(p.get(), 42);
1550    }
1551
1552    #[test]
1553    fn prop_bound() {
1554        let s = Signal::new(10);
1555        let p: Prop<i32> = s.clone().into();
1556        assert_eq!(p.get(), 10);
1557        s.set(20);
1558        assert_eq!(p.get(), 20);
1559    }
1560
1561    #[test]
1562    fn prop_register_if_bound() {
1563        use slotmap::KeyData;
1564        let fake_id: WidgetId = KeyData::from_ffi(1).into();
1565        let registry = BindingRegistry::new();
1566
1567        let s = Signal::new(0);
1568        let p: Prop<i32> = s.clone().into();
1569        p.register_if_bound(fake_id, &registry, BindingLevel::RepaintOnly);
1570
1571        s.set(1);
1572        let dirty = registry.flush_dirty();
1573        assert_eq!(dirty.len(), 1);
1574
1575        // Static prop does not register
1576        let p2: Prop<i32> = 42.into();
1577        p2.register_if_bound(fake_id, &registry, BindingLevel::RepaintOnly);
1578        assert!(registry.flush_dirty().is_empty());
1579    }
1580
1581    // --- Multi-source derived signals (zip / zip3 / and / or / not) -------
1582
1583    #[test]
1584    fn zip_reads_both_sources() {
1585        let a = Signal::new(1_i32);
1586        let b = Signal::new("x".to_string());
1587        let z = a.zip(&b);
1588        assert_eq!(z.get(), (1, "x".to_string()));
1589        a.set(7);
1590        b.set("y".to_string());
1591        assert_eq!(z.get(), (7, "y".to_string()));
1592    }
1593
1594    #[test]
1595    fn zip_generation_advances_when_either_source_is_written() {
1596        let a = Signal::new(0_i32);
1597        let b = Signal::new(0_i32);
1598        let z = a.zip(&b);
1599        let mut seen = z.generation();
1600
1601        a.set(1);
1602        assert_ne!(z.generation(), seen, "a write to the first source shows");
1603        seen = z.generation();
1604
1605        b.set(2);
1606        assert_ne!(z.generation(), seen, "a write to the second source shows");
1607        seen = z.generation();
1608
1609        assert_eq!(z.generation(), seen, "and settles with no further write");
1610    }
1611
1612    /// A multi-source derived signal folds its upstreams into one number
1613    /// by summing, and that is only sound because every upstream
1614    /// generation is monotone: writes to *different* sources can never
1615    /// cancel each other out.
1616    #[test]
1617    fn zip_generation_reflects_writes_to_both_sources_independently() {
1618        let a = Signal::new(0_i32);
1619        let b = Signal::new(0_i32);
1620        let z = a.zip(&b);
1621
1622        let start = z.generation();
1623        a.set(1);
1624        let after_a = z.generation();
1625        b.set(1);
1626        let after_b = z.generation();
1627
1628        assert!(
1629            after_a > start && after_b > after_a,
1630            "monotone in both sources: {start} < {after_a} < {after_b}"
1631        );
1632    }
1633
1634    #[test]
1635    fn zip3_reads_three_sources() {
1636        let a = Signal::new(1_i32);
1637        let b = Signal::new(2_i32);
1638        let c = Signal::new(3_i32);
1639        let z = a.zip3(&b, &c);
1640        assert_eq!(z.get(), (1, 2, 3));
1641        c.set(30);
1642        assert_eq!(z.get(), (1, 2, 30));
1643    }
1644
1645    #[test]
1646    fn zip3_generation_advances_for_any_source() {
1647        let a = Signal::new(0_i32);
1648        let b = Signal::new(0_i32);
1649        let c = Signal::new(0_i32);
1650        let z = a.zip3(&b, &c);
1651        let mut seen = z.generation();
1652
1653        for write in [&c, &b, &a] {
1654            write.set(1);
1655            assert_ne!(z.generation(), seen);
1656            seen = z.generation();
1657        }
1658    }
1659
1660    #[test]
1661    fn and_reads_logical_and() {
1662        let a = Signal::new(true);
1663        let b = Signal::new(false);
1664        let anded = a.and(&b);
1665        assert!(!anded.get());
1666        b.set(true);
1667        assert!(anded.get());
1668        a.set(false);
1669        assert!(!anded.get());
1670    }
1671
1672    #[test]
1673    fn or_reads_logical_or() {
1674        let a = Signal::new(false);
1675        let b = Signal::new(false);
1676        let ored = a.or(&b);
1677        assert!(!ored.get());
1678        a.set(true);
1679        assert!(ored.get());
1680        a.set(false);
1681        b.set(true);
1682        assert!(ored.get());
1683    }
1684
1685    #[test]
1686    fn not_reads_logical_negation() {
1687        let a = Signal::new(true);
1688        let n = a.not();
1689        assert!(!n.get());
1690        a.set(false);
1691        assert!(n.get());
1692    }
1693
1694    #[test]
1695    fn combined_predicate_fires_binding_on_any_source() {
1696        use crate::binding::BindingRegistry;
1697        use slotmap::KeyData;
1698
1699        let reg = BindingRegistry::new();
1700        let id: WidgetId = KeyData::from_ffi(1).into();
1701
1702        let focus = Signal::new(false);
1703        let readonly = Signal::new(true);
1704        let in_editor = Signal::new(true);
1705
1706        // Composite: focus && !readonly && in_editor — built with
1707        // combinators, bound to a widget at Relayout level.
1708        let when = focus.and(&readonly.not()).and(&in_editor);
1709        when.bind_to(id, &reg, BindingLevel::Relayout);
1710        assert!(!when.get(), "all sources start producing false");
1711
1712        // Flip any source — the registry must see a dirty binding.
1713        focus.set(true);
1714        let dirty = reg.flush_dirty();
1715        assert_eq!(dirty.len(), 1, "focus change must fire the binding");
1716        assert_eq!(dirty[0].0, id);
1717
1718        // Flip a different source — same widget, same outcome.
1719        readonly.set(false);
1720        let dirty = reg.flush_dirty();
1721        assert_eq!(dirty.len(), 1, "readonly change must fire the binding");
1722        // And the predicate now reads true.
1723        assert!(when.get());
1724
1725        // Third source.
1726        in_editor.set(false);
1727        let dirty = reg.flush_dirty();
1728        assert_eq!(dirty.len(), 1, "in_editor change must fire the binding");
1729        assert!(!when.get());
1730    }
1731
1732    #[test]
1733    fn zip_dedups_identical_source() {
1734        // `a.zip(&a.map(|v| v + 1))` shares one upstream mutable root.
1735        // The derived should register exactly one binding per widget,
1736        // not two.
1737        use crate::binding::BindingRegistry;
1738        use slotmap::KeyData;
1739
1740        let reg = BindingRegistry::new();
1741        let id: WidgetId = KeyData::from_ffi(1).into();
1742        let a = Signal::new(0_i32);
1743        let derived = a.map(|v| v + 1);
1744        let z = a.zip(&derived);
1745        z.bind_to(id, &reg, BindingLevel::RepaintOnly);
1746        assert_eq!(
1747            reg.len(),
1748            1,
1749            "duplicate upstream root must register once, not twice"
1750        );
1751    }
1752
1753    #[test]
1754    fn signal_animated_f32() {
1755        let s = Signal::<f32>::new_animated(0.0);
1756        assert!(!s.has_pending_animation());
1757        s.animate_to(
1758            100.0,
1759            std::time::Duration::from_millis(200),
1760            teksilo_tokens::Easing::Linear,
1761        );
1762        assert!(s.has_pending_animation());
1763        assert_eq!(s.animation_target(), Some(100.0));
1764        let req = s.take_pending_animation().unwrap();
1765        assert_eq!(req.target, 100.0);
1766        assert!(!s.has_pending_animation());
1767    }
1768
1769    #[test]
1770    fn map_coalesced_collapses_multi_source_to_one_binding() {
1771        // A 4-source zip projects through map_coalesced; the
1772        // resulting derived signal exposes a single combined
1773        // DerivedSource. Verifies the source-count collapse.
1774        let a = Signal::new(1u32);
1775        let b = Signal::new(2u32);
1776        let c = Signal::new(3u32);
1777        let d = Signal::new(4u32);
1778        let composite = a
1779            .zip3(&b, &c)
1780            .zip(&d)
1781            .map_coalesced(|((x, y, z), w)| *x + *y + *z + *w);
1782        // Plain `map` would produce 4 sources; `map_coalesced` 1.
1783        assert_eq!(composite.as_sources().len(), 1);
1784        assert_eq!(composite.get(), 10);
1785        // Writing ANY underlying source advances the composite's single
1786        // generation, and it stays put in between.
1787        let mut seen = composite.generation();
1788        a.set(10);
1789        assert_ne!(composite.generation(), seen);
1790        seen = composite.generation();
1791        assert_eq!(composite.generation(), seen);
1792        c.set(30);
1793        assert_ne!(composite.generation(), seen);
1794    }
1795
1796    /// The coalesced composite is one memo shared by every consumer of
1797    /// the derived signal — including two different windows' binding
1798    /// registries. Reading it must not consume it.
1799    #[test]
1800    fn map_coalesced_generation_is_readable_by_every_consumer() {
1801        let a = Signal::new(1u32);
1802        let b = Signal::new(2u32);
1803        let composite = a.zip(&b).map_coalesced(|(x, y)| *x + *y);
1804
1805        let (window_a, window_b) = (composite.generation(), composite.generation());
1806        b.set(5);
1807
1808        let a_now = composite.generation();
1809        assert_ne!(a_now, window_a);
1810        assert_ne!(
1811            composite.generation(),
1812            window_b,
1813            "the first read must not have cleared anything"
1814        );
1815        assert_eq!(composite.generation(), a_now);
1816    }
1817
1818    #[test]
1819    fn map_coalesced_with_single_source_delegates_to_map() {
1820        // No coalescing benefit when there's only one source —
1821        // `map_coalesced` is equivalent to `map`.
1822        let a = Signal::new(7u32);
1823        let derived = a.map_coalesced(|v| *v * 2);
1824        assert_eq!(derived.get(), 14);
1825        assert_eq!(derived.as_sources().len(), 1);
1826    }
1827
1828    #[test]
1829    fn reentrant_set_in_observer_does_not_panic() {
1830        // An observer that writes the same signal must not trip the inner
1831        // RefCell: no borrow is held while callbacks run. (Before the fix,
1832        // try_set held a shared borrow across the callback loop, so the
1833        // nested borrow_mut panicked with BorrowMutError.)
1834        let s = Signal::new(0_i32);
1835        let s2 = s.clone();
1836        let _handle = s.observe(move |v| {
1837            // Recurse exactly once — only re-enter while the value is 1.
1838            if *v == 1 {
1839                s2.set(2);
1840            }
1841        });
1842        s.set(1);
1843        assert_eq!(s.get(), 2);
1844    }
1845
1846    #[cfg(debug_assertions)]
1847    #[test]
1848    #[should_panic(expected = "feedback loop")]
1849    fn unbounded_feedback_loop_panics_with_diagnostic() {
1850        // A's observer bumps B and B's observer bumps A, unconditionally, so
1851        // the cascade never settles. The debug depth guard must convert the
1852        // would-be stack overflow into an actionable panic.
1853        let a = Signal::new(0_i32);
1854        let b = Signal::new(0_i32);
1855        let b_for_a = b.clone();
1856        let _ha = a.observe(move |v| b_for_a.set(*v + 1));
1857        let a_for_b = a.clone();
1858        let _hb = b.observe(move |v| a_for_b.set(*v + 1));
1859        a.set(1);
1860    }
1861
1862    #[test]
1863    fn detaching_observer_during_notification_does_not_panic() {
1864        use std::cell::RefCell;
1865        // Observer A drops observer B's handle when fired; B's remover takes
1866        // borrow_mut on the same inner. Must not panic now that no borrow is
1867        // held during the callback loop.
1868        let s = Signal::new(0_i32);
1869        let b_slot: Rc<RefCell<Option<ObserverHandle>>> = Rc::new(RefCell::new(None));
1870        let b_slot_for_a = b_slot.clone();
1871        let _a = s.observe(move |_| {
1872            b_slot_for_a.borrow_mut().take();
1873        });
1874        let b = s.observe(|_| {});
1875        *b_slot.borrow_mut() = Some(b);
1876        s.set(1);
1877    }
1878
1879    #[test]
1880    fn registering_observer_during_notification_does_not_panic() {
1881        use std::cell::RefCell;
1882        // Registering a new observer mid-callback takes borrow_mut on the
1883        // same inner (via try_observe). Must not panic.
1884        let s = Signal::new(0_i32);
1885        let s2 = s.clone();
1886        let extra: Rc<RefCell<Option<ObserverHandle>>> = Rc::new(RefCell::new(None));
1887        let extra2 = extra.clone();
1888        let _h = s.observe(move |_| {
1889            if extra2.borrow().is_none() {
1890                *extra2.borrow_mut() = Some(s2.observe(|_| {}));
1891            }
1892        });
1893        s.set(1);
1894    }
1895}