use crate::{
Computed, Signal, cache::Cached, distinct::Distinct, map::Map, signal::WithMetadata, zip::Zip,
};
use alloc::string::String;
use num_traits::{Signed, Zero};
#[cfg(feature = "timer")]
use crate::debounce::Debounce;
#[cfg(feature = "timer")]
use core::time::Duration;
type BoolZipMap<A, B> = Map<Zip<A, B>, fn((bool, bool)) -> bool, bool>;
pub trait SignalExt: Signal {
#[track_caller]
fn map<F, Output>(&self, f: F) -> Map<Self, F, Output>
where
F: 'static + Clone + Fn(Self::Output) -> Output,
Output: 'static,
Self: 'static,
{
Map::new(self.clone(), f)
}
#[track_caller]
fn zip<B>(&self, b: &B) -> Zip<Self, B>
where
B: Signal,
Self::Output: Clone,
B::Output: Clone,
{
Zip::new(self.clone(), b.clone())
}
fn cached(&self) -> Cached<Self>
where
Self::Output: Clone,
{
Cached::new(self.clone())
}
#[track_caller]
fn computed(&self) -> Computed<Self::Output>
where
Self: 'static,
{
Computed::new(self.clone())
}
#[track_caller]
fn with<T: 'static>(&self, metadata: T) -> WithMetadata<Self, T> {
WithMetadata::new(metadata, self.clone())
}
#[track_caller]
fn map_into<U>(&self) -> Map<Self, fn(Self::Output) -> U, U>
where
Self: 'static,
Self::Output: Into<U>,
U: 'static,
{
self.map(Into::into)
}
#[track_caller]
fn inspect<F>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> Self::Output, Self::Output>
where
Self: 'static,
Self::Output: Clone + 'static,
F: 'static + Clone + Fn(&Self::Output),
{
Map::new(self.clone(), move |value| {
f(&value);
value
})
}
fn distinct(&self) -> Distinct<Self>
where
Self::Output: PartialEq + Clone,
{
Distinct::new(self.clone())
}
#[track_caller]
fn equal_to(
&self,
other: Self::Output,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
Self::Output: Clone + PartialEq + 'static,
{
Map::new(self.clone(), move |value| value == other)
}
#[track_caller]
fn condition<F>(
&self,
predicate: F,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
F: 'static + Clone + Fn(&Self::Output) -> bool,
{
Map::new(self.clone(), move |value| predicate(&value))
}
#[track_caller]
fn gt(
&self,
other: Self::Output,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
Self::Output: Clone + PartialOrd + 'static,
{
Map::new(self.clone(), move |value| value > other)
}
#[track_caller]
fn lt(
&self,
other: Self::Output,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
Self::Output: Clone + PartialOrd + 'static,
{
Map::new(self.clone(), move |value| value < other)
}
#[track_caller]
fn ge(
&self,
other: Self::Output,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
Self::Output: Clone + PartialOrd + 'static,
{
Map::new(self.clone(), move |value| value >= other)
}
#[track_caller]
fn le(
&self,
other: Self::Output,
) -> Map<Self, impl 'static + Clone + Fn(Self::Output) -> bool, bool>
where
Self: 'static,
Self::Output: Clone + PartialOrd + 'static,
{
Map::new(self.clone(), move |value| value <= other)
}
#[allow(clippy::wrong_self_convention)]
#[track_caller]
fn is_some<T>(&self) -> Map<Self, fn(Option<T>) -> bool, bool>
where
Self: Signal<Output = Option<T>> + 'static,
T: 'static,
{
self.map(|opt| opt.is_some())
}
#[allow(clippy::wrong_self_convention)]
#[track_caller]
fn is_none<T>(&self) -> Map<Self, fn(Option<T>) -> bool, bool>
where
Self: Signal<Output = Option<T>> + 'static,
T: 'static,
{
self.map(|opt| opt.is_none())
}
#[track_caller]
fn unwrap_or<T>(&self, default: T) -> Map<Self, impl 'static + Clone + Fn(Option<T>) -> T, T>
where
Self: Signal<Output = Option<T>> + 'static,
T: Clone + 'static,
{
Map::new(self.clone(), move |opt| {
opt.unwrap_or_else(|| default.clone())
})
}
#[track_caller]
fn unwrap_or_else<T, F>(
&self,
default: F,
) -> Map<Self, impl 'static + Clone + Fn(Option<T>) -> T, T>
where
Self: Signal<Output = Option<T>> + 'static,
T: 'static,
F: 'static + Clone + Fn() -> T,
{
Map::new(self.clone(), move |opt| opt.unwrap_or_else(&default))
}
#[track_caller]
fn unwrap_or_default<T>(&self) -> Map<Self, fn(Option<T>) -> T, T>
where
Self: Signal<Output = Option<T>> + 'static,
T: Default + 'static,
{
self.map(Option::unwrap_or_default)
}
#[track_caller]
fn some_equal_to<T>(
&self,
value: T,
) -> Map<Self, impl 'static + Clone + Fn(Option<T>) -> bool, bool>
where
Self: Signal<Output = Option<T>> + 'static,
T: Clone + PartialEq + 'static,
{
Map::new(self.clone(), move |opt| {
opt.as_ref().is_some_and(|v| v == &value)
})
}
#[allow(clippy::type_complexity)]
#[track_caller]
fn flatten<T>(&self) -> Map<Self, fn(Option<Option<T>>) -> Option<T>, Option<T>>
where
Self: Signal<Output = Option<Option<T>>> + 'static,
T: 'static,
{
self.map(Option::flatten)
}
#[track_caller]
fn map_some<T, U, F>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Option<T>) -> Option<U>, Option<U>>
where
Self: Signal<Output = Option<T>> + 'static,
T: 'static,
U: 'static,
F: 'static + Clone + Fn(T) -> U,
{
Map::new(self.clone(), move |opt| opt.map(&f))
}
#[track_caller]
fn and_then_some<T, U, F>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Option<T>) -> Option<U>, Option<U>>
where
Self: Signal<Output = Option<T>> + 'static,
T: 'static,
U: 'static,
F: 'static + Clone + Fn(T) -> Option<U>,
{
Map::new(self.clone(), move |opt| opt.and_then(&f))
}
#[track_caller]
fn not(&self) -> Map<Self, fn(bool) -> bool, bool>
where
Self: Signal<Output = bool> + 'static,
{
self.map(core::ops::Not::not)
}
#[track_caller]
fn and<B>(&self, other: &B) -> BoolZipMap<Self, B>
where
Self: Signal<Output = bool> + 'static,
B: Signal<Output = bool> + 'static,
{
Zip::new(self.clone(), other.clone()).map(|(a, b)| a && b)
}
#[track_caller]
fn or<B>(&self, other: &B) -> BoolZipMap<Self, B>
where
Self: Signal<Output = bool> + 'static,
B: Signal<Output = bool> + 'static,
{
Zip::new(self.clone(), other.clone()).map(|(a, b)| a || b)
}
#[track_caller]
fn then_some<T>(
&self,
value: T,
) -> Map<Self, impl 'static + Clone + Fn(bool) -> Option<T>, Option<T>>
where
Self: Signal<Output = bool> + 'static,
T: Clone + 'static,
{
Map::new(self.clone(), move |b| b.then_some(value.clone()))
}
#[track_caller]
fn select<T>(
&self,
if_true: T,
if_false: T,
) -> Map<Self, impl 'static + Clone + Fn(bool) -> T, T>
where
Self: Signal<Output = bool> + 'static,
T: Clone + 'static,
{
Map::new(self.clone(), move |b| {
if b { if_true.clone() } else { if_false.clone() }
})
}
#[track_caller]
fn negate<T>(&self) -> Map<Self, fn(T) -> T, T>
where
Self: Signal<Output = T> + 'static,
T: Signed + 'static,
{
self.map(core::ops::Neg::neg)
}
#[track_caller]
fn abs<T>(&self) -> Map<Self, fn(T) -> T, T>
where
Self: Signal<Output = T> + 'static,
T: Signed + 'static,
{
self.map(|v| v.abs())
}
#[track_caller]
fn sign<T>(&self) -> Map<Self, fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: Signed + 'static,
{
self.map(|v| !v.is_negative())
}
#[allow(clippy::wrong_self_convention)]
#[track_caller]
fn is_positive<T>(&self) -> Map<Self, fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: Signed + 'static,
{
self.map(|v| v.is_positive())
}
#[allow(clippy::wrong_self_convention)]
#[track_caller]
fn is_negative<T>(&self) -> Map<Self, fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: Signed + 'static,
{
self.map(|v| v.is_negative())
}
#[allow(clippy::wrong_self_convention)]
#[track_caller]
fn is_zero<T>(&self) -> Map<Self, fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: Zero + 'static,
{
self.map(|v| v.is_zero())
}
#[allow(clippy::wrong_self_convention, clippy::type_complexity)]
#[track_caller]
fn is_ok<T, E>(&self) -> Map<Self, fn(Result<T, E>) -> bool, bool>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
{
self.map(|r| r.is_ok())
}
#[allow(clippy::wrong_self_convention, clippy::type_complexity)]
#[track_caller]
fn is_err<T, E>(&self) -> Map<Self, fn(Result<T, E>) -> bool, bool>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
{
self.map(|r| r.is_err())
}
#[allow(clippy::type_complexity)]
#[track_caller]
fn ok<T, E>(&self) -> Map<Self, fn(Result<T, E>) -> Option<T>, Option<T>>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
{
self.map(Result::ok)
}
#[allow(clippy::type_complexity)]
#[track_caller]
fn err<T, E>(&self) -> Map<Self, fn(Result<T, E>) -> Option<E>, Option<E>>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
{
self.map(Result::err)
}
#[track_caller]
fn unwrap_or_result<T, E>(
&self,
default: T,
) -> Map<Self, impl 'static + Clone + Fn(Result<T, E>) -> T, T>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: Clone + 'static,
E: 'static,
{
Map::new(self.clone(), move |r| r.unwrap_or_else(|_| default.clone()))
}
#[track_caller]
fn unwrap_or_else_result<T, E, F>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Result<T, E>) -> T, T>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
F: 'static + Clone + Fn(E) -> T,
{
Map::new(self.clone(), move |r| r.unwrap_or_else(&f))
}
#[allow(clippy::type_complexity)]
#[track_caller]
fn map_ok<T, E, U, F>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Result<T, E>) -> Result<U, E>, Result<U, E>>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
U: 'static,
F: 'static + Clone + Fn(T) -> U,
{
Map::new(self.clone(), move |r| r.map(&f))
}
#[allow(clippy::type_complexity)]
#[track_caller]
fn map_err<T, E, F, U>(
&self,
f: F,
) -> Map<Self, impl 'static + Clone + Fn(Result<T, E>) -> Result<T, U>, Result<T, U>>
where
Self: Signal<Output = Result<T, E>> + 'static,
T: 'static,
E: 'static,
U: 'static,
F: 'static + Clone + Fn(E) -> U,
{
Map::new(self.clone(), move |r| r.map_err(&f))
}
#[cfg(feature = "timer")]
fn debounce(&self, duration: Duration) -> Debounce<Self, executor_core::DefaultExecutor>
where
Self::Output: Clone,
{
Debounce::new(self.clone(), duration)
}
#[cfg(feature = "timer")]
fn throttle(
&self,
duration: Duration,
) -> crate::throttle::Throttle<Self, executor_core::DefaultExecutor>
where
Self::Output: Clone,
{
crate::throttle::Throttle::new(self.clone(), duration)
}
#[track_caller]
fn str_is_empty<T>(&self) -> Map<Self, fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: AsRef<str> + 'static,
{
self.map(|s| s.as_ref().is_empty())
}
#[track_caller]
fn str_len<T>(&self) -> Map<Self, fn(T) -> usize, usize>
where
Self: Signal<Output = T> + 'static,
T: AsRef<str> + 'static,
{
self.map(|s| s.as_ref().len())
}
#[track_caller]
fn str_contains<T>(
&self,
pattern: impl Into<String>,
) -> Map<Self, impl 'static + Clone + Fn(T) -> bool, bool>
where
Self: Signal<Output = T> + 'static,
T: AsRef<str> + 'static,
{
let pattern = pattern.into();
Map::new(self.clone(), move |s| s.as_ref().contains(&pattern))
}
}
impl<C: Signal> SignalExt for C {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Binding, binding};
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn derived_signal_identity_is_stable_across_reconstruction() {
fn mapped_signal(signal: &Binding<i32>) -> Map<Binding<i32>, fn(i32) -> i64, i64> {
signal.map(i64::from)
}
fn metadata_signal(signal: &Binding<i32>) -> WithMetadata<Binding<i32>, &'static str> {
signal.with("animation")
}
fn zipped_signal(
signal: &Binding<i32>,
other: &Binding<i32>,
) -> Zip<Binding<i32>, Binding<i32>> {
signal.zip(other)
}
let signal: Binding<i32> = binding(42);
let metadata_a = metadata_signal(&signal);
let metadata_b = metadata_signal(&signal);
assert_eq!(metadata_a.identity(), metadata_b.identity());
assert_ne!(metadata_a.identity(), signal.identity());
let mapped_a = mapped_signal(&signal);
let mapped_b = mapped_signal(&signal);
assert_eq!(mapped_a.identity(), mapped_b.identity());
assert_ne!(mapped_a.identity(), signal.identity());
let different_map = signal.map(|value| i64::from(value) + 1);
assert_ne!(mapped_a.identity(), different_map.identity());
let other: Binding<i32> = binding(7);
let zipped_a = zipped_signal(&signal, &other);
let zipped_b = zipped_signal(&signal, &other);
assert_eq!(zipped_a.identity(), zipped_b.identity());
assert_ne!(zipped_a.identity(), signal.identity());
assert_ne!(zipped_a.identity(), other.identity());
}
#[test]
fn test_map_into() {
let signal: Binding<i32> = binding(42i32);
let mapped: Map<_, _, i64> = signal.map_into();
assert_eq!(mapped.get(), 42i64);
}
#[test]
fn test_distinct() {
let signal: Binding<i32> = binding(42);
let distinct = signal.distinct();
assert_eq!(distinct.get(), 42);
}
#[test]
fn test_distinct_watch_notifies_only_on_change() {
use core::cell::RefCell;
let signal: Binding<i32> = binding(0);
let distinct = signal.distinct();
let seen = Rc::new(RefCell::new(Vec::new()));
let seen_in_watch = seen.clone();
let _guard = distinct.watch(move |ctx| {
seen_in_watch.borrow_mut().push(*ctx.value());
});
signal.set(1);
signal.set(1);
signal.set(2);
assert_eq!(*seen.borrow(), vec![1, 2]);
}
#[test]
fn test_equal_to() {
let signal: Binding<i32> = binding(42);
let is_42 = signal.equal_to(42);
assert!(is_42.get());
signal.set(10);
assert!(!is_42.get());
}
#[test]
fn test_not_equal_to() {
let signal: Binding<i32> = binding(42);
let not_42 = signal.equal_to(42).not();
assert!(!not_42.get());
signal.set(10);
assert!(not_42.get());
}
#[test]
fn test_condition() {
let signal: Binding<i32> = binding(42);
let is_even = signal.condition(|x| x % 2 == 0);
assert!(is_even.get());
signal.set(43);
assert!(!is_even.get());
}
#[test]
fn test_gt() {
let signal: Binding<i32> = binding(42);
let is_gt_40 = signal.gt(40);
assert!(is_gt_40.get());
signal.set(40);
assert!(!is_gt_40.get());
signal.set(30);
assert!(!is_gt_40.get());
}
#[test]
fn test_lt() {
let signal: Binding<i32> = binding(30);
let is_lt_40 = signal.lt(40);
assert!(is_lt_40.get());
signal.set(40);
assert!(!is_lt_40.get());
signal.set(50);
assert!(!is_lt_40.get());
}
#[test]
fn test_ge() {
let signal: Binding<i32> = binding(42);
let is_ge_40 = signal.ge(40);
assert!(is_ge_40.get());
signal.set(40);
assert!(is_ge_40.get());
signal.set(30);
assert!(!is_ge_40.get());
}
#[test]
fn test_le() {
let signal: Binding<i32> = binding(30);
let is_le_40 = signal.le(40);
assert!(is_le_40.get());
signal.set(40);
assert!(is_le_40.get());
signal.set(50);
assert!(!is_le_40.get());
}
#[test]
fn test_is_some() {
let signal: Binding<Option<i32>> = binding(Some(42));
assert!(signal.is_some().get());
signal.set(None);
assert!(!signal.is_some().get());
}
#[test]
fn test_is_none() {
let signal: Binding<Option<i32>> = binding(None);
assert!(signal.is_none().get());
signal.set(Some(42));
assert!(!signal.is_none().get());
}
#[test]
fn test_unwrap_or() {
let signal: Binding<Option<i32>> = binding(Some(42));
let unwrapped = signal.unwrap_or(0);
assert_eq!(unwrapped.get(), 42);
signal.set(None);
assert_eq!(unwrapped.get(), 0);
}
#[test]
fn test_unwrap_or_else() {
let signal: Binding<Option<i32>> = binding(Some(42));
let unwrapped = signal.unwrap_or_else(|| 100);
assert_eq!(unwrapped.get(), 42);
signal.set(None);
assert_eq!(unwrapped.get(), 100);
}
#[test]
fn test_unwrap_or_default() {
let signal: Binding<Option<i32>> = binding(Some(42));
assert_eq!(signal.unwrap_or_default().get(), 42);
signal.set(None);
assert_eq!(signal.unwrap_or_default().get(), 0);
}
#[test]
fn test_some_equal_to() {
let signal: Binding<Option<i32>> = binding(Some(42));
let eq_42 = signal.some_equal_to(42);
let eq_0 = signal.some_equal_to(0);
assert!(eq_42.get());
assert!(!eq_0.get());
signal.set(None);
assert!(!eq_42.get());
}
#[test]
fn test_flatten() {
let signal: Binding<Option<Option<i32>>> = binding(Some(Some(42)));
assert_eq!(signal.flatten().get(), Some(42));
signal.set(Some(None));
assert_eq!(signal.flatten().get(), None);
signal.set(None);
assert_eq!(signal.flatten().get(), None);
}
#[test]
fn test_not() {
let signal: Binding<bool> = binding(true);
assert!(!signal.not().get());
signal.set(false);
assert!(signal.not().get());
}
#[test]
fn test_and() {
let a: Binding<bool> = binding(true);
let b: Binding<bool> = binding(true);
let result = a.and(&b);
assert!(result.get());
a.set(false);
assert!(!result.get());
a.set(true);
b.set(false);
assert!(!result.get());
a.set(false);
assert!(!result.get());
}
#[test]
fn test_or() {
let a: Binding<bool> = binding(false);
let b: Binding<bool> = binding(false);
let result = a.or(&b);
assert!(!result.get());
a.set(true);
assert!(result.get());
a.set(false);
b.set(true);
assert!(result.get());
a.set(true);
assert!(result.get());
}
#[test]
fn test_then_some() {
let signal: Binding<bool> = binding(true);
let maybe = signal.then_some(42);
assert_eq!(maybe.get(), Some(42));
signal.set(false);
assert_eq!(maybe.get(), None);
}
#[test]
fn test_select() {
let signal: Binding<bool> = binding(true);
let selected = signal.select("yes", "no");
assert_eq!(selected.get(), "yes");
signal.set(false);
assert_eq!(selected.get(), "no");
}
#[test]
fn test_negate() {
let signal: Binding<i32> = binding(42);
assert_eq!(signal.negate().get(), -42);
signal.set(-10);
assert_eq!(signal.negate().get(), 10);
}
#[test]
fn test_abs() {
let signal: Binding<i32> = binding(-42);
assert_eq!(signal.abs().get(), 42);
signal.set(10);
assert_eq!(signal.abs().get(), 10);
}
#[test]
fn test_sign() {
let signal: Binding<i32> = binding(42);
assert!(signal.sign().get());
signal.set(-10);
assert!(!signal.sign().get());
signal.set(0);
assert!(signal.sign().get()); }
#[test]
fn test_is_positive() {
let signal: Binding<i32> = binding(42);
assert!(signal.is_positive().get());
signal.set(-10);
assert!(!signal.is_positive().get());
signal.set(0);
assert!(!signal.is_positive().get());
}
#[test]
fn test_is_negative() {
let signal: Binding<i32> = binding(-42);
assert!(signal.is_negative().get());
signal.set(10);
assert!(!signal.is_negative().get());
signal.set(0);
assert!(!signal.is_negative().get());
}
#[test]
fn test_is_zero() {
let signal: Binding<i32> = binding(0);
assert!(signal.is_zero().get());
signal.set(42);
assert!(!signal.is_zero().get());
}
#[test]
fn test_is_ok() {
let signal: Binding<Result<i32, &str>> = binding(Ok(42));
assert!(signal.is_ok().get());
signal.set(Err("error"));
assert!(!signal.is_ok().get());
}
#[test]
fn test_is_err() {
let signal: Binding<Result<i32, &str>> = binding(Err("error"));
assert!(signal.is_err().get());
signal.set(Ok(42));
assert!(!signal.is_err().get());
}
#[test]
fn test_ok() {
let signal: Binding<Result<i32, &str>> = binding(Ok(42));
assert_eq!(signal.ok().get(), Some(42));
signal.set(Err("error"));
assert_eq!(signal.ok().get(), None);
}
#[test]
fn test_err() {
let signal: Binding<Result<i32, &str>> = binding(Err("error"));
assert_eq!(signal.err().get(), Some("error"));
signal.set(Ok(42));
assert_eq!(signal.err().get(), None);
}
#[test]
fn test_is_empty_string() {
let signal: Binding<String> = binding(String::new());
assert!(signal.str_is_empty().get());
signal.set("hello".to_string());
assert!(!signal.str_is_empty().get());
}
#[test]
fn test_is_empty_str() {
let signal: Binding<&str> = binding("");
assert!(signal.str_is_empty().get());
signal.set("hello");
assert!(!signal.str_is_empty().get());
}
#[test]
fn test_str_len() {
let signal: Binding<String> = binding("hello".to_string());
assert_eq!(signal.str_len().get(), 5);
signal.set(String::new());
assert_eq!(signal.str_len().get(), 0);
}
#[test]
fn test_contains() {
let signal: Binding<&str> = binding("hello world");
let has_world = signal.str_contains("world");
assert!(has_world.get());
signal.set("hello");
assert!(!has_world.get());
}
#[test]
fn test_contains_str() {
let signal: Binding<&str> = binding("hello world");
let has_world = signal.str_contains("world");
assert!(has_world.get());
signal.set("hello");
assert!(!has_world.get());
}
}