use std::cell::{Cell, Ref, RefCell};
use std::rc::{Rc, Weak};
use crate::binding::{Binding, BindingLevel, BindingRegistry};
use crate::widget_id::WidgetId;
#[cfg(debug_assertions)]
const SIGNAL_NOTIFY_DEPTH_LIMIT: u32 = 256;
#[cfg(debug_assertions)]
thread_local! {
static SIGNAL_NOTIFY_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
#[cfg(debug_assertions)]
struct NotifyDepthGuard;
#[cfg(debug_assertions)]
impl NotifyDepthGuard {
fn enter() -> Self {
SIGNAL_NOTIFY_DEPTH.with(|d| {
let next = d.get() + 1;
assert!(
next <= SIGNAL_NOTIFY_DEPTH_LIMIT,
"Signal notification nested {next} deep (limit {SIGNAL_NOTIFY_DEPTH_LIMIT}) — \
almost certainly an unbounded feedback loop between observers (e.g. signal A's \
observer sets B and B's observer sets A). Break the cycle: guard the write with \
an equality check (`if sig.get() != v {{ sig.set(v) }}`), or drop one edge with \
a WeakSignal."
);
d.set(next);
});
NotifyDepthGuard
}
}
#[cfg(debug_assertions)]
impl Drop for NotifyDepthGuard {
fn drop(&mut self) {
SIGNAL_NOTIFY_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
pub struct ObserverHandle {
_signal: Rc<dyn std::any::Any>,
observer_id: u64,
remover: Rc<dyn Fn(u64)>,
}
impl ObserverHandle {
pub fn new(keeper: Rc<dyn std::any::Any>, observer_id: u64, remover: Rc<dyn Fn(u64)>) -> Self {
Self {
_signal: keeper,
observer_id,
remover,
}
}
pub fn detach(self) {
drop(self);
}
}
impl Drop for ObserverHandle {
fn drop(&mut self) {
(self.remover)(self.observer_id);
}
}
impl std::fmt::Debug for ObserverHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ObserverHandle")
.field("observer_id", &self.observer_id)
.finish()
}
}
struct ObserverEntry<T> {
id: u64,
callback: Rc<dyn Fn(&T)>,
}
struct MutableInner<T> {
value: T,
generation: u64,
observers: Vec<ObserverEntry<T>>,
next_observer_id: u64,
keepalive: Vec<Box<dyn std::any::Any>>,
}
struct AnimationState {
pending: Option<crate::animation::AnimationRequest>,
target: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SignalAccessError {
#[error("signal is read-only")]
ReadOnly,
#[error("signal does not support animation")]
AnimationUnsupported,
}
pub struct WeakSignal<T> {
inner: Weak<RefCell<MutableInner<T>>>,
animation: Option<Weak<RefCell<AnimationState>>>,
}
impl<T> Clone for WeakSignal<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
animation: self.animation.clone(),
}
}
}
impl<T: 'static> WeakSignal<T> {
pub fn upgrade(&self) -> Option<Signal<T>> {
let inner = self.inner.upgrade()?;
let animation = self.animation.as_ref().and_then(|weak| weak.upgrade());
Some(Signal {
kind: SignalKind::Mutable { inner, animation },
})
}
}
pub(crate) struct WeakAnimatedSignal {
inner: Weak<RefCell<MutableInner<f32>>>,
animation: Weak<RefCell<AnimationState>>,
}
impl WeakAnimatedSignal {
pub(crate) fn upgrade(&self) -> Option<Signal<f32>> {
Some(Signal {
kind: SignalKind::Mutable {
inner: self.inner.upgrade()?,
animation: Some(self.animation.upgrade()?),
},
})
}
pub(crate) fn same_signal(&self, signal: &Signal<f32>) -> bool {
match &signal.kind {
SignalKind::Mutable { inner, .. } => self.inner.as_ptr() == Rc::as_ptr(inner),
SignalKind::Derived { .. } => false,
}
}
}
#[derive(Clone)]
struct DerivedSource {
generation: Rc<dyn Fn() -> u64>,
source_id: usize,
}
fn coalesced_source(inputs: Rc<dyn Fn() -> Vec<u64>>) -> DerivedSource {
let token: Rc<()> = Rc::new(());
let source_id = Rc::as_ptr(&token) as usize;
let state: Rc<(Cell<u64>, RefCell<Option<Vec<u64>>>)> =
Rc::new((Cell::new(0), RefCell::new(None)));
DerivedSource {
generation: Rc::new(move || {
let _keep = &token;
let now = inputs();
let (own, seen) = &*state;
let mut seen = seen.borrow_mut();
if seen.as_deref() != Some(now.as_slice()) {
*seen = Some(now);
own.set(own.get().wrapping_add(1));
}
own.get()
}),
source_id,
}
}
enum SignalKind<T> {
Mutable {
inner: Rc<RefCell<MutableInner<T>>>,
animation: Option<Rc<RefCell<AnimationState>>>,
},
Derived {
compute: Rc<dyn Fn() -> T>,
sources: Vec<DerivedSource>,
},
}
pub struct Signal<T> {
kind: SignalKind<T>,
}
impl<T: 'static> Signal<T> {
pub fn new(value: T) -> Self {
Self {
kind: SignalKind::Mutable {
inner: Rc::new(RefCell::new(MutableInner {
value,
generation: 0,
observers: Vec::new(),
next_observer_id: 1,
keepalive: Vec::new(),
})),
animation: None,
},
}
}
pub fn attach_keepalive<G: 'static>(&self, guard: G) {
if let SignalKind::Mutable { inner, .. } = &self.kind {
inner.borrow_mut().keepalive.push(Box::new(guard));
}
}
pub fn downgrade(&self) -> Option<WeakSignal<T>> {
match &self.kind {
SignalKind::Mutable { inner, animation } => Some(WeakSignal {
inner: Rc::downgrade(inner),
animation: animation.as_ref().map(Rc::downgrade),
}),
SignalKind::Derived { .. } => None,
}
}
pub fn observe(&self, f: impl Fn(&T) + 'static) -> ObserverHandle {
self.try_observe(f)
.expect("observe() is only supported on mutable signals")
}
pub fn try_observe(
&self,
f: impl Fn(&T) + 'static,
) -> Result<ObserverHandle, SignalAccessError> {
match &self.kind {
SignalKind::Mutable { inner, .. } => {
let mut guard = inner.borrow_mut();
let id = guard.next_observer_id;
guard.next_observer_id += 1;
guard.observers.push(ObserverEntry {
id,
callback: Rc::new(f),
});
Ok(ObserverHandle {
_signal: inner.clone(),
observer_id: id,
remover: {
let inner = inner.clone();
Rc::new(move |observer_id| {
inner.borrow_mut().observers.retain(|e| e.id != observer_id);
})
},
})
}
SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
}
}
pub fn observer_count(&self) -> usize {
match &self.kind {
SignalKind::Mutable { inner, .. } => inner.borrow().observers.len(),
SignalKind::Derived { .. } => 0,
}
}
pub fn same(a: &Self, b: &Self) -> bool {
match (&a.kind, &b.kind) {
(SignalKind::Mutable { inner: a, .. }, SignalKind::Mutable { inner: b, .. }) => {
Rc::ptr_eq(a, b)
}
_ => false,
}
}
}
impl<T: Clone + 'static> Signal<T> {
pub fn set(&self, value: T) {
self.try_set(value)
.expect("cannot set() on a derived Signal — it is read-only");
}
pub fn set_if_changed(&self, value: T) -> bool
where
T: PartialEq,
{
if self.get() == value {
return false;
}
self.set(value);
true
}
pub fn try_set(&self, value: T) -> Result<(), SignalAccessError> {
match &self.kind {
SignalKind::Mutable { inner, .. } => {
let (snapshot, callbacks) = {
let mut guard = inner.borrow_mut();
guard.value = value;
guard.generation = guard.generation.wrapping_add(1);
let callbacks: Vec<_> =
guard.observers.iter().map(|e| e.callback.clone()).collect();
(guard.value.clone(), callbacks)
};
#[cfg(debug_assertions)]
let _depth = NotifyDepthGuard::enter();
for cb in &callbacks {
cb(&snapshot);
}
Ok(())
}
SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
}
}
pub fn get(&self) -> T {
match &self.kind {
SignalKind::Mutable { inner, .. } => inner.borrow().value.clone(),
SignalKind::Derived { compute, .. } => compute(),
}
}
pub fn get_ref(&self) -> Ref<'_, T> {
self.try_get_ref()
.expect("get_ref() is only supported on mutable signals")
}
pub fn try_get_ref(&self) -> Result<Ref<'_, T>, SignalAccessError> {
match &self.kind {
SignalKind::Mutable { inner, .. } => Ok(Ref::map(inner.borrow(), |guard| &guard.value)),
SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
}
}
pub fn map<U: Clone + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Signal<U> {
let compute = self.as_compute();
let sources = self.as_sources();
Signal {
kind: SignalKind::Derived {
compute: Rc::new(move || f(&compute())),
sources,
},
}
}
pub fn zip<U: Clone + 'static>(&self, other: &Signal<U>) -> Signal<(T, U)> {
let a = self.as_compute();
let b = other.as_compute();
let mut sources = self.as_sources();
merge_sources(&mut sources, other.as_sources());
Signal {
kind: SignalKind::Derived {
compute: Rc::new(move || (a(), b())),
sources,
},
}
}
pub fn zip3<U: Clone + 'static, V: Clone + 'static>(
&self,
b: &Signal<U>,
c: &Signal<V>,
) -> Signal<(T, U, V)> {
let fa = self.as_compute();
let fb = b.as_compute();
let fc = c.as_compute();
let mut sources = self.as_sources();
merge_sources(&mut sources, b.as_sources());
merge_sources(&mut sources, c.as_sources());
Signal {
kind: SignalKind::Derived {
compute: Rc::new(move || (fa(), fb(), fc())),
sources,
},
}
}
pub fn map_coalesced<U: Clone + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Signal<U> {
let compute = self.as_compute();
let underlying = self.as_sources();
if underlying.len() <= 1 {
return self.map(f);
}
let coalesced = coalesced_source(Rc::new(move || {
underlying.iter().map(|s| (s.generation)()).collect()
}));
Signal {
kind: SignalKind::Derived {
compute: Rc::new(move || f(&compute())),
sources: vec![coalesced],
},
}
}
pub fn flat_map<U: Clone + 'static>(&self, f: impl Fn(&T) -> Signal<U> + 'static) -> Signal<U> {
let f: Rc<dyn Fn(&T) -> Signal<U>> = Rc::new(f);
let outer_compute = self.as_compute();
let outer_sources = self.as_sources();
let compute: Rc<dyn Fn() -> U> = {
let f = f.clone();
let outer_compute = outer_compute.clone();
Rc::new(move || f(&outer_compute()).get())
};
let composite = coalesced_source({
let f = f.clone();
let outer_compute = outer_compute.clone();
Rc::new(move || {
let mut gens: Vec<u64> = outer_sources.iter().map(|s| (s.generation)()).collect();
gens.extend(
f(&outer_compute())
.as_sources()
.iter()
.map(|s| (s.generation)()),
);
gens
})
});
Signal {
kind: SignalKind::Derived {
compute,
sources: vec![composite],
},
}
}
fn as_compute(&self) -> Rc<dyn Fn() -> T> {
match &self.kind {
SignalKind::Mutable { inner, .. } => {
let source = inner.clone();
Rc::new(move || source.borrow().value.clone())
}
SignalKind::Derived { compute, .. } => compute.clone(),
}
}
pub fn bind_to(&self, widget_id: WidgetId, registry: &BindingRegistry, level: BindingLevel) {
for src in self.as_sources() {
registry.register(Binding {
widget_id,
level,
generation: src.generation,
source_id: src.source_id,
});
}
}
fn as_sources(&self) -> Vec<DerivedSource> {
match &self.kind {
SignalKind::Mutable { inner, .. } => {
let gen_src = inner.clone();
let source_id = Rc::as_ptr(inner) as *const () as usize;
vec![DerivedSource {
generation: Rc::new(move || gen_src.borrow().generation),
source_id,
}]
}
SignalKind::Derived { sources, .. } => sources.clone(),
}
}
}
fn merge_sources(dst: &mut Vec<DerivedSource>, incoming: Vec<DerivedSource>) {
for s in incoming {
if !dst.iter().any(|d| d.source_id == s.source_id) {
dst.push(s);
}
}
}
impl<T: 'static> Signal<T> {
pub fn is_mutable(&self) -> bool {
matches!(self.kind, SignalKind::Mutable { .. })
}
pub fn generation(&self) -> u64 {
match &self.kind {
SignalKind::Mutable { inner, .. } => inner.borrow().generation,
SignalKind::Derived { sources, .. } => sources
.iter()
.map(|s| (s.generation)())
.fold(0u64, u64::wrapping_add),
}
}
}
impl Signal<bool> {
pub fn and(&self, other: &Signal<bool>) -> Signal<bool> {
self.zip(other).map(|(a, b)| *a && *b)
}
pub fn or(&self, other: &Signal<bool>) -> Signal<bool> {
self.zip(other).map(|(a, b)| *a || *b)
}
pub fn not(&self) -> Signal<bool> {
self.map(|b| !*b)
}
}
impl Signal<f32> {
pub fn new_animated(value: f32) -> Self {
Self {
kind: SignalKind::Mutable {
inner: Rc::new(RefCell::new(MutableInner {
value,
generation: 0,
observers: Vec::new(),
next_observer_id: 1,
keepalive: Vec::new(),
})),
animation: Some(Rc::new(RefCell::new(AnimationState {
pending: None,
target: None,
}))),
},
}
}
pub fn supports_animation(&self) -> bool {
matches!(
&self.kind,
SignalKind::Mutable {
animation: Some(_),
..
}
)
}
pub(crate) fn weak_handle(&self) -> Option<WeakAnimatedSignal> {
match &self.kind {
SignalKind::Mutable {
inner,
animation: Some(animation),
} => Some(WeakAnimatedSignal {
inner: Rc::downgrade(inner),
animation: Rc::downgrade(animation),
}),
_ => None,
}
}
pub fn animate_to(
&self,
target: f32,
duration: std::time::Duration,
easing: teksilo_tokens::Easing,
) {
self.animate_to_with_frame_interval(target, duration, easing, None);
}
pub fn animate_to_with_frame_interval(
&self,
target: f32,
duration: std::time::Duration,
easing: teksilo_tokens::Easing,
frame_interval: Option<std::time::Duration>,
) {
self.try_animate_to_with_frame_interval(target, duration, easing, frame_interval)
.unwrap_or_else(|err| match err {
SignalAccessError::ReadOnly => {
panic!("animate_to is not supported on derived signals")
}
SignalAccessError::AnimationUnsupported => {
panic!(
"animate_to called on Signal<f32> without animation support; use Signal::new_animated()"
)
}
});
}
pub fn try_animate_to(
&self,
target: f32,
duration: std::time::Duration,
easing: teksilo_tokens::Easing,
) -> Result<(), SignalAccessError> {
self.try_animate_to_with_frame_interval(target, duration, easing, None)
}
pub fn try_animate_to_with_frame_interval(
&self,
target: f32,
duration: std::time::Duration,
easing: teksilo_tokens::Easing,
frame_interval: Option<std::time::Duration>,
) -> Result<(), SignalAccessError> {
self.try_animate_with_options(crate::animation::AnimationRequest {
target,
duration,
easing,
frame_interval,
looping: false,
epsilon: 0.0,
max_duration: None,
})
}
pub fn animate_looping(
&self,
target: f32,
period: std::time::Duration,
easing: teksilo_tokens::Easing,
frame_interval: Option<std::time::Duration>,
) {
let _ = self.try_animate_with_options(crate::animation::AnimationRequest {
target,
duration: period,
easing,
frame_interval,
looping: true,
epsilon: 0.0,
max_duration: None,
});
}
pub fn try_animate_with_options(
&self,
request: crate::animation::AnimationRequest,
) -> Result<(), SignalAccessError> {
match &self.kind {
SignalKind::Mutable {
inner,
animation: Some(animation),
} => {
let mut anim = animation.borrow_mut();
anim.target = Some(request.target);
anim.pending = Some(request);
drop(anim);
let mut guard = inner.borrow_mut();
guard.generation = guard.generation.wrapping_add(1);
Ok(())
}
SignalKind::Mutable {
animation: None, ..
} => Err(SignalAccessError::AnimationUnsupported),
SignalKind::Derived { .. } => Err(SignalAccessError::ReadOnly),
}
}
pub fn animation_target(&self) -> Option<f32> {
match &self.kind {
SignalKind::Mutable { animation, .. } => {
animation.as_ref().and_then(|a| a.borrow().target)
}
_ => None,
}
}
pub fn clear_animation_target(&self) {
if let SignalKind::Mutable { animation, .. } = &self.kind
&& let Some(a) = animation
{
a.borrow_mut().target = None;
}
}
pub fn take_pending_animation(&self) -> Option<crate::animation::AnimationRequest> {
match &self.kind {
SignalKind::Mutable { animation, .. } => animation
.as_ref()
.and_then(|a| a.borrow_mut().pending.take()),
_ => None,
}
}
pub fn has_pending_animation(&self) -> bool {
match &self.kind {
SignalKind::Mutable { animation, .. } => animation
.as_ref()
.is_some_and(|a| a.borrow().pending.is_some()),
_ => false,
}
}
}
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
kind: match &self.kind {
SignalKind::Mutable { inner, animation } => SignalKind::Mutable {
inner: inner.clone(),
animation: animation.clone(),
},
SignalKind::Derived { compute, sources } => SignalKind::Derived {
compute: compute.clone(),
sources: sources.clone(),
},
},
}
}
}
impl<T: std::fmt::Debug + 'static> std::fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
SignalKind::Mutable { inner, .. } => f
.debug_struct("Signal::Mutable")
.field("value", &inner.borrow().value)
.field("generation", &inner.borrow().generation)
.finish(),
SignalKind::Derived { .. } => f.write_str("Signal::Derived(..)"),
}
}
}
pub enum Prop<T: Clone + 'static> {
Static(T),
Bound(Signal<T>),
}
impl<T: Clone + 'static> Prop<T> {
pub fn get(&self) -> T {
match self {
Prop::Static(v) => v.clone(),
Prop::Bound(signal) => signal.get(),
}
}
pub fn register_if_bound(
&self,
widget_id: WidgetId,
registry: &BindingRegistry,
level: BindingLevel,
) {
if let Prop::Bound(signal) = self {
signal.bind_to(widget_id, registry, level);
}
}
pub fn as_signal(&self) -> Signal<T> {
match self {
Prop::Static(v) => Signal::new(v.clone()),
Prop::Bound(signal) => signal.clone(),
}
}
}
impl<T: Clone + 'static> Clone for Prop<T> {
fn clone(&self) -> Self {
match self {
Prop::Static(v) => Prop::Static(v.clone()),
Prop::Bound(s) => Prop::Bound(s.clone()),
}
}
}
impl<T: Clone + std::fmt::Debug + 'static> std::fmt::Debug for Prop<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Prop::Static(v) => write!(f, "Prop::Static({:?})", v),
Prop::Bound(_) => f.write_str("Prop::Bound(..)"),
}
}
}
impl<T: Clone + 'static> From<T> for Prop<T> {
fn from(value: T) -> Self {
Prop::Static(value)
}
}
impl<T: Clone + 'static> From<Signal<T>> for Prop<T> {
fn from(signal: Signal<T>) -> Self {
Prop::Bound(signal)
}
}
impl From<&str> for Prop<String> {
fn from(s: &str) -> Self {
Prop::Static(s.to_owned())
}
}
impl From<&String> for Prop<String> {
fn from(s: &String) -> Self {
Prop::Static(s.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_get_set() {
let s = Signal::new(42);
assert_eq!(s.get(), 42);
s.set(99);
assert_eq!(s.get(), 99);
}
#[test]
fn generation_advances_on_every_write_and_never_resets() {
let s = Signal::new(0);
let start = s.generation();
assert_eq!(s.generation(), start, "reading is not a change");
s.set(1);
let after_one = s.generation();
assert_ne!(after_one, start, "a write advances the generation");
s.set(1);
let after_republish = s.generation();
assert_ne!(
after_republish, after_one,
"`set` is unconditional — a republish of the same value is still \
a write, and callers who want the equality guard use \
`set_if_changed`"
);
assert!(!s.set_if_changed(1), "value is unchanged");
assert_eq!(
s.generation(),
after_republish,
"`set_if_changed` with an identical value writes nothing at all"
);
}
#[test]
fn observing_the_generation_does_not_consume_it() {
let s = Signal::new(0);
let (mut seen_a, mut seen_b) = (s.generation(), s.generation());
s.set(1);
assert_ne!(s.generation(), seen_a);
seen_a = s.generation();
assert_ne!(
s.generation(),
seen_b,
"consumer A catching up must leave consumer B behind, not clean"
);
seen_b = s.generation();
assert_eq!(s.generation(), seen_a);
assert_eq!(s.generation(), seen_b);
}
#[test]
fn signal_clone_shares() {
let a = Signal::new(10);
let b = a.clone();
a.set(20);
assert_eq!(b.get(), 20);
assert!(Signal::same(&a, &b));
}
#[test]
fn signal_map_derived() {
let text = Signal::new(String::from("hello"));
let len = text.map(|t| t.len());
assert_eq!(len.get(), 5);
text.set(String::from("hi"));
assert_eq!(len.get(), 2);
}
#[test]
fn signal_map_chained() {
let s = Signal::new(5);
let doubled = s.map(|v| v * 2);
let as_string = doubled.map(|v| format!("{}", v));
assert_eq!(as_string.get(), "10");
s.set(7);
assert_eq!(as_string.get(), "14");
}
#[test]
fn signal_derived_generation_tracks_source() {
let s = Signal::new(0);
let derived = s.map(|v| v + 1);
let seen = derived.generation();
s.set(5);
assert_ne!(
derived.generation(),
seen,
"the source's write shows through"
);
let seen = derived.generation();
assert_eq!(
derived.generation(),
seen,
"and settles with no further write"
);
}
#[test]
fn flat_map_follows_selected_inner_value() {
let a = Signal::new(10);
let b = Signal::new(20);
let which = Signal::new(0usize);
let (a2, b2) = (a.clone(), b.clone());
let out = which.flat_map(move |i| if *i == 0 { a2.clone() } else { b2.clone() });
assert_eq!(out.get(), 10); a.set(11);
assert_eq!(out.get(), 11); which.set(1);
assert_eq!(out.get(), 20); b.set(21);
assert_eq!(out.get(), 21);
a.set(999); assert_eq!(out.get(), 21);
}
#[test]
fn flat_map_generation_tracks_outer_and_current_inner() {
let a = Signal::new(0);
let b = Signal::new(0);
let which = Signal::new(0usize);
let (a2, b2) = (a.clone(), b.clone());
let out = which.flat_map(move |i| if *i == 0 { a2.clone() } else { b2.clone() });
let mut seen = out.generation();
a.set(5);
assert_ne!(out.generation(), seen);
seen = out.generation();
assert_eq!(out.generation(), seen);
b.set(7);
assert_eq!(out.generation(), seen, "b is not selected");
which.set(1);
assert_ne!(out.generation(), seen);
seen = out.generation();
b.set(8);
assert_ne!(out.generation(), seen);
seen = out.generation();
a.set(9);
assert_eq!(out.generation(), seen, "a is no longer selected");
}
#[test]
fn flat_map_survives_an_inner_switch_that_would_cancel_out_in_a_sum() {
let hot = Signal::new(0_i32);
let cold = Signal::new(0_i32);
hot.set(1);
let (hot2, cold2) = (hot.clone(), cold.clone());
let which = Signal::new(0usize);
let out = which.flat_map(move |i| if *i == 0 { hot2.clone() } else { cold2.clone() });
assert_eq!(out.get(), 1, "starts on `hot`");
let seen = out.generation();
which.set(1);
assert_eq!(out.get(), 0, "the value really did change");
assert_ne!(
out.generation(),
seen,
"and the generation says so — a plain sum of (outer + inner) \
would have been unchanged here"
);
}
#[test]
fn a_composite_sources_generation_is_readable_by_every_consumer() {
let inner = Signal::new(0_i32);
let inner2 = inner.clone();
let which = Signal::new(0usize);
let out = which.flat_map(move |_| inner2.clone());
let (window_a, window_b) = (out.generation(), out.generation());
inner.set(1);
let a_now = out.generation();
assert_ne!(a_now, window_a, "window A notices");
assert_ne!(
out.generation(),
window_b,
"and window B still notices, after A already looked"
);
assert_eq!(out.generation(), a_now, "both see the SAME new generation");
}
#[test]
fn flat_map_binding_rerenders_on_inner_and_outer_change() {
use crate::binding::{BindingLevel, BindingRegistry};
use slotmap::KeyData;
let fake_id: WidgetId = KeyData::from_ffi(1).into();
let inner = Signal::new(false);
let which = Signal::new(0usize);
let inner2 = inner.clone();
let gate = which.flat_map(move |_| inner2.clone());
let registry = BindingRegistry::new();
gate.bind_to(fake_id, ®istry, BindingLevel::Relayout);
assert!(registry.flush_dirty().is_empty());
inner.set(true);
let dirty = registry.flush_dirty();
assert_eq!(dirty.len(), 1, "selected-inner change must re-render");
assert_eq!(dirty[0].0, fake_id);
which.set(0);
let dirty = registry.flush_dirty();
assert_eq!(dirty.len(), 1, "outer-selector change must re-render");
}
#[test]
fn observer_called_on_set() {
use std::cell::Cell;
let s = Signal::new(0);
let called = Rc::new(Cell::new(false));
let c = called.clone();
let _handle = s.observe(move |val| {
assert_eq!(*val, 42);
c.set(true);
});
s.set(42);
assert!(called.get());
}
#[test]
fn set_if_changed_does_not_notify_when_the_value_is_identical() {
use std::cell::Cell;
let s = Signal::new(7);
let calls = Rc::new(Cell::new(0));
let c = calls.clone();
let _handle = s.observe(move |_| c.set(c.get() + 1));
assert!(
!s.set_if_changed(7),
"an identical write must report no change"
);
assert_eq!(calls.get(), 0, "an identical write must not walk observers");
assert!(
s.set_if_changed(8),
"a differing write must report a change"
);
assert_eq!(calls.get(), 1, "a differing write must notify");
assert_eq!(s.get(), 8);
}
#[test]
fn set_if_changed_settles_a_two_signal_feedback_loop() {
let a = Signal::new(0);
let b = Signal::new(0);
let _ha = {
let b = b.clone();
a.observe(move |v| {
b.set_if_changed(*v);
})
};
let _hb = {
let a = a.clone();
b.observe(move |v| {
a.set_if_changed(*v);
})
};
a.set(5);
assert_eq!(b.get(), 5);
assert_eq!(a.get(), 5);
}
#[test]
fn observer_removed_on_handle_drop() {
use std::cell::Cell;
let s = Signal::new(0);
let count = Rc::new(Cell::new(0));
let c = count.clone();
let handle = s.observe(move |_| {
c.set(c.get() + 1);
});
s.set(1);
assert_eq!(count.get(), 1);
drop(handle);
s.set(2);
assert_eq!(count.get(), 1); }
#[test]
fn multiple_observers() {
use std::cell::Cell;
let s = Signal::new(0);
let count = Rc::new(Cell::new(0));
let c1 = count.clone();
let c2 = count.clone();
let _h1 = s.observe(move |_| c1.set(c1.get() + 1));
let _h2 = s.observe(move |_| c2.set(c2.get() + 1));
s.set(10);
assert_eq!(count.get(), 2);
}
#[test]
fn binding_registry_integration() {
use slotmap::KeyData;
let fake_id: WidgetId = KeyData::from_ffi(1).into();
let registry = BindingRegistry::new();
let s = Signal::new(0);
s.bind_to(fake_id, ®istry, BindingLevel::RepaintOnly);
assert!(registry.flush_dirty().is_empty());
s.set(42);
let dirty = registry.flush_dirty();
assert_eq!(dirty.len(), 1);
assert_eq!(dirty[0].0, fake_id);
assert_eq!(dirty[0].1, BindingLevel::RepaintOnly);
assert!(registry.flush_dirty().is_empty());
}
#[test]
fn derived_binding_registry() {
use slotmap::KeyData;
let fake_id: WidgetId = KeyData::from_ffi(1).into();
let registry = BindingRegistry::new();
let s = Signal::new(0);
let doubled = s.map(|v| v * 2);
doubled.bind_to(fake_id, ®istry, BindingLevel::Relayout);
assert!(registry.flush_dirty().is_empty());
s.set(5);
let dirty = registry.flush_dirty();
assert_eq!(dirty.len(), 1);
assert_eq!(dirty[0].1, BindingLevel::Relayout);
}
#[test]
fn get_ref_works() {
let s = Signal::new(String::from("hello"));
{
let r = s.get_ref();
assert_eq!(&*r, "hello");
}
}
#[test]
#[should_panic(expected = "cannot set() on a derived Signal")]
fn set_on_derived_panics() {
let s = Signal::new(0);
let d = s.map(|v| v + 1);
d.set(99);
}
#[test]
fn prop_static() {
let p: Prop<i32> = 42.into();
assert_eq!(p.get(), 42);
}
#[test]
fn prop_bound() {
let s = Signal::new(10);
let p: Prop<i32> = s.clone().into();
assert_eq!(p.get(), 10);
s.set(20);
assert_eq!(p.get(), 20);
}
#[test]
fn prop_register_if_bound() {
use slotmap::KeyData;
let fake_id: WidgetId = KeyData::from_ffi(1).into();
let registry = BindingRegistry::new();
let s = Signal::new(0);
let p: Prop<i32> = s.clone().into();
p.register_if_bound(fake_id, ®istry, BindingLevel::RepaintOnly);
s.set(1);
let dirty = registry.flush_dirty();
assert_eq!(dirty.len(), 1);
let p2: Prop<i32> = 42.into();
p2.register_if_bound(fake_id, ®istry, BindingLevel::RepaintOnly);
assert!(registry.flush_dirty().is_empty());
}
#[test]
fn zip_reads_both_sources() {
let a = Signal::new(1_i32);
let b = Signal::new("x".to_string());
let z = a.zip(&b);
assert_eq!(z.get(), (1, "x".to_string()));
a.set(7);
b.set("y".to_string());
assert_eq!(z.get(), (7, "y".to_string()));
}
#[test]
fn zip_generation_advances_when_either_source_is_written() {
let a = Signal::new(0_i32);
let b = Signal::new(0_i32);
let z = a.zip(&b);
let mut seen = z.generation();
a.set(1);
assert_ne!(z.generation(), seen, "a write to the first source shows");
seen = z.generation();
b.set(2);
assert_ne!(z.generation(), seen, "a write to the second source shows");
seen = z.generation();
assert_eq!(z.generation(), seen, "and settles with no further write");
}
#[test]
fn zip_generation_reflects_writes_to_both_sources_independently() {
let a = Signal::new(0_i32);
let b = Signal::new(0_i32);
let z = a.zip(&b);
let start = z.generation();
a.set(1);
let after_a = z.generation();
b.set(1);
let after_b = z.generation();
assert!(
after_a > start && after_b > after_a,
"monotone in both sources: {start} < {after_a} < {after_b}"
);
}
#[test]
fn zip3_reads_three_sources() {
let a = Signal::new(1_i32);
let b = Signal::new(2_i32);
let c = Signal::new(3_i32);
let z = a.zip3(&b, &c);
assert_eq!(z.get(), (1, 2, 3));
c.set(30);
assert_eq!(z.get(), (1, 2, 30));
}
#[test]
fn zip3_generation_advances_for_any_source() {
let a = Signal::new(0_i32);
let b = Signal::new(0_i32);
let c = Signal::new(0_i32);
let z = a.zip3(&b, &c);
let mut seen = z.generation();
for write in [&c, &b, &a] {
write.set(1);
assert_ne!(z.generation(), seen);
seen = z.generation();
}
}
#[test]
fn and_reads_logical_and() {
let a = Signal::new(true);
let b = Signal::new(false);
let anded = a.and(&b);
assert!(!anded.get());
b.set(true);
assert!(anded.get());
a.set(false);
assert!(!anded.get());
}
#[test]
fn or_reads_logical_or() {
let a = Signal::new(false);
let b = Signal::new(false);
let ored = a.or(&b);
assert!(!ored.get());
a.set(true);
assert!(ored.get());
a.set(false);
b.set(true);
assert!(ored.get());
}
#[test]
fn not_reads_logical_negation() {
let a = Signal::new(true);
let n = a.not();
assert!(!n.get());
a.set(false);
assert!(n.get());
}
#[test]
fn combined_predicate_fires_binding_on_any_source() {
use crate::binding::BindingRegistry;
use slotmap::KeyData;
let reg = BindingRegistry::new();
let id: WidgetId = KeyData::from_ffi(1).into();
let focus = Signal::new(false);
let readonly = Signal::new(true);
let in_editor = Signal::new(true);
let when = focus.and(&readonly.not()).and(&in_editor);
when.bind_to(id, ®, BindingLevel::Relayout);
assert!(!when.get(), "all sources start producing false");
focus.set(true);
let dirty = reg.flush_dirty();
assert_eq!(dirty.len(), 1, "focus change must fire the binding");
assert_eq!(dirty[0].0, id);
readonly.set(false);
let dirty = reg.flush_dirty();
assert_eq!(dirty.len(), 1, "readonly change must fire the binding");
assert!(when.get());
in_editor.set(false);
let dirty = reg.flush_dirty();
assert_eq!(dirty.len(), 1, "in_editor change must fire the binding");
assert!(!when.get());
}
#[test]
fn zip_dedups_identical_source() {
use crate::binding::BindingRegistry;
use slotmap::KeyData;
let reg = BindingRegistry::new();
let id: WidgetId = KeyData::from_ffi(1).into();
let a = Signal::new(0_i32);
let derived = a.map(|v| v + 1);
let z = a.zip(&derived);
z.bind_to(id, ®, BindingLevel::RepaintOnly);
assert_eq!(
reg.len(),
1,
"duplicate upstream root must register once, not twice"
);
}
#[test]
fn signal_animated_f32() {
let s = Signal::<f32>::new_animated(0.0);
assert!(!s.has_pending_animation());
s.animate_to(
100.0,
std::time::Duration::from_millis(200),
teksilo_tokens::Easing::Linear,
);
assert!(s.has_pending_animation());
assert_eq!(s.animation_target(), Some(100.0));
let req = s.take_pending_animation().unwrap();
assert_eq!(req.target, 100.0);
assert!(!s.has_pending_animation());
}
#[test]
fn map_coalesced_collapses_multi_source_to_one_binding() {
let a = Signal::new(1u32);
let b = Signal::new(2u32);
let c = Signal::new(3u32);
let d = Signal::new(4u32);
let composite = a
.zip3(&b, &c)
.zip(&d)
.map_coalesced(|((x, y, z), w)| *x + *y + *z + *w);
assert_eq!(composite.as_sources().len(), 1);
assert_eq!(composite.get(), 10);
let mut seen = composite.generation();
a.set(10);
assert_ne!(composite.generation(), seen);
seen = composite.generation();
assert_eq!(composite.generation(), seen);
c.set(30);
assert_ne!(composite.generation(), seen);
}
#[test]
fn map_coalesced_generation_is_readable_by_every_consumer() {
let a = Signal::new(1u32);
let b = Signal::new(2u32);
let composite = a.zip(&b).map_coalesced(|(x, y)| *x + *y);
let (window_a, window_b) = (composite.generation(), composite.generation());
b.set(5);
let a_now = composite.generation();
assert_ne!(a_now, window_a);
assert_ne!(
composite.generation(),
window_b,
"the first read must not have cleared anything"
);
assert_eq!(composite.generation(), a_now);
}
#[test]
fn map_coalesced_with_single_source_delegates_to_map() {
let a = Signal::new(7u32);
let derived = a.map_coalesced(|v| *v * 2);
assert_eq!(derived.get(), 14);
assert_eq!(derived.as_sources().len(), 1);
}
#[test]
fn reentrant_set_in_observer_does_not_panic() {
let s = Signal::new(0_i32);
let s2 = s.clone();
let _handle = s.observe(move |v| {
if *v == 1 {
s2.set(2);
}
});
s.set(1);
assert_eq!(s.get(), 2);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "feedback loop")]
fn unbounded_feedback_loop_panics_with_diagnostic() {
let a = Signal::new(0_i32);
let b = Signal::new(0_i32);
let b_for_a = b.clone();
let _ha = a.observe(move |v| b_for_a.set(*v + 1));
let a_for_b = a.clone();
let _hb = b.observe(move |v| a_for_b.set(*v + 1));
a.set(1);
}
#[test]
fn detaching_observer_during_notification_does_not_panic() {
use std::cell::RefCell;
let s = Signal::new(0_i32);
let b_slot: Rc<RefCell<Option<ObserverHandle>>> = Rc::new(RefCell::new(None));
let b_slot_for_a = b_slot.clone();
let _a = s.observe(move |_| {
b_slot_for_a.borrow_mut().take();
});
let b = s.observe(|_| {});
*b_slot.borrow_mut() = Some(b);
s.set(1);
}
#[test]
fn registering_observer_during_notification_does_not_panic() {
use std::cell::RefCell;
let s = Signal::new(0_i32);
let s2 = s.clone();
let extra: Rc<RefCell<Option<ObserverHandle>>> = Rc::new(RefCell::new(None));
let extra2 = extra.clone();
let _h = s.observe(move |_| {
if extra2.borrow().is_none() {
*extra2.borrow_mut() = Some(s2.observe(|_| {}));
}
});
s.set(1);
}
}