feat: resupport From<Fn() -> T> for Signal<T>, ArcSignal<T>, Callback<T, _> and similar (#4273)

This commit is contained in:
zakstucke
2025-11-24 20:50:11 +02:00
committed by GitHub
parent 2e09f3d102
commit a7a8970150
12 changed files with 607 additions and 92 deletions

View File

@@ -29,6 +29,7 @@ send_wrapper = { features = [
], workspace = true, default-features = true }
subsecond = { workspace = true, default-features = true, optional = true }
indexmap = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
@@ -40,6 +41,7 @@ tokio = { features = [
], workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
typed-builder.workspace = true
[build-dependencies]
rustc_version = { workspace = true, default-features = true }

View File

@@ -0,0 +1,417 @@
//! Callbacks define a standard way to store functions and closures. They are useful
//! for component properties, because they can be used to define optional callback functions,
//! which generic props dont support.
//!
//! The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`](reactive_graph::callback::Callback)
//! - [`UnsyncCallback`](reactive_graph::callback::UnsyncCallback)
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use crate::{
owner::{LocalStorage, StoredValue},
traits::{Dispose, WithValue},
IntoReactiveValue,
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
///
/// Returns None if the callback has been disposed
fn try_run(&self, input: In) -> Option<Out>;
/// calls the callback with the specified argument.
///
/// # Panics
/// Panics if you try to run a callback that has been disposed
fn run(&self, input: In) -> Out;
}
/// A callback type that is not required to be [`Send`] or [`Sync`].
///
/// # Example
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: UnsyncCallback<()> = UnsyncCallback::new(|_| {});
/// let _: UnsyncCallback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: UnsyncCallback<i32, String> = UnsyncCallback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// ```
pub struct UnsyncCallback<In: 'static, Out: 'static = ()>(
StoredValue<Rc<dyn Fn(In) -> Out>, LocalStorage>,
);
impl<In> fmt::Debug for UnsyncCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("Callback")
}
}
impl<In, Out> Copy for UnsyncCallback<In, Out> {}
impl<In, Out> Clone for UnsyncCallback<In, Out> {
fn clone(&self) -> Self {
*self
}
}
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> UnsyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
Self(StoredValue::new_local(Rc::new(f)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0.with_value(|self_value| {
other
.0
.with_value(|other_value| Rc::ptr_eq(self_value, other_value))
})
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|fun| fun(input))
}
}
macro_rules! impl_unsync_callable_from_fn {
($($arg:ident),*) => {
impl<F, $($arg,)* T, Out> From<F> for UnsyncCallback<($($arg,)*), Out>
where
F: Fn($($arg),*) -> T + 'static,
T: Into<Out> + 'static,
$($arg: 'static,)*
{
fn from(f: F) -> Self {
paste::paste!(
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
)
}
}
};
}
impl_unsync_callable_from_fn!();
impl_unsync_callable_from_fn!(P1);
impl_unsync_callable_from_fn!(P1, P2);
impl_unsync_callable_from_fn!(P1, P2, P3);
impl_unsync_callable_from_fn!(P1, P2, P3, P4);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
impl_unsync_callable_from_fn!(
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
);
/// A callback type that is [`Send`] + [`Sync`].
///
/// # Example
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: Callback<()> = Callback::new(|_| {});
/// let _: Callback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: Callback<i32, String> = Callback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// ```
pub struct Callback<In, Out = ()>(
StoredValue<Arc<dyn Fn(In) -> Out + Send + Sync>>,
)
where
In: 'static,
Out: 'static;
impl<In, Out> fmt::Debug for Callback<In, Out> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("SyncCallback")
}
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
}
}
impl<In, Out> Clone for Callback<In, Out> {
fn clone(&self) -> Self {
*self
}
}
impl<In, Out> Dispose for Callback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> Copy for Callback<In, Out> {}
macro_rules! impl_callable_from_fn {
($($arg:ident),*) => {
impl<F, $($arg,)* T, Out> From<F> for Callback<($($arg,)*), Out>
where
F: Fn($($arg),*) -> T + Send + Sync + 'static,
T: Into<Out> + 'static,
$($arg: Send + Sync + 'static,)*
{
fn from(f: F) -> Self {
paste::paste!(
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
)
}
}
};
}
impl_callable_from_fn!();
impl_callable_from_fn!(P1);
impl_callable_from_fn!(P1, P2);
impl_callable_from_fn!(P1, P2, P3);
impl_callable_from_fn!(P1, P2, P3, P4);
impl_callable_from_fn!(P1, P2, P3, P4, P5);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
impl<In: 'static, Out: 'static> Callback<In, Out> {
/// Creates a new callback from the given function.
#[track_caller]
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + Send + Sync + 'static,
{
Self(StoredValue::new(Arc::new(fun)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0
.try_with_value(|self_value| {
other.0.try_with_value(|other_value| {
Arc::ptr_eq(self_value, other_value)
})
})
.flatten()
.unwrap_or(false)
}
}
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackSingleParam;
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackStrOutputToString;
impl<I, O, F>
IntoReactiveValue<
Callback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, O> {
Callback::new(self)
}
}
impl<I, O, F>
IntoReactiveValue<
UnsyncCallback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, O> {
UnsyncCallback::new(self)
}
}
impl<I, F>
IntoReactiveValue<
Callback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, String> {
Callback::new(move |i| self(i).to_string())
}
}
impl<I, F>
IntoReactiveValue<
UnsyncCallback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, String> {
UnsyncCallback::new(move |i| self(i).to_string())
}
}
#[cfg(test)]
mod tests {
use super::Callable;
use crate::{
callback::{Callback, UnsyncCallback},
owner::Owner,
traits::Dispose,
IntoReactiveValue,
};
struct NoClone {}
#[test]
fn clone_callback() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
}
#[test]
fn clone_unsync_callback() {
let owner = Owner::new();
owner.set();
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
}
#[test]
fn runback_from() {
let owner = Owner::new();
owner.set();
let _callback: Callback<(), String> = (|| "test").into();
let _callback: Callback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: Callback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: Callback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_from() {
let owner = Owner::new();
owner.set();
let _callback: UnsyncCallback<(), String> = (|| "test").into();
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: UnsyncCallback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: UnsyncCallback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn unsync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
}
#[test]
fn callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
}

View File

@@ -0,0 +1,67 @@
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerBaseCase;
/// A helper trait that works like `Into<T>` but uses a marker generic
/// to allow more `From` implementations than would be allowed with just `Into<T>`.
pub trait IntoReactiveValue<T, M> {
/// Converts `self` into a `T`.
fn into_reactive_value(self) -> T;
}
// The base case, which allows anything which implements .into() to work:
impl<T, I> IntoReactiveValue<T, __IntoReactiveValueMarkerBaseCase> for I
where
I: Into<T>,
{
fn into_reactive_value(self) -> T {
self.into()
}
}
#[cfg(test)]
mod tests {
use crate::{
into_reactive_value::IntoReactiveValue,
owner::{LocalStorage, Owner},
traits::GetUntracked,
wrappers::read::Signal,
};
use typed_builder::TypedBuilder;
#[test]
fn test_into_signal_compiles() {
let owner = Owner::new();
owner.set();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize> = (|| 2).into_reactive_value();
let _: Signal<usize, LocalStorage> = 2.into_reactive_value();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize, LocalStorage> = (|| 2).into_reactive_value();
let _: Signal<String> = "str".into_reactive_value();
let _: Signal<String, LocalStorage> = "str".into_reactive_value();
#[derive(TypedBuilder)]
struct Foo {
#[builder(setter(
fn transform<M>(value: impl IntoReactiveValue<Signal<usize>, M>) {
value.into_reactive_value()
}
))]
sig: Signal<usize>,
}
assert_eq!(Foo::builder().sig(2).build().sig.get_untracked(), 2);
#[cfg(not(feature = "nightly"))]
assert_eq!(Foo::builder().sig(|| 2).build().sig.get_untracked(), 2);
assert_eq!(
Foo::builder()
.sig(Signal::stored(2))
.build()
.sig
.get_untracked(),
2
);
}
}

View File

@@ -90,6 +90,12 @@ pub mod traits;
pub mod transition;
pub mod wrappers;
mod into_reactive_value;
pub use into_reactive_value::*;
/// A standard way to wrap functions and closures to pass them to components.
pub mod callback;
use computed::ScopedFuture;
#[cfg(all(feature = "nightly", rustc_nightly))]
@@ -97,7 +103,9 @@ mod nightly;
/// Reexports frequently-used traits.
pub mod prelude {
pub use crate::{owner::FromLocal, traits::*};
pub use crate::{
into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*,
};
}
// TODO remove this, it's just useful while developing

View File

@@ -324,6 +324,22 @@ pub mod read {
}
}
impl<S> From<&'static str> for ArcSignal<String, S>
where
S: Storage<&'static str> + Storage<String>,
{
#[track_caller]
fn from(value: &'static str) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(
value.to_string(),
)),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T, S> DefinedAt for ArcSignal<T, S>
where
S: Storage<T>,
@@ -1049,6 +1065,13 @@ pub mod read {
}
}
impl From<Signal<&'static str, LocalStorage>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().to_string())
}
}
impl From<Signal<&'static str>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str>) -> Self {
@@ -1077,6 +1100,15 @@ pub mod read {
}
}
impl From<Signal<Option<&'static str>, LocalStorage>>
for Signal<Option<String>, LocalStorage>
{
#[track_caller]
fn from(value: Signal<Option<&'static str>, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().map(str::to_string))
}
}
impl From<Signal<Option<&'static str>>>
for Signal<Option<String>, LocalStorage>
{
@@ -1086,6 +1118,192 @@ pub mod read {
}
}
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalFromReactiveClosure;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalStrOutputToString;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways;
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<T, SyncStorage> {
Signal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, SyncStorage> {
ArcSignal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<T, LocalStorage> {
Signal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, LocalStorage> {
ArcSignal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<String, SyncStorage> {
Signal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, SyncStorage> {
ArcSignal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> Signal<String, LocalStorage> {
Signal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, LocalStorage> {
ArcSignal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, SyncStorage> {
Signal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, SyncStorage> {
ArcSignal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, LocalStorage> {
Signal::derive_local(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, LocalStorage> {
ArcSignal::derive_local(move || Some(self()))
}
}
#[allow(deprecated)]
impl<T> From<MaybeSignal<T>> for Signal<T>
where