diff --git a/Cargo.toml b/Cargo.toml index 7b71f122e1..68d5f616c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,7 +129,7 @@ members = [ "packages/playwright-tests/nested-suspense", "packages/playwright-tests/cli-optimization", "packages/playwright-tests/wasm-split-harness", - "packages/playwright-tests/default-features-disabled", + "packages/playwright-tests/default-features-disabled" ] [workspace.package] diff --git a/example-projects/ecommerce-site/src/components/product_page.rs b/example-projects/ecommerce-site/src/components/product_page.rs index e6741112dd..18f8f91edc 100644 --- a/example-projects/ecommerce-site/src/components/product_page.rs +++ b/example-projects/ecommerce-site/src/components/product_page.rs @@ -36,7 +36,7 @@ impl FromStr for Size { } #[component] -pub fn product_page(product_id: ReadOnlySignal) -> Element { +pub fn product_page(product_id: ReadSignal) -> Element { let mut quantity = use_signal(|| 1); let mut size = use_signal(Size::default); diff --git a/example-projects/fullstack-hackernews/src/main.rs b/example-projects/fullstack-hackernews/src/main.rs index c99fd7a31c..ec389d371d 100644 --- a/example-projects/fullstack-hackernews/src/main.rs +++ b/example-projects/fullstack-hackernews/src/main.rs @@ -32,7 +32,7 @@ pub fn App() -> Element { } #[component] -fn Homepage(story: ReadOnlySignal) -> Element { +fn Homepage(story: ReadSignal) -> Element { rsx! { document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") } div { display: "flex", flex_direction: "row", width: "100%", @@ -84,7 +84,7 @@ fn Stories() -> Element { } #[component] -fn StoryListing(story: ReadOnlySignal) -> Element { +fn StoryListing(story: ReadSignal) -> Element { let story = use_server_future(move || get_story(story()))?; let StoryItem { @@ -167,7 +167,7 @@ impl Display for PreviewState { } #[component] -fn Preview(story: ReadOnlySignal) -> Element { +fn Preview(story: ReadSignal) -> Element { let PreviewState { active_story: Some(id), } = story() diff --git a/examples/backgrounded_futures.rs b/examples/backgrounded_futures.rs index 4fabef09f7..0360638a0a 100644 --- a/examples/backgrounded_futures.rs +++ b/examples/backgrounded_futures.rs @@ -36,7 +36,7 @@ fn app() -> Element { } #[component] -fn Child(count: Signal) -> Element { +fn Child(count: WriteSignal) -> Element { let mut early_return = use_signal(|| false); let early = rsx! { diff --git a/examples/bevy/src/main.rs b/examples/bevy/src/main.rs index 6348b4b622..8a3f63dbea 100644 --- a/examples/bevy/src/main.rs +++ b/examples/bevy/src/main.rs @@ -75,7 +75,7 @@ fn app() -> Element { } #[component] -fn ColorControl(label: &'static str, color_str: Signal) -> Element { +fn ColorControl(label: &'static str, color_str: WriteSignal) -> Element { rsx!(div { class: "color-control", { label }, diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 6bee3b0b43..0fb35c71b6 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -60,7 +60,7 @@ fn app() -> Element { } #[component] -fn BreedPic(breed: Signal) -> Element { +fn BreedPic(breed: WriteSignal) -> Element { // This resource will restart whenever the breed changes let mut fut = use_resource(move || async move { #[derive(serde::Deserialize, Debug)] diff --git a/examples/hash_fragment_state.rs b/examples/hash_fragment_state.rs index 7eb5aa67eb..ab5ded505c 100644 --- a/examples/hash_fragment_state.rs +++ b/examples/hash_fragment_state.rs @@ -81,7 +81,7 @@ impl FromStr for State { } #[component] -fn Home(url_hash: ReadOnlySignal) -> Element { +fn Home(url_hash: ReadSignal) -> Element { // The initial state of the state comes from the url hash let mut state = use_signal(&*url_hash); diff --git a/examples/memo_chain.rs b/examples/memo_chain.rs index ab57089397..e7a744dbd2 100644 --- a/examples/memo_chain.rs +++ b/examples/memo_chain.rs @@ -28,7 +28,7 @@ fn app() -> Element { } #[component] -fn Child(state: Memo, items: Memo>, depth: ReadOnlySignal) -> Element { +fn Child(state: Memo, items: Memo>, depth: ReadSignal) -> Element { // These memos don't get re-computed when early returns happen let state = use_memo(move || state() + 1); let item = use_memo(move || items()[depth() - 1]); diff --git a/examples/query_segment_search.rs b/examples/query_segment_search.rs index 4366cc0118..8830d33478 100644 --- a/examples/query_segment_search.rs +++ b/examples/query_segment_search.rs @@ -63,9 +63,9 @@ fn Home() -> Element { } } -// Instead of accepting String and usize directly, we use ReadOnlySignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme +// Instead of accepting String and usize directly, we use ReadSignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme #[component] -fn Search(query: ReadOnlySignal, word_count: ReadOnlySignal) -> Element { +fn Search(query: ReadSignal, word_count: ReadSignal) -> Element { const ITEMS: &[&str] = &[ "hello", "world", diff --git a/examples/router_resource.rs b/examples/router_resource.rs index 6898585f3b..2306f181c8 100644 --- a/examples/router_resource.rs +++ b/examples/router_resource.rs @@ -1,7 +1,7 @@ //! Example: Updating components with use_resource //! ----------------- //! -//! This example shows how to use ReadOnlySignal to make props reactive +//! This example shows how to use ReadSignal to make props reactive //! when linking to it from the same component, when using use_resource use dioxus::prelude::*; @@ -25,15 +25,15 @@ fn App() -> Element { } } -// We use id: ReadOnlySignal instead of id: i32 to make id work with reactive hooks -// Any i32 we pass in will automatically be converted into a ReadOnlySignal +// We use id: ReadSignal instead of id: i32 to make id work with reactive hooks +// Any i32 we pass in will automatically be converted into a ReadSignal #[component] -fn Blog(id: ReadOnlySignal) -> Element { +fn Blog(id: ReadSignal) -> Element { async fn future(n: i32) -> i32 { n } - // Because we accept ReadOnlySignal instead of i32, the resource will automatically subscribe to the id when we read it + // Because we accept ReadSignal instead of i32, the resource will automatically subscribe to the id when we read it let res = use_resource(move || future(id())); match res() { diff --git a/examples/signals.rs b/examples/signals.rs index 6b881587bc..5d0e3aae42 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -3,7 +3,7 @@ //! This simple example implements a counter that can be incremented, decremented, and paused. It also demonstrates //! that background tasks in use_futures can modify the value as well. //! -//! Most signals implement Into>, making ReadOnlySignal a good default type when building new +//! Most signals implement Into>, making ReadSignal a good default type when building new //! library components that don't need to modify their values. use async_std::task::sleep; @@ -81,7 +81,7 @@ fn app() -> Element { } #[component] -fn Child(mut count: ReadOnlySignal) -> Element { +fn Child(mut count: ReadSignal) -> Element { println!("rendering child with count {count}"); rsx! { diff --git a/examples/todomvc.rs b/examples/todomvc.rs index f57a8d6c7b..fd85d550e1 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -110,7 +110,7 @@ fn app() -> Element { } #[component] -fn TodoHeader(mut todos: Signal>) -> Element { +fn TodoHeader(mut todos: WriteSignal>) -> Element { let mut draft = use_signal(|| "".to_string()); let mut todo_id = use_signal(|| 0); @@ -146,7 +146,7 @@ fn TodoHeader(mut todos: Signal>) -> Element { /// This takes the ID of the todo and the todos signal as props /// We can use these together to memoize the todo contents and checked state #[component] -fn TodoEntry(mut todos: Signal>, id: u32) -> Element { +fn TodoEntry(mut todos: WriteSignal>, id: u32) -> Element { let mut is_editing = use_signal(|| false); // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos @@ -208,9 +208,9 @@ fn TodoEntry(mut todos: Signal>, id: u32) -> Element { #[component] fn ListFooter( - mut todos: Signal>, - active_todo_count: ReadOnlySignal, - mut filter: Signal, + mut todos: WriteSignal>, + active_todo_count: ReadSignal, + mut filter: WriteSignal, ) -> Element { // We use a memoized signal to calculate whether we should show the "Clear completed" button. // This will recompute whenever the todos change, and if the value is true, the button will be shown. diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 9df13c7a20..687188f701 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -108,7 +108,7 @@ fn Forecast(weather: WeatherResponse) -> Element { } #[component] -fn SearchBox(mut country: Signal) -> Element { +fn SearchBox(mut country: WriteSignal) -> Element { let mut input = use_signal(|| "".to_string()); let locations = use_resource(move || async move { get_locations(&input()).await }); diff --git a/examples/wgpu-texture/src/main.rs b/examples/wgpu-texture/src/main.rs index 58d5698b61..dca4413841 100644 --- a/examples/wgpu-texture/src/main.rs +++ b/examples/wgpu-texture/src/main.rs @@ -87,7 +87,7 @@ fn app() -> Element { } #[component] -fn ColorControl(label: &'static str, color_str: Signal) -> Element { +fn ColorControl(label: &'static str, color_str: WriteSignal) -> Element { rsx!(div { class: "color-control", { label }, diff --git a/packages/core-macro/docs/component.md b/packages/core-macro/docs/component.md index d30a84e83c..dbabc90d60 100644 --- a/packages/core-macro/docs/component.md +++ b/packages/core-macro/docs/component.md @@ -51,7 +51,7 @@ You can use the `#[props()]` attribute to modify the behavior of the props the c Props also act slightly differently when used with: - [`Option`](#optional-props) - The field is automatically optional with a default value of `None`. -- [`ReadOnlySignal`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal` when it is passed as a prop. +- [`ReadSignal`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal` when it is passed as a prop. - [`String`](#formatted-props) - The props macro will accept formatted strings for any prop field with the type `String`. - [`children`](#children-props) - The props macro will accept child elements if you include the `children` prop. @@ -257,14 +257,14 @@ fn Counter(count: i32) -> Element { To fix this issue you can either: -1. Make the prop reactive by wrapping it in `ReadOnlySignal` (recommended): +1. Make the prop reactive by wrapping it in `ReadSignal` (recommended): -`ReadOnlySignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadOnlySignal` when it is passed as a prop. +`ReadSignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadSignal` when it is passed as a prop. ```rust, no_run # use dioxus::prelude::*; #[component] -fn Counter(count: ReadOnlySignal) -> Element { +fn Counter(count: ReadSignal) -> Element { // Since we made count reactive, the memo will automatically rerun when count changes. let doubled_count = use_memo(move || count() * 2); rsx! { diff --git a/packages/core-macro/docs/props.md b/packages/core-macro/docs/props.md index 093fb695b2..b553d8caa3 100644 --- a/packages/core-macro/docs/props.md +++ b/packages/core-macro/docs/props.md @@ -47,7 +47,7 @@ You can use the `#[props()]` attribute to modify the behavior of the props deriv Props also act slightly differently when used with: - [`Option`](#optional-props) - The field is automatically optional with a default value of `None`. -- [`ReadOnlySignal`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal` when it is passed as a prop. +- [`ReadSignal`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal` when it is passed as a prop. - [`String`](#formatted-props) - The props macro will accept formatted strings for any prop field with the type `String`. - [`children`](#children-props) - The props macro will accept child elements if you include the `children` prop. @@ -270,14 +270,14 @@ fn Counter(count: i32) -> Element { To fix this issue you can either: -1. Make the prop reactive by wrapping it in `ReadOnlySignal` (recommended): +1. Make the prop reactive by wrapping it in `ReadSignal` (recommended): -`ReadOnlySignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadOnlySignal` when it is passed as a prop. +`ReadSignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadSignal` when it is passed as a prop. ```rust, no_run # use dioxus::prelude::*; #[component] -fn Counter(count: ReadOnlySignal) -> Element { +fn Counter(count: ReadSignal) -> Element { // Since we made count reactive, the memo will automatically rerun when count changes. let doubled_count = use_memo(move || count() * 2); rsx! { diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index c0c389be22..400705797a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -172,7 +172,7 @@ mod util { } mod field_info { - use crate::props::type_from_inside_option; + use crate::props::{looks_like_write_type, type_from_inside_option}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::spanned::Spanned; @@ -221,6 +221,11 @@ mod field_info { builder_attr.auto_into = false; } + // Write fields automatically use impl Into + if looks_like_write_type(&field.ty) { + builder_attr.auto_into = true; + } + // extended field is automatically empty if !builder_attr.extends.is_empty() { builder_attr.default = Some( @@ -503,9 +508,9 @@ fn type_from_inside_option(ty: &Type) -> Option<&Type> { let seg = path.segments.last()?; // If the segment is a supported optional type, provide the inner type. - // Return the inner type if the pattern is `Option` or `ReadOnlySignal>`` - if seg.ident == "ReadOnlySignal" { - // Get the inner type. E.g. the `u16` in `ReadOnlySignal` or `Option` in `ReadOnlySignal>` + // Return the inner type if the pattern is `Option` or `ReadSignal>`` + if seg.ident == "ReadOnlySignal" || seg.ident == "ReadSignal" { + // Get the inner type. E.g. the `u16` in `ReadSignal` or `Option` in `ReadSignal>` let inner_type = extract_inner_type_from_segment(seg)?; let Type::Path(inner_path) = inner_type else { // If it isn't a path, the inner type isn't option @@ -614,13 +619,13 @@ mod struct_info { generics } - /// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadOnlySignal`, the new signal should be owned by the child + /// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadSignal`, the new signal should be owned by the child fn has_child_owned_fields(&self) -> bool { self.fields.iter().any(|f| child_owned_type(f.ty)) } fn memoize_impl(&self) -> Result { - // First check if there are any ReadOnlySignal fields, if there are not, we can just use the partialEq impl + // First check if there are any ReadSignal fields, if there are not, we can just use the partialEq impl let signal_fields: Vec<_> = self .included_fields() .filter(|f| looks_like_signal_type(f.ty)) @@ -1736,7 +1741,7 @@ fn remove_option_wrapper(type_: Type) -> Type { /// Check if a type should be owned by the child component after conversion fn child_owned_type(ty: &Type) -> bool { - looks_like_signal_type(ty) || looks_like_callback_type(ty) + looks_like_signal_type(ty) || looks_like_write_type(ty) || looks_like_callback_type(ty) } fn looks_like_signal_type(ty: &Type) -> bool { @@ -1745,6 +1750,20 @@ fn looks_like_signal_type(ty: &Type) -> bool { path_without_generics == parse_quote!(dioxus_core::ReadOnlySignal) || path_without_generics == parse_quote!(prelude::ReadOnlySignal) || path_without_generics == parse_quote!(ReadOnlySignal) + || path_without_generics == parse_quote!(dioxus_core::prelude::ReadSignal) + || path_without_generics == parse_quote!(prelude::ReadSignal) + || path_without_generics == parse_quote!(ReadSignal) + } + None => false, + } +} + +fn looks_like_write_type(ty: &Type) -> bool { + match extract_base_type_without_generics(ty) { + Some(path_without_generics) => { + path_without_generics == parse_quote!(dioxus_core::prelude::WriteSignal) + || path_without_generics == parse_quote!(prelude::WriteSignal) + || path_without_generics == parse_quote!(WriteSignal) } None => false, } @@ -1778,6 +1797,17 @@ fn test_looks_like_type() { ReadOnlySignal, UnsyncStorage> ))); + assert!(!looks_like_signal_type(&parse_quote!( + Option> + ))); + assert!(looks_like_signal_type(&parse_quote!(ReadSignal))); + assert!(looks_like_signal_type( + &parse_quote!(ReadSignal) + )); + assert!(looks_like_signal_type(&parse_quote!( + ReadSignal, UnsyncStorage> + ))); + assert!(looks_like_callback_type(&parse_quote!( Option ))); diff --git a/packages/core-macro/tests/rsx.rs b/packages/core-macro/tests/rsx.rs index 2246534bef..f25b1b05b6 100644 --- a/packages/core-macro/tests/rsx.rs +++ b/packages/core-macro/tests/rsx.rs @@ -52,13 +52,13 @@ mod test_default_into { // Test default values for signals #[props(default)] - read_only_w_default: ReadOnlySignal, + read_only_w_default: ReadSignal, #[props(default = true)] - read_only_w_default_val: ReadOnlySignal, + read_only_w_default_val: ReadSignal, - #[props(default = ReadOnlySignal::new(Signal::new(true)))] - read_only_w_default_val_explicit: ReadOnlySignal, + #[props(default = ReadSignal::new(Signal::new(true)))] + read_only_w_default_val_explicit: ReadSignal, // Test default values for callbacks/event handlers #[props(default)] @@ -108,7 +108,7 @@ mod test_optional_signals { fn UsesComponents() -> Element { rsx! { PropsStruct { - regular_read_signal: ReadOnlySignal::new(Signal::new(1234)), + regular_read_signal: ReadSignal::new(Signal::new(1234)), } PropsStruct { optional_read_signal: 1234, @@ -126,8 +126,8 @@ mod test_optional_signals { // Test props as struct param. #[derive(Props, Clone, PartialEq)] struct MyTestProps { - pub optional_read_signal: ReadOnlySignal>, - pub regular_read_signal: ReadOnlySignal, + pub optional_read_signal: ReadSignal>, + pub regular_read_signal: ReadSignal, } #[component] @@ -137,7 +137,7 @@ mod test_optional_signals { // Test props as params. #[component] - fn PropParams(opt_read_sig: ReadOnlySignal>) -> Element { + fn PropParams(opt_read_sig: ReadSignal>) -> Element { rsx! { "hi!" } } diff --git a/packages/core-macro/tests/values_memoize_in_place.rs b/packages/core-macro/tests/values_memoize_in_place.rs index edfb6afc05..17738b818f 100644 --- a/packages/core-macro/tests/values_memoize_in_place.rs +++ b/packages/core-macro/tests/values_memoize_in_place.rs @@ -124,7 +124,7 @@ fn TakesEventHandler(click: EventHandler, number: usize) -> Element { } #[component] -fn TakesSignal(sig: ReadOnlySignal, number: usize) -> Element { +fn TakesSignal(sig: ReadSignal, number: usize) -> Element { let first_render_sig = use_hook(move || sig); if generation() > 0 { // Make sure the signal is memoized in place and never gets dropped @@ -190,12 +190,12 @@ fn cloning_read_only_signal_components_work() { struct NonCloneable(T); #[component] - fn TakesReadOnlySignalNum(sig: ReadOnlySignal) -> Element { + fn TakesReadOnlySignalNum(sig: ReadSignal) -> Element { rsx! {} } #[component] - fn TakesReadOnlySignalNonClone(sig: ReadOnlySignal>) -> Element { + fn TakesReadOnlySignalNonClone(sig: ReadSignal>) -> Element { rsx! {} } diff --git a/packages/core/docs/common_spawn_errors.md b/packages/core/docs/common_spawn_errors.md index 257d7c54d9..bd99f335e7 100644 --- a/packages/core/docs/common_spawn_errors.md +++ b/packages/core/docs/common_spawn_errors.md @@ -74,13 +74,13 @@ fn MyComponent(string: String) -> Element { You can fix this issue by either: -- Making your data `Copy` with `ReadOnlySignal`: +- Making your data `Copy` with `ReadSignal`: ```rust, no_run # use dioxus::prelude::*; -// `MyComponent` accepts `ReadOnlySignal` which implements `Copy` +// `MyComponent` accepts `ReadSignal` which implements `Copy` #[component] -fn MyComponent(string: ReadOnlySignal) -> Element { +fn MyComponent(string: ReadSignal) -> Element { use_hook(move || { // ✅ Because the `string` signal is `Copy`, we can copy it into the async task while still having access to it elsewhere spawn(async move { diff --git a/packages/core/docs/reactivity.md b/packages/core/docs/reactivity.md index fdb0fe8c8b..4fbfcdb49a 100644 --- a/packages/core/docs/reactivity.md +++ b/packages/core/docs/reactivity.md @@ -92,9 +92,9 @@ fn MyComponent(state: i32) -> Element { todo!() } -// ✅ Wrap your props in ReadOnlySignal to make them reactive +// ✅ Wrap your props in ReadSignal to make them reactive #[component] -fn MyReactiveComponent(state: ReadOnlySignal) -> Element { +fn MyReactiveComponent(state: ReadSignal) -> Element { let doubled = use_memo(move || state() * 2); todo!() } diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 8f0dfe2012..05697cc61a 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -730,7 +730,7 @@ impl< /// } /// /// #[component] -/// fn Counter(multiplier: ReadOnlySignal) -> Element { +/// fn Counter(multiplier: ReadSignal) -> Element { /// let multiplier_parsed = multiplier().parse::()?; /// let mut count = use_signal(|| multiplier_parsed); /// rsx! { @@ -783,7 +783,7 @@ impl< /// } /// /// #[component] -/// fn Counter(multiplier: ReadOnlySignal) -> Element { +/// fn Counter(multiplier: ReadSignal) -> Element { /// let multiplier_parsed = multiplier().parse::()?; /// let mut count = use_signal(|| multiplier_parsed); /// rsx! { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index e851915ee4..c093003e0e 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -87,10 +87,10 @@ pub use crate::innerlude::{ Element, ElementId, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LaunchConfig, ListenerCallback, MarkerWrapper, Mutation, Mutations, NoOpMutations, Ok, OptionStringFromMarker, Properties, ReactiveContext, RenderError, - Result, Runtime, RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SuperFrom, SuperInto, - SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, - Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, - VText, VirtualDom, WriteMutations, + Result, Runtime, RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SubscriberList, Subscribers, + SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, + SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, + VComponent, VNode, VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations, }; pub use const_format; diff --git a/packages/core/src/reactive_context.rs b/packages/core/src/reactive_context.rs index 3bdc50c41d..b54d475712 100644 --- a/packages/core/src/reactive_context.rs +++ b/packages/core/src/reactive_context.rs @@ -141,7 +141,7 @@ impl ReactiveContext { #[allow(clippy::mutable_key_type)] let old_subscribers = std::mem::take(&mut self.inner.write().subscribers); for subscriber in old_subscribers { - subscriber.0.lock().unwrap().remove(self); + subscriber.0.remove(self); } } @@ -150,7 +150,7 @@ impl ReactiveContext { #[allow(clippy::mutable_key_type)] let subscribers = &self.inner.read().subscribers; for subscriber in subscribers.iter() { - subscriber.0.lock().unwrap().insert(*self); + subscriber.0.add(*self); } } @@ -238,11 +238,14 @@ impl ReactiveContext { } /// Subscribe to this context. The reactive context will automatically remove itself from the subscriptions when it is reset. - pub fn subscribe(&self, subscriptions: Arc>>) { + pub fn subscribe(&self, subscriptions: impl Into) { match self.inner.try_write() { Ok(mut inner) => { - subscriptions.lock().unwrap().insert(*self); - inner.subscribers.insert(PointerHash(subscriptions)); + let subscriptions = subscriptions.into(); + subscriptions.add(*self); + inner + .subscribers + .insert(PointerHash(subscriptions.inner.clone())); } // If the context was dropped, we don't need to subscribe to it anymore Err(BorrowMutError::Dropped(_)) => {} @@ -266,30 +269,28 @@ impl Hash for ReactiveContext { } } -struct PointerHash(Arc); +struct PointerHash(Arc); -impl Hash for PointerHash { +impl Hash for PointerHash { fn hash(&self, state: &mut H) { std::sync::Arc::::as_ptr(&self.0).hash(state); } } -impl PartialEq for PointerHash { +impl PartialEq for PointerHash { fn eq(&self, other: &Self) -> bool { - std::sync::Arc::::as_ptr(&self.0) == std::sync::Arc::::as_ptr(&other.0) + std::sync::Arc::ptr_eq(&self.0, &other.0) } } -impl Eq for PointerHash {} +impl Eq for PointerHash {} -impl Clone for PointerHash { +impl Clone for PointerHash { fn clone(&self) -> Self { Self(self.0.clone()) } } -type SubscriberMap = Mutex>; - struct Inner { self_: Option, @@ -297,7 +298,7 @@ struct Inner { update: Box, // Subscribers to this context - subscribers: HashSet>, + subscribers: HashSet>, // Debug information for signal subscriptions #[cfg(debug_assertions)] @@ -315,9 +316,88 @@ impl Drop for Inner { }; for subscriber in std::mem::take(&mut self.subscribers) { - if let Ok(mut subscriber) = subscriber.0.lock() { - subscriber.remove(&self_); - } + subscriber.0.remove(&self_); + } + } +} + +/// A list of [ReactiveContext]s that are subscribed. This is used to notify subscribers when the value changes. +#[derive(Clone)] +pub struct Subscribers { + /// The list of subscribers. + pub(crate) inner: Arc, +} + +impl Default for Subscribers { + fn default() -> Self { + Self::new() + } +} + +impl Subscribers { + /// Create a new list of subscribers. + pub fn new() -> Self { + Subscribers { + inner: Arc::new(Mutex::new(HashSet::new())), + } + } + + /// Add a subscriber to the list. + pub fn add(&self, subscriber: ReactiveContext) { + self.inner.add(subscriber); + } + + /// Remove a subscriber from the list. + pub fn remove(&self, subscriber: &ReactiveContext) { + self.inner.remove(subscriber); + } + + /// Visit all subscribers in the list. + pub fn visit(&self, mut f: impl FnMut(&ReactiveContext)) { + self.inner.visit(&mut f); + } +} + +impl From> for Subscribers { + fn from(inner: Arc) -> Self { + Subscribers { inner } + } +} + +/// A list of subscribers that can be notified when the value changes. This is used to track when the value changes and notify subscribers. +pub trait SubscriberList: Send + Sync { + /// Add a subscriber to the list. + fn add(&self, subscriber: ReactiveContext); + + /// Remove a subscriber from the list. + fn remove(&self, subscriber: &ReactiveContext); + + /// Visit all subscribers in the list. + fn visit(&self, f: &mut dyn FnMut(&ReactiveContext)); +} + +impl SubscriberList for Mutex> { + fn add(&self, subscriber: ReactiveContext) { + if let Ok(mut lock) = self.lock() { + lock.insert(subscriber); + } else { + tracing::warn!("Failed to lock subscriber list to add subscriber: {subscriber}"); + } + } + + fn remove(&self, subscriber: &ReactiveContext) { + if let Ok(mut lock) = self.lock() { + lock.remove(subscriber); + } else { + tracing::warn!("Failed to lock subscriber list to remove subscriber: {subscriber}"); + } + } + + fn visit(&self, f: &mut dyn FnMut(&ReactiveContext)) { + if let Ok(lock) = self.lock() { + lock.iter().for_each(f); + } else { + tracing::warn!("Failed to lock subscriber list to visit subscribers"); } } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index c67dbba498..ba0fea6c0b 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -17,7 +17,7 @@ //! } //! //! #[component] -//! fn ChildComponent(signal: Signal>) -> Element { +//! fn ChildComponent(signal: WriteSignal>) -> Element { //! // It feels safe to assume that signal is some because the parent component checked that it was some //! rsx! { "{signal.read().unwrap()}" } //! } diff --git a/packages/core/tests/memory_leak.rs b/packages/core/tests/memory_leak.rs index c3311921f4..c6d63ea9ef 100644 --- a/packages/core/tests/memory_leak.rs +++ b/packages/core/tests/memory_leak.rs @@ -44,7 +44,7 @@ async fn test_for_memory_leaks() { #[component] fn AcceptsEventHandlerAndReadOnlySignal( event_handler: EventHandler, - signal: ReadOnlySignal, + signal: ReadSignal, ) -> Element { rsx! { div { diff --git a/packages/devtools/src/lib.rs b/packages/devtools/src/lib.rs index 833aedd065..53aa732005 100644 --- a/packages/devtools/src/lib.rs +++ b/packages/devtools/src/lib.rs @@ -1,6 +1,6 @@ use dioxus_core::internal::HotReloadedTemplate; use dioxus_core::{ScopeId, VirtualDom}; -use dioxus_signals::{GlobalKey, Signal, Writable}; +use dioxus_signals::{GlobalKey, Signal, WritableExt}; pub use dioxus_devtools_types::*; pub use subsecond; diff --git a/packages/dioxus/README.md b/packages/dioxus/README.md index 8563ad49da..b0a8eec427 100644 --- a/packages/dioxus/README.md +++ b/packages/dioxus/README.md @@ -32,6 +32,7 @@ Dioxus is crossplatform app framework that empowers developer to build beautiful ## Quick start To get started with Dioxus, you'll want to grab the dioxus-cli tool: `dx`. We distribute `dx` with `cargo-binstall` - if you already have binstall skip this step. + ```shell # skip if you already have cargo-binstall cargo install cargo-binstall @@ -123,7 +124,7 @@ fn App() -> Element { We can compose these function components to build a complex app. Each new component takes some Properties. For components with no explicit properties, we can omit the type altogether. -In Dioxus, all properties are memoized by default with `Clone` and `PartialEq`. For props you can't clone, simply wrap the fields in a [`ReadOnlySignal`](dioxus_signals::ReadOnlySignal) and Dioxus will handle converting types for you. +In Dioxus, all properties are memoized by default with `Clone` and `PartialEq`. For props you can't clone, simply wrap the fields in a [`ReadSignal`](dioxus_signals::ReadSignal) and Dioxus will handle converting types for you. ```rust, no_run # use dioxus::prelude::*; diff --git a/packages/fullstack-hooks/src/hooks/server_future.rs b/packages/fullstack-hooks/src/hooks/server_future.rs index f18a0f9e16..6937341e08 100644 --- a/packages/fullstack-hooks/src/hooks/server_future.rs +++ b/packages/fullstack-hooks/src/hooks/server_future.rs @@ -1,6 +1,6 @@ use dioxus_core::{suspend, use_hook, RenderError}; use dioxus_hooks::*; -use dioxus_signals::Readable; +use dioxus_signals::ReadableExt; use serde::{de::DeserializeOwned, Serialize}; use std::future::Future; diff --git a/packages/fullstack-hooks/src/streaming.rs b/packages/fullstack-hooks/src/streaming.rs index da9d90459b..5f7177bc4c 100644 --- a/packages/fullstack-hooks/src/streaming.rs +++ b/packages/fullstack-hooks/src/streaming.rs @@ -1,5 +1,5 @@ use dioxus_core::try_consume_context; -use dioxus_signals::{Readable, Signal, Writable}; +use dioxus_signals::{ReadableExt, Signal, WritableExt}; /// The status of the streaming response #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/packages/hooks/docs/use_resource.md b/packages/hooks/docs/use_resource.md index f252b62a32..8901fe4df6 100644 --- a/packages/hooks/docs/use_resource.md +++ b/packages/hooks/docs/use_resource.md @@ -111,7 +111,7 @@ println!("{:?}", double_count.read()); // Prints "Some(8)" ## With non-reactive dependencies -`use_resource` can determine dependencies automatically with any reactive value ([`Signal`]s, [`ReadOnlySignal`]s, [`Memo`]s, [`Resource`]s, etc). If you need to rerun the future when a normal rust value changes, you can add it as a dependency with the [`crate::use_reactive()`] hook: +`use_resource` can determine dependencies automatically with any reactive value ([`Signal`]s, [`ReadSignal`]s, [`Memo`]s, [`Resource`]s, etc). If you need to rerun the future when a normal rust value changes, you can add it as a dependency with the [`crate::use_reactive()`] hook: ```rust # use dioxus::prelude::*; @@ -128,9 +128,9 @@ fn Comp(count: u32) -> Element { } // If your value is already reactive, you never need to call `use_reactive` manually -// Instead of manually adding count to the dependencies list, you can make your prop reactive by wrapping it in `ReadOnlySignal` +// Instead of manually adding count to the dependencies list, you can make your prop reactive by wrapping it in `ReadSignal` #[component] -fn ReactiveComp(count: ReadOnlySignal) -> Element { +fn ReactiveComp(count: ReadSignal) -> Element { // Because `count` is reactive, the resource knows to rerun when `count` changes automatically let new_count = use_resource(move || async move { sleep(100).await; diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index cc3c5e461e..388cd53514 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] use crate::{use_callback, use_hook_did_run, use_signal}; -use dioxus_core::{use_hook, Callback, Task}; +use dioxus_core::{use_hook, Callback, Subscribers, Task}; use dioxus_signals::*; use std::future::Future; use std::ops::Deref; @@ -152,12 +152,12 @@ impl UseFuture { } /// Get the current state of the future. - pub fn state(&self) -> ReadOnlySignal { + pub fn state(&self) -> ReadSignal { self.state.into() } } -impl From for ReadOnlySignal { +impl From for ReadSignal { fn from(val: UseFuture) -> Self { val.state.into() } @@ -180,6 +180,10 @@ impl Readable for UseFuture { ) -> Result, generational_box::BorrowError> { self.state.try_peek_unchecked() } + + fn subscribers(&self) -> Option { + self.state.subscribers() + } } /// Allow calling a signal with signal() syntax @@ -189,6 +193,6 @@ impl Deref for UseFuture { type Target = dyn Fn() -> UseFutureState; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } diff --git a/packages/hooks/src/use_hook_did_run.rs b/packages/hooks/src/use_hook_did_run.rs index d65873a15d..87fcfe0582 100644 --- a/packages/hooks/src/use_hook_did_run.rs +++ b/packages/hooks/src/use_hook_did_run.rs @@ -1,5 +1,5 @@ use dioxus_core::{use_after_render, use_before_render, use_hook}; -use dioxus_signals::{CopyValue, Writable}; +use dioxus_signals::{CopyValue, WritableExt}; /// A hook that uses before/after lifecycle hooks to determine if the hook was run #[doc = include_str!("../docs/rules_of_hooks.md")] diff --git a/packages/hooks/src/use_reactive.rs b/packages/hooks/src/use_reactive.rs index 4fcf07a957..eead128155 100644 --- a/packages/hooks/src/use_reactive.rs +++ b/packages/hooks/src/use_reactive.rs @@ -1,4 +1,4 @@ -use dioxus_signals::{Readable, Writable}; +use dioxus_signals::{ReadableExt, WritableExt}; use crate::use_signal; diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index 50a635f4d4..5b63e3ba6f 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -4,7 +4,7 @@ use crate::{use_callback, use_signal}; use dioxus_core::{ spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, RenderError, - SuspendedFuture, Task, + Subscribers, SuspendedFuture, Task, }; use dioxus_signals::*; use futures_util::{future, pin_mut, FutureExt, StreamExt}; @@ -349,7 +349,7 @@ impl Resource { ) } - /// Get the current state of the resource's future. This method returns a [`ReadOnlySignal`] which can be read to get the current state of the resource or passed to other hooks and components. + /// Get the current state of the resource's future. This method returns a [`ReadSignal`] which can be read to get the current state of the resource or passed to other hooks and components. /// /// ## Example /// ```rust, no_run @@ -379,11 +379,11 @@ impl Resource { /// } /// } /// ``` - pub fn state(&self) -> ReadOnlySignal { + pub fn state(&self) -> ReadSignal { self.state.into() } - /// Get the current value of the resource's future. This method returns a [`ReadOnlySignal`] which can be read to get the current value of the resource or passed to other hooks and components. + /// Get the current value of the resource's future. This method returns a [`ReadSignal`] which can be read to get the current value of the resource or passed to other hooks and components. /// /// ## Example /// @@ -410,12 +410,12 @@ impl Resource { /// } /// } /// ``` - pub fn value(&self) -> ReadOnlySignal> { + pub fn value(&self) -> ReadSignal> { self.value.into() } /// Suspend the resource's future and only continue rendering when the future is ready - pub fn suspend(&self) -> std::result::Result, RenderError> { + pub fn suspend(&self) -> std::result::Result>>, RenderError> { match self.state.cloned() { UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => { let task = self.task(); @@ -430,7 +430,7 @@ impl Resource { } } -impl From> for ReadOnlySignal> { +impl From> for ReadSignal> { fn from(val: Resource) -> Self { val.value.into() } @@ -453,6 +453,10 @@ impl Readable for Resource { ) -> Result, generational_box::BorrowError> { self.value.try_peek_unchecked() } + + fn subscribers(&self) -> Option { + self.value.subscribers() + } } impl IntoAttributeValue for Resource @@ -480,6 +484,6 @@ impl Deref for Resource { type Target = dyn Fn() -> Option; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } diff --git a/packages/hooks/src/use_set_compare.rs b/packages/hooks/src/use_set_compare.rs index 624257aa67..faa3727df8 100644 --- a/packages/hooks/src/use_set_compare.rs +++ b/packages/hooks/src/use_set_compare.rs @@ -1,5 +1,5 @@ use dioxus_core::use_hook; -use dioxus_signals::{ReadOnlySignal, SetCompare}; +use dioxus_signals::{ReadSignal, SetCompare}; use std::hash::Hash; /// Creates a new SetCompare which efficiently tracks when a value changes to check if it is equal to a set of values. @@ -51,6 +51,6 @@ pub fn use_set_compare(f: impl FnMut() -> R + 'static) -> SetCompa pub fn use_set_compare_equal( value: R, mut compare: SetCompare, -) -> ReadOnlySignal { +) -> ReadSignal { use_hook(move || compare.equal(value)) } diff --git a/packages/hooks/src/use_sorted.rs b/packages/hooks/src/use_sorted.rs index f293e4b101..889a1183e8 100644 --- a/packages/hooks/src/use_sorted.rs +++ b/packages/hooks/src/use_sorted.rs @@ -2,12 +2,12 @@ use std::cmp::Ordering; use std::ops::DerefMut; use crate::use_memo; -use dioxus_signals::{ReadOnlySignal, Signal}; +use dioxus_signals::{ReadSignal, Signal}; pub fn use_sorted( collection: impl FnMut() -> Signal, -) -> ReadOnlySignal> -// pub fn use_sorted(iterable: impl FnMut() -> Signal) -> ReadOnlySignal +) -> ReadSignal> +// pub fn use_sorted(iterable: impl FnMut() -> Signal) -> ReadSignal // where // S: Into>, // T: Ord, diff --git a/packages/html/docs/common_event_handler_errors.md b/packages/html/docs/common_event_handler_errors.md index b3935006a3..f5e65a064c 100644 --- a/packages/html/docs/common_event_handler_errors.md +++ b/packages/html/docs/common_event_handler_errors.md @@ -81,13 +81,13 @@ fn MyComponent(string: String) -> Element { You can fix this issue by either: -- Making your data `Copy` with `ReadOnlySignal`: +- Making your data `Copy` with `ReadSignal`: ```rust, no_run # use dioxus::prelude::*; -// `MyComponent` accepts `ReadOnlySignal` which implements `Copy` +// `MyComponent` accepts `ReadSignal` which implements `Copy` #[component] -fn MyComponent(string: ReadOnlySignal) -> Element { +fn MyComponent(string: ReadSignal) -> Element { rsx! { button { // ✅ Because the `string` signal is `Copy`, we can copy it into the closure while still having access to it elsewhere diff --git a/packages/router/src/components/default_errors.rs b/packages/router/src/components/default_errors.rs index d203844662..077c944659 100644 --- a/packages/router/src/components/default_errors.rs +++ b/packages/router/src/components/default_errors.rs @@ -1,7 +1,7 @@ use dioxus_core::Element; use dioxus_core_macro::rsx; use dioxus_html as dioxus_elements; -use dioxus_signals::{GlobalSignal, Readable}; +use dioxus_signals::{GlobalSignal, ReadableExt}; #[allow(deprecated)] use crate::hooks::use_router; diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs index 5fa3ed727f..cc607f032f 100644 --- a/packages/router/src/components/history_buttons.rs +++ b/packages/router/src/components/history_buttons.rs @@ -1,7 +1,7 @@ use dioxus_core::{Element, VNode}; use dioxus_core_macro::{rsx, Props}; use dioxus_html as dioxus_elements; -use dioxus_signals::{GlobalSignal, Readable}; +use dioxus_signals::{GlobalSignal, ReadableExt}; use tracing::error; diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 3c7e684e23..21dc1336c3 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -7,7 +7,7 @@ use dioxus_core_macro::{rsx, Props}; use dioxus_html::{ self as dioxus_elements, ModifiersInteraction, MountedEvent, MouseEvent, PointerInteraction, }; -use dioxus_signals::{GlobalSignal, Owner, Readable}; +use dioxus_signals::{GlobalSignal, Owner, ReadableExt}; use tracing::error; diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index ba559568d1..00583372b7 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -1,7 +1,7 @@ use crate::{provide_router_context, routable::Routable, router_cfg::RouterConfig, Outlet}; use dioxus_core::{provide_context, use_hook, Callback, Element}; use dioxus_core_macro::{rsx, Props}; -use dioxus_signals::{GlobalSignal, Owner, Readable}; +use dioxus_signals::{GlobalSignal, Owner, ReadableExt}; /// The props for [`Router`]. #[derive(Props)] diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index a93f382d34..54db4998cf 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -7,7 +7,7 @@ use std::{ use dioxus_core::{provide_context, Element, ReactiveContext, ScopeId}; use dioxus_history::history; -use dioxus_signals::{CopyValue, Readable, Signal, Writable}; +use dioxus_signals::{CopyValue, ReadableExt, Signal, Writable}; use crate::{ components::child_router::consume_child_route_mapping, navigation::NavigationTarget, diff --git a/packages/server/src/document.rs b/packages/server/src/document.rs index 5eb566cafe..ee404fe410 100644 --- a/packages/server/src/document.rs +++ b/packages/server/src/document.rs @@ -10,7 +10,7 @@ use dioxus_document::{ Document, Eval, LinkProps, MetaProps, NoOpDocument, ScriptProps, StyleProps, }; use dioxus_html as dioxus_elements; -use dioxus_signals::{GlobalSignal, Readable}; +use dioxus_signals::{GlobalSignal, ReadableExt}; use dioxus_ssr::Renderer; use parking_lot::RwLock; use std::sync::LazyLock; diff --git a/packages/signals/README.md b/packages/signals/README.md index 7a1f39f0d1..5ec2256ddf 100644 --- a/packages/signals/README.md +++ b/packages/signals/README.md @@ -119,7 +119,7 @@ fn App() -> Element { } #[component] -fn Child(signal: ReadOnlySignal) -> Element { +fn Child(signal: ReadSignal) -> Element { rsx! { "{signal}" } diff --git a/packages/signals/examples/map_signal.rs b/packages/signals/examples/map_signal.rs index f8a5cd681c..9b846b5fcd 100644 --- a/packages/signals/examples/map_signal.rs +++ b/packages/signals/examples/map_signal.rs @@ -27,24 +27,16 @@ fn app() -> Element { } for i in 0..vec.len() { - Child { count: vec.map(move |v| &v[i]) } + Child { count: vec.map_mut(move |v| &v[i], move |v| &mut v[i]) } } } } #[component] -fn Child(count: MappedSignal) -> Element { - use_memo({ - to_owned![count]; - move || { - let value = count.read(); - println!("Child value: {value}"); - } - }); - +fn Child(count: WriteSignal) -> Element { rsx! { div { - "Child: {count}" + "{count}" } } } diff --git a/packages/signals/examples/read_only_degrade.rs b/packages/signals/examples/read_only_degrade.rs index 52e6efe889..9b43abf319 100644 --- a/packages/signals/examples/read_only_degrade.rs +++ b/packages/signals/examples/read_only_degrade.rs @@ -1,4 +1,4 @@ -//! Signals can degrade into a ReadOnlySignal variant automatically +//! Signals can degrade into a Read variant automatically //! This is done thanks to a conversion by the #[component] macro use dioxus::prelude::*; @@ -22,7 +22,7 @@ fn app() -> Element { } #[component] -fn Child(count: ReadOnlySignal, children: Element) -> Element { +fn Child(count: ReadSignal, children: Element) -> Element { rsx! { div { "Count: {count}" } {children} diff --git a/packages/signals/examples/selector.rs b/packages/signals/examples/selector.rs index 6e282cbc55..063ed6a7c6 100644 --- a/packages/signals/examples/selector.rs +++ b/packages/signals/examples/selector.rs @@ -18,6 +18,6 @@ fn app() -> Element { } #[component] -fn Child(signal: ReadOnlySignal) -> Element { +fn Child(signal: ReadSignal) -> Element { rsx! { "{signal}" } } diff --git a/packages/signals/src/boxed.rs b/packages/signals/src/boxed.rs new file mode 100644 index 0000000000..d257941019 --- /dev/null +++ b/packages/signals/src/boxed.rs @@ -0,0 +1,381 @@ +use std::{any::Any, ops::Deref}; + +use dioxus_core::{IntoAttributeValue, IntoDynNode, Subscribers}; +use generational_box::{BorrowResult, UnsyncStorage}; + +use crate::{ + read_impls, write_impls, CopyValue, Global, InitializeFromFunction, MappedMutSignal, + MappedSignal, Memo, Readable, ReadableExt, ReadableRef, Signal, Writable, WritableExt, +}; + +/// A signal that can only be read from. +pub type ReadOnlySignal = ReadSignal; + +/// A boxed version of [Readable] that can be used to store any readable type. +pub struct ReadSignal { + value: CopyValue>>, +} + +impl ReadSignal { + /// Create a new boxed readable value. + pub fn new(value: impl Readable + 'static) -> Self { + Self { + value: CopyValue::new(Box::new(value)), + } + } + + /// Point to another [ReadSignal]. This will subscribe the other [ReadSignal] to all subscribers of this [ReadSignal]. + pub fn point_to(&self, other: Self) -> BorrowResult + where + T: Sized + 'static, + { + #[allow(clippy::mutable_key_type)] + let this_subscribers = self.subscribers(); + let other_subscribers = other.subscribers(); + if let (Some(this_subscribers), Some(other_subscribers)) = + (this_subscribers, other_subscribers) + { + this_subscribers.visit(|subscriber| { + subscriber.subscribe(other_subscribers.clone()); + }); + } + self.value.point_to(other.value) + } + + #[doc(hidden)] + /// This is only used by the `props` macro. + /// Mark any readers of the signal as dirty + pub fn mark_dirty(&mut self) { + let subscribers = self.value.subscribers(); + if let Some(subscribers) = subscribers { + subscribers.visit(|subscriber| { + subscriber.mark_dirty(); + }); + } + } +} + +impl Clone for ReadSignal { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ReadSignal {} + +impl PartialEq for ReadSignal { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Default for ReadSignal { + fn default() -> Self { + Self::new(Signal::new(T::default())) + } +} + +read_impls!(ReadSignal); + +impl IntoAttributeValue for ReadSignal +where + T: Clone + IntoAttributeValue + 'static, +{ + fn into_value(self) -> dioxus_core::AttributeValue { + self.with(|f| f.clone().into_value()) + } +} + +impl IntoDynNode for ReadSignal +where + T: Clone + IntoDynNode + 'static, +{ + fn into_dyn_node(self) -> dioxus_core::DynamicNode { + self.with(|f| f.clone().into_dyn_node()) + } +} + +impl Deref for ReadSignal { + type Target = dyn Fn() -> T; + + fn deref(&self) -> &Self::Target { + unsafe { ReadableExt::deref_impl(self) } + } +} + +impl Readable for ReadSignal { + type Target = T; + type Storage = UnsyncStorage; + + #[track_caller] + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + self.value + .try_peek_unchecked() + .unwrap() + .try_read_unchecked() + } + + #[track_caller] + fn try_peek_unchecked(&self) -> BorrowResult> { + self.value + .try_peek_unchecked() + .unwrap() + .try_peek_unchecked() + } + + fn subscribers(&self) -> Option { + self.value.subscribers() + } +} + +// We can't implement From + 'static> for ReadSignal +// because it would conflict with the From for T implementation, but we can implement it for +// all specific readable types +impl From> for ReadSignal { + fn from(value: Signal) -> Self { + Self::new(value) + } +} +impl From> for ReadSignal { + fn from(value: Memo) -> Self { + Self::new(value) + } +} +impl From> for ReadSignal { + fn from(value: CopyValue) -> Self { + Self::new(value) + } +} +impl From> for ReadSignal +where + T: Readable + InitializeFromFunction, +{ + fn from(value: Global) -> Self { + Self::new(value) + } +} +impl From> for ReadSignal +where + O: ?Sized, + V: Readable + 'static, + F: Fn(&V::Target) -> &O + 'static, +{ + fn from(value: MappedSignal) -> Self { + Self::new(value) + } +} +impl From> for ReadSignal +where + O: ?Sized, + V: Readable + 'static, + F: Fn(&V::Target) -> &O + 'static, + FMut: 'static, +{ + fn from(value: MappedMutSignal) -> Self { + Self::new(value) + } +} + +/// A boxed version of [Writable] that can be used to store any writable type. +pub struct WriteSignal { + value: CopyValue< + Box>>, + >, +} + +impl WriteSignal { + /// Create a new boxed writable value. + pub fn new( + value: impl Writable + 'static, + ) -> Self { + Self { + value: CopyValue::new(Box::new(BoxWriteMetadata::new(value))), + } + } +} + +struct BoxWriteMetadata { + value: W, +} + +impl BoxWriteMetadata { + fn new(value: W) -> Self { + Self { value } + } +} + +impl Readable for BoxWriteMetadata { + type Target = W::Target; + + type Storage = W::Storage; + + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + self.value.try_read_unchecked() + } + + fn try_peek_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + self.value.try_peek_unchecked() + } + + fn subscribers(&self) -> Option { + self.value.subscribers() + } +} + +impl Writable for BoxWriteMetadata { + type WriteMetadata = Box; + + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> { + self.value + .try_write_unchecked() + .map(|w| w.map_metadata(|data| Box::new(data) as Box)) + } + + fn write(&mut self) -> crate::WritableRef<'_, Self> { + self.value + .write() + .map_metadata(|data| Box::new(data) as Box) + } + + fn try_write( + &mut self, + ) -> Result, generational_box::BorrowMutError> { + self.value + .try_write() + .map(|w| w.map_metadata(|data| Box::new(data) as Box)) + } + + fn write_unchecked(&self) -> crate::WritableRef<'static, Self> { + self.value + .write_unchecked() + .map_metadata(|data| Box::new(data) as Box) + } +} + +impl Clone for WriteSignal { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for WriteSignal {} + +impl PartialEq for WriteSignal { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +read_impls!(WriteSignal); +write_impls!(WriteSignal); + +impl IntoAttributeValue for WriteSignal +where + T: Clone + IntoAttributeValue + 'static, +{ + fn into_value(self) -> dioxus_core::AttributeValue { + self.with(|f| f.clone().into_value()) + } +} + +impl IntoDynNode for WriteSignal +where + T: Clone + IntoDynNode + 'static, +{ + fn into_dyn_node(self) -> dioxus_core::DynamicNode { + self.with(|f| f.clone().into_dyn_node()) + } +} + +impl Deref for WriteSignal { + type Target = dyn Fn() -> T; + + fn deref(&self) -> &Self::Target { + unsafe { ReadableExt::deref_impl(self) } + } +} + +impl Readable for WriteSignal { + type Target = T; + type Storage = UnsyncStorage; + + #[track_caller] + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + self.value + .try_peek_unchecked() + .unwrap() + .try_read_unchecked() + } + + #[track_caller] + fn try_peek_unchecked(&self) -> BorrowResult> { + self.value + .try_peek_unchecked() + .unwrap() + .try_peek_unchecked() + } + + fn subscribers(&self) -> Option { + self.value.subscribers() + } +} + +impl Writable for WriteSignal { + type WriteMetadata = Box; + + fn write_unchecked(&self) -> crate::WritableRef<'static, Self> { + self.value.try_peek_unchecked().unwrap().write_unchecked() + } + + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> { + self.value + .try_peek_unchecked() + .unwrap() + .try_write_unchecked() + } +} + +// We can't implement From + 'static> for Write +// because it would conflict with the From for T implementation, but we can implement it for +// all specific readable types +impl From> for WriteSignal { + fn from(value: Signal) -> Self { + Self::new(value) + } +} +impl From> for WriteSignal { + fn from(value: CopyValue) -> Self { + Self::new(value) + } +} +impl From> for WriteSignal +where + T: Writable + InitializeFromFunction, +{ + fn from(value: Global) -> Self { + Self::new(value) + } +} +impl From> for WriteSignal +where + O: ?Sized, + V: Writable + 'static, + F: Fn(&V::Target) -> &O + 'static, + FMut: Fn(&mut V::Target) -> &mut O + 'static, +{ + fn from(value: MappedMutSignal) -> Self { + Self::new(value) + } +} diff --git a/packages/signals/src/copy_value.rs b/packages/signals/src/copy_value.rs index 54a7fb9a0d..b53543d776 100644 --- a/packages/signals/src/copy_value.rs +++ b/packages/signals/src/copy_value.rs @@ -1,16 +1,19 @@ #![allow(clippy::unnecessary_operation)] #![allow(clippy::no_effect)] +use dioxus_core::Subscribers; use dioxus_core::{current_owner, current_scope_id, ScopeId}; use generational_box::{BorrowResult, GenerationalBox, GenerationalBoxId, Storage, UnsyncStorage}; use std::ops::Deref; use crate::read_impls; use crate::Readable; +use crate::ReadableExt; use crate::ReadableRef; use crate::Writable; use crate::WritableRef; -use crate::{default_impl, write_impls}; +use crate::WriteLock; +use crate::{default_impl, write_impls, WritableExt}; /// CopyValue is a wrapper around a value to make the value mutable and Copy. /// @@ -151,43 +154,21 @@ impl> Readable for CopyValue { crate::warnings::copy_value_hoisted(self, std::panic::Location::caller()); self.value.try_read() } -} - -impl> Writable for CopyValue { - type Mut<'a, R: ?Sized + 'static> = S::Mut<'a, R>; - fn map_mut &mut U>( - mut_: Self::Mut<'_, I>, - f: F, - ) -> Self::Mut<'_, U> { - S::map_mut(mut_, f) - } - - fn try_map_mut Option<&mut U>>( - mut_: Self::Mut<'_, I>, - f: F, - ) -> Option> { - S::try_map_mut(mut_, f) + fn subscribers(&self) -> Option { + None } +} - fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>( - mut_: Self::Mut<'a, R>, - ) -> Self::Mut<'b, R> { - S::downcast_lifetime_mut(mut_) - } +impl> Writable for CopyValue { + type WriteMetadata = (); #[track_caller] fn try_write_unchecked( &self, ) -> Result, generational_box::BorrowMutError> { crate::warnings::copy_value_hoisted(self, std::panic::Location::caller()); - self.value.try_write() - } - - #[track_caller] - fn set(&mut self, value: T) { - crate::warnings::copy_value_hoisted(self, std::panic::Location::caller()); - self.value.set(value); + self.value.try_write().map(WriteLock::new) } } @@ -202,7 +183,7 @@ impl> Deref for CopyValue { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } diff --git a/packages/signals/src/global/memo.rs b/packages/signals/src/global/memo.rs index cdc7759285..5d67694962 100644 --- a/packages/signals/src/global/memo.rs +++ b/packages/signals/src/global/memo.rs @@ -1,5 +1,5 @@ use super::{Global, InitializeFromFunction}; -use crate::read::Readable; +use crate::read::ReadableExt; use crate::read_impls; use crate::Memo; diff --git a/packages/signals/src/global/mod.rs b/packages/signals/src/global/mod.rs index d5b8091d56..b6f316e295 100644 --- a/packages/signals/src/global/mod.rs +++ b/packages/signals/src/global/mod.rs @@ -1,4 +1,4 @@ -use dioxus_core::ScopeId; +use dioxus_core::{ScopeId, Subscribers}; use generational_box::BorrowResult; use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc}; @@ -8,7 +8,7 @@ pub use memo::*; mod signal; pub use signal::*; -use crate::{Readable, ReadableRef, Signal, Writable, WritableRef}; +use crate::{Readable, ReadableExt, ReadableRef, Signal, Writable, WritableExt, WritableRef}; /// A trait for an item that can be constructed from an initialization function pub trait InitializeFromFunction { @@ -39,7 +39,7 @@ where type Target = dyn Fn() -> R; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } @@ -61,37 +61,17 @@ where fn try_peek_unchecked(&self) -> BorrowResult> { self.resolve().try_peek_unchecked() } + + fn subscribers(&self) -> Option { + self.resolve().subscribers() + } } impl Writable for Global where T: Writable + InitializeFromFunction, { - type Mut<'a, Read: ?Sized + 'static> = T::Mut<'a, Read>; - - fn map_mut &mut U>( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Self::Mut<'_, U> { - T::map_mut(ref_, f) - } - - fn try_map_mut< - I: ?Sized + 'static, - U: ?Sized + 'static, - F: FnOnce(&mut I) -> Option<&mut U>, - >( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Option> { - T::try_map_mut(ref_, f) - } - - fn downcast_lifetime_mut<'a: 'b, 'b, Read: ?Sized + 'static>( - mut_: Self::Mut<'a, Read>, - ) -> Self::Mut<'b, Read> { - T::downcast_lifetime_mut(mut_) - } + type WriteMetadata = T::WriteMetadata; #[track_caller] fn try_write_unchecked( @@ -106,7 +86,7 @@ where T: Writable + InitializeFromFunction, { /// Write this value - pub fn write(&self) -> T::Mut<'static, R> { + pub fn write(&self) -> WritableRef<'static, T, R> { self.resolve().try_write_unchecked().unwrap() } diff --git a/packages/signals/src/global/signal.rs b/packages/signals/src/global/signal.rs index 66063f4142..b136ff2617 100644 --- a/packages/signals/src/global/signal.rs +++ b/packages/signals/src/global/signal.rs @@ -1,5 +1,5 @@ use super::{Global, InitializeFromFunction}; -use crate::read::Readable; +use crate::read::ReadableExt; use crate::read_impls; use crate::Signal; diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 173e490235..560d472905 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -4,6 +4,7 @@ /// ```rust /// use generational_box::*; /// use dioxus::prelude::*; +/// use dioxus_core::Subscribers; /// /// struct MyCopyValue> { /// value: CopyValue, @@ -30,6 +31,10 @@ /// ) -> Result, generational_box::BorrowError> { /// self.value.try_read_unchecked() /// } +/// +/// fn subscribers(&self) -> Option { +/// self.value.subscribers() +/// } /// } /// /// default_impl!(MyCopyValue>); @@ -72,6 +77,7 @@ macro_rules! default_impl { /// ```rust /// use generational_box::*; /// use dioxus::prelude::*; +/// use dioxus_core::Subscribers; /// /// struct MyCopyValue> { /// value: CopyValue, @@ -92,6 +98,10 @@ macro_rules! default_impl { /// ) -> Result, generational_box::BorrowError> { /// self.value.try_read_unchecked() /// } +/// +/// fn subscribers(&self) -> Option { +/// self.value.subscribers() +/// } /// } /// /// read_impls!(MyCopyValue>); @@ -145,6 +155,7 @@ macro_rules! read_impls { /// ```rust /// use generational_box::*; /// use dioxus::prelude::*; +/// use dioxus_core::Subscribers; /// /// struct MyCopyValue> { /// value: CopyValue, @@ -165,6 +176,10 @@ macro_rules! read_impls { /// ) -> Result, generational_box::BorrowError> { /// self.value.try_read_unchecked() /// } +/// +/// fn subscribers(&self) -> Option { +/// self.value.subscribers() +/// } /// } /// /// fmt_impls!(MyCopyValue>); @@ -221,6 +236,7 @@ macro_rules! fmt_impls { /// ```rust /// use generational_box::*; /// use dioxus::prelude::*; +/// use dioxus_core::Subscribers; /// /// struct MyCopyValue> { /// value: CopyValue, @@ -241,6 +257,10 @@ macro_rules! fmt_impls { /// ) -> Result, generational_box::BorrowError> { /// self.value.try_read_unchecked() /// } +/// +/// fn subscribers(&self) -> Option { +/// self.value.subscribers() +/// } /// } /// /// eq_impls!(MyCopyValue>); @@ -307,8 +327,7 @@ macro_rules! eq_impls { /// &self, /// ) -> Result, generational_box::BorrowMutError> { /// self.value.try_write_unchecked() -/// -/// } +/// } /// /// //... /// } @@ -332,6 +351,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::Add for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { type Output = T; @@ -345,6 +368,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::AddAssign for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { #[track_caller] fn add_assign(&mut self, rhs: T) { @@ -356,6 +383,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::SubAssign for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { #[track_caller] fn sub_assign(&mut self, rhs: T) { @@ -367,6 +398,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::Sub for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { type Output = T; @@ -380,6 +415,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::MulAssign for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { #[track_caller] fn mul_assign(&mut self, rhs: T) { @@ -391,6 +430,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::Mul for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { type Output = T; @@ -404,6 +447,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::DivAssign for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { #[track_caller] fn div_assign(&mut self, rhs: T) { @@ -415,6 +462,10 @@ macro_rules! write_impls { $(, $gen $(: $gen_bound)?)* > std::ops::Div for $ty + $( + where + $($extra_bound_ty: $extra_bound,)* + )? { type Output = T; diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index b8b6a75747..926cba75d9 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -10,12 +10,12 @@ pub use copy_value::*; pub(crate) mod signal; pub use signal::*; -mod read_only_signal; -pub use read_only_signal::*; - mod map; pub use map::*; +mod map_mut; +pub use map_mut::*; + mod set_compare; pub use set_compare::*; @@ -41,3 +41,6 @@ mod props; pub use props::*; pub mod warnings; + +mod boxed; +pub use boxed::*; diff --git a/packages/signals/src/map.rs b/packages/signals/src/map.rs index bf4b2c3ab0..1e9dfdd2ad 100644 --- a/packages/signals/src/map.rs +++ b/packages/signals/src/map.rs @@ -1,94 +1,114 @@ -use crate::{read::Readable, read_impls, ReadableRef}; -use dioxus_core::IntoAttributeValue; -use generational_box::{AnyStorage, BorrowResult, UnsyncStorage}; -use std::{ops::Deref, rc::Rc}; +use crate::{read::Readable, read_impls, ReadableExt, ReadableRef}; +use dioxus_core::{IntoAttributeValue, Subscribers}; +use generational_box::{AnyStorage, BorrowResult}; +use std::ops::Deref; /// A read only signal that has been mapped to a new type. -pub struct MappedSignal { - try_read: Rc Result, generational_box::BorrowError> + 'static>, - try_peek: Rc Result, generational_box::BorrowError> + 'static>, +pub struct MappedSignal::Target) -> &O> { + value: V, + map_fn: F, + _marker: std::marker::PhantomData, } -impl Clone for MappedSignal { +impl Clone for MappedSignal +where + V: Readable + Clone, + F: Clone, +{ fn clone(&self) -> Self { MappedSignal { - try_read: self.try_read.clone(), - try_peek: self.try_peek.clone(), + value: self.value.clone(), + map_fn: self.map_fn.clone(), + _marker: std::marker::PhantomData, } } } -impl MappedSignal +impl Copy for MappedSignal +where + V: Readable + Copy, + F: Copy, +{ +} + +impl MappedSignal where O: ?Sized, - S: AnyStorage, + V: Readable, + F: Fn(&V::Target) -> &O, { /// Create a new mapped signal. - pub(crate) fn new( - try_read: Rc< - dyn Fn() -> Result, generational_box::BorrowError> + 'static, - >, - try_peek: Rc< - dyn Fn() -> Result, generational_box::BorrowError> + 'static, - >, - ) -> Self { - MappedSignal { try_read, try_peek } + pub(crate) fn new(value: V, map_fn: F) -> Self { + MappedSignal { + value, + map_fn, + _marker: std::marker::PhantomData, + } } } -impl Readable for MappedSignal +impl Readable for MappedSignal where O: ?Sized, - S: AnyStorage, + V: Readable, + F: Fn(&V::Target) -> &O, { type Target = O; - type Storage = S; + type Storage = V::Storage; fn try_read_unchecked( &self, ) -> Result, generational_box::BorrowError> { - (self.try_read)() + let value = self.value.try_read_unchecked()?; + Ok(V::Storage::map(value, |v| (self.map_fn)(v))) } fn try_peek_unchecked(&self) -> BorrowResult> { - (self.try_peek)() + let value = self.value.try_peek_unchecked()?; + Ok(V::Storage::map(value, |v| (self.map_fn)(v))) + } + + fn subscribers(&self) -> Option { + self.value.subscribers() } } -impl IntoAttributeValue for MappedSignal +impl IntoAttributeValue for MappedSignal where O: Clone + IntoAttributeValue, - S: AnyStorage, + V: Readable, + F: Fn(&V::Target) -> &O, { fn into_value(self) -> dioxus_core::AttributeValue { self.with(|f| f.clone().into_value()) } } -impl PartialEq for MappedSignal +impl PartialEq for MappedSignal where O: ?Sized, - S: AnyStorage, + V: Readable + PartialEq, + F: PartialEq, { fn eq(&self, other: &Self) -> bool { - std::ptr::eq(&self.try_peek, &other.try_peek) - && std::ptr::eq(&self.try_read, &other.try_read) + self.value == other.value && self.map_fn == other.map_fn } } /// Allow calling a signal with signal() syntax /// -/// Currently only limited to copy types, though could probably specialize for string/arc/rc -impl Deref for MappedSignal +/// Currently only limited to clone types, though could probably specialize for string/arc/rc +impl Deref for MappedSignal where O: Clone, - S: AnyStorage + 'static, + V: Readable + 'static, + F: Fn(&V::Target) -> &O + 'static, { type Target = dyn Fn() -> O; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } -read_impls!(MappedSignal); +read_impls!(MappedSignal where V: Readable, F: Fn(&V::Target) -> &T); diff --git a/packages/signals/src/map_mut.rs b/packages/signals/src/map_mut.rs new file mode 100644 index 0000000000..2e845a4458 --- /dev/null +++ b/packages/signals/src/map_mut.rs @@ -0,0 +1,150 @@ +use std::ops::Deref; + +use crate::{ + read_impls, write_impls, Readable, ReadableExt, ReadableRef, Writable, WritableExt, + WritableRef, WriteLock, +}; +use dioxus_core::{IntoAttributeValue, Subscribers}; +use generational_box::{AnyStorage, BorrowResult}; + +/// A read only signal that has been mapped to a new type. +pub struct MappedMutSignal< + O: ?Sized + 'static, + V: Readable, + F = fn(&::Target) -> &O, + FMut = fn(&mut ::Target) -> &mut O, +> { + value: V, + map_fn: F, + map_fn_mut: FMut, + _marker: std::marker::PhantomData, +} + +impl Clone for MappedMutSignal +where + V: Readable + Clone, + F: Clone, + FMut: Clone, +{ + fn clone(&self) -> Self { + MappedMutSignal { + value: self.value.clone(), + map_fn: self.map_fn.clone(), + map_fn_mut: self.map_fn_mut.clone(), + _marker: std::marker::PhantomData, + } + } +} + +impl Copy for MappedMutSignal +where + V: Readable + Copy, + F: Copy, + FMut: Copy, +{ +} + +impl MappedMutSignal +where + O: ?Sized, + V: Readable, + F: Fn(&V::Target) -> &O, +{ + /// Create a new mapped signal. + pub(crate) fn new(value: V, map_fn: F, map_fn_mut: FMut) -> Self { + MappedMutSignal { + value, + map_fn, + map_fn_mut, + _marker: std::marker::PhantomData, + } + } +} + +impl Readable for MappedMutSignal +where + O: ?Sized, + V: Readable, + F: Fn(&V::Target) -> &O, +{ + type Target = O; + type Storage = V::Storage; + + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + let value = self.value.try_read_unchecked()?; + Ok(V::Storage::map(value, |v| (self.map_fn)(v))) + } + + fn try_peek_unchecked(&self) -> BorrowResult> { + let value = self.value.try_peek_unchecked()?; + Ok(V::Storage::map(value, |v| (self.map_fn)(v))) + } + + fn subscribers(&self) -> Option { + self.value.subscribers() + } +} + +impl Writable for MappedMutSignal +where + O: ?Sized, + V: Writable, + F: Fn(&V::Target) -> &O, + FMut: Fn(&mut V::Target) -> &mut O, +{ + type WriteMetadata = V::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> { + let value = self.value.try_write_unchecked()?; + Ok(WriteLock::map(value, |v| (self.map_fn_mut)(v))) + } +} + +impl IntoAttributeValue for MappedMutSignal +where + O: Clone + IntoAttributeValue, + V: Readable, + F: Fn(&V::Target) -> &O, +{ + fn into_value(self) -> dioxus_core::AttributeValue { + self.with(|f| f.clone().into_value()) + } +} + +impl PartialEq for MappedMutSignal +where + O: ?Sized, + V: Readable + PartialEq, + F: PartialEq, + FMut: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.value == other.value + && self.map_fn == other.map_fn + && self.map_fn_mut == other.map_fn_mut + } +} + +/// Allow calling a signal with signal() syntax +/// +/// Currently only limited to clone types, though could probably specialize for string/arc/rc +impl Deref for MappedMutSignal +where + O: Clone, + V: Readable + 'static, + F: Fn(&V::Target) -> &O + 'static, + FMut: 'static, +{ + type Target = dyn Fn() -> O; + + fn deref(&self) -> &Self::Target { + unsafe { ReadableExt::deref_impl(self) } + } +} + +read_impls!(MappedMutSignal where V: Readable, F: Fn(&V::Target) -> &T); +write_impls!(MappedMutSignal where V: Writable, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T); diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 953d2cb7d6..21bc982051 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,7 +1,7 @@ use crate::write::Writable; +use crate::CopyValue; use crate::{read::Readable, ReadableRef, Signal}; -use crate::{read_impls, GlobalMemo}; -use crate::{CopyValue, ReadOnlySignal}; +use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt}; use std::{ cell::RefCell, ops::Deref, @@ -10,6 +10,7 @@ use std::{ use dioxus_core::{ current_scope_id, spawn_isomorphic, IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, + Subscribers, }; use futures_util::StreamExt; use generational_box::{AnyStorage, BorrowResult, UnsyncStorage}; @@ -28,15 +29,6 @@ pub struct Memo { update: CopyValue>, } -impl From> for ReadOnlySignal -where - T: PartialEq, -{ - fn from(val: Memo) -> Self { - ReadOnlySignal::new(val.inner) - } -} - impl Memo { /// Create a new memo #[track_caller] @@ -203,6 +195,10 @@ where fn try_peek_unchecked(&self) -> BorrowResult> { self.inner.try_peek_unchecked() } + + fn subscribers(&self) -> Option { + self.inner.subscribers() + } } impl IntoAttributeValue for Memo @@ -236,7 +232,7 @@ where type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } diff --git a/packages/signals/src/props.rs b/packages/signals/src/props.rs index fc004089a0..3a7f17c1f2 100644 --- a/packages/signals/src/props.rs +++ b/packages/signals/src/props.rs @@ -1,15 +1,15 @@ -use crate::{ReadOnlySignal, Signal}; +use crate::{ReadSignal, Signal}; use dioxus_core::SuperFrom; #[doc(hidden)] -pub struct SignalFromMarker(std::marker::PhantomData); +pub struct ReadFromMarker(std::marker::PhantomData); -impl SuperFrom> for ReadOnlySignal +impl SuperFrom> for ReadSignal where - O: SuperFrom, + O: SuperFrom + 'static, { fn super_from(input: T) -> Self { - ReadOnlySignal::new(Signal::new(O::super_from(input))) + ReadSignal::new(Signal::new(O::super_from(input))) } } @@ -17,9 +17,9 @@ where #[allow(unused)] fn into_signal_compiles() { use dioxus_core::SuperInto; - fn takes_signal_string(_: impl SuperInto, M>) {} + fn takes_signal_string(_: impl SuperInto, M>) {} - fn takes_option_signal_string(_: impl SuperInto>, M>) {} + fn takes_option_signal_string(_: impl SuperInto>, M>) {} fn don_t_run() { takes_signal_string("hello world"); diff --git a/packages/signals/src/read.rs b/packages/signals/src/read.rs index 3d50d2aee9..9f8bb75621 100644 --- a/packages/signals/src/read.rs +++ b/packages/signals/src/read.rs @@ -1,15 +1,16 @@ -use std::{mem::MaybeUninit, ops::Index, rc::Rc}; +use std::{mem::MaybeUninit, ops::Index}; -use generational_box::AnyStorage; +use dioxus_core::Subscribers; +use generational_box::{AnyStorage, UnsyncStorage}; -use crate::MappedSignal; +use crate::{MappedSignal, ReadSignal}; /// A reference to a value that can be read from. #[allow(type_alias_bounds)] pub type ReadableRef<'a, T: Readable, O = ::Target> = ::Ref<'a, O>; -/// A trait for states that can be read from like [`crate::Signal`], [`crate::GlobalSignal`], or [`crate::ReadOnlySignal`]. You may choose to accept this trait as a parameter instead of the concrete type to allow for more flexibility in your API. For example, instead of creating two functions, one that accepts a [`crate::Signal`] and one that accepts a [`crate::GlobalSignal`], you can create one function that accepts a [`Readable`] type. +/// A trait for states that can be read from like [`crate::Signal`], [`crate::GlobalSignal`], or [`crate::ReadSignal`]. You may choose to accept this trait as a parameter instead of the concrete type to allow for more flexibility in your API. For example, instead of creating two functions, one that accepts a [`crate::Signal`] and one that accepts a [`crate::GlobalSignal`], you can create one function that accepts a [`Readable`] type. /// /// # Example /// ```rust @@ -21,7 +22,7 @@ pub type ReadableRef<'a, T: Readable, O = ::Target> = /// static COUNT: GlobalSignal = Signal::global(|| 0); /// /// fn MyComponent(count: Signal) -> Element { -/// // Since we defined the function in terms of the readable trait, we can use it with any readable type (Signal, GlobalSignal, ReadOnlySignal, etc) +/// // Since we defined the function in terms of the readable trait, we can use it with any readable type (Signal, GlobalSignal, ReadSignal, etc) /// let doubled = use_memo(move || double(&count)); /// let global_count_doubled = use_memo(|| double(&COUNT)); /// rsx! { @@ -41,64 +42,27 @@ pub trait Readable { /// The type of the storage this readable uses. type Storage: AnyStorage; - /// Map the readable type to a new type. This lets you provide a view into a readable type without needing to clone the inner value. - /// - /// Anything that subscribes to the readable value will be rerun whenever the original value changes, even if the view does not change. If you want to memorize the view, you can use a [`crate::Memo`] instead. + /// Try to get a reference to the value without checking the lifetime. This will subscribe the current scope to the signal. /// - /// # Example - /// ```rust - /// # use dioxus::prelude::*; - /// fn List(list: Signal>) -> Element { - /// rsx! { - /// for index in 0..list.len() { - /// // We can use the `map` method to provide a view into the single item in the list that the child component will render - /// Item { item: list.map(move |v| &v[index]) } - /// } - /// } - /// } + /// NOTE: This method is completely safe because borrow checking is done at runtime. + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError>; + + /// Try to peek the current value of the signal without subscribing to updates. If the value has + /// been dropped, this will return an error. /// - /// // The child component doesn't need to know that the mapped value is coming from a list - /// #[component] - /// fn Item(item: MappedSignal) -> Element { - /// rsx! { - /// div { "Item: {item}" } - /// } - /// } - /// ``` - fn map(self, f: impl Fn(&Self::Target) -> &O + 'static) -> MappedSignal - where - Self: Clone + Sized + 'static, - { - let mapping = Rc::new(f); - let try_read = Rc::new({ - let self_ = self.clone(); - let mapping = mapping.clone(); - move || { - self_ - .try_read_unchecked() - .map(|ref_| ::map(ref_, |r| mapping(r))) - } - }) - as Rc< - dyn Fn() -> Result, generational_box::BorrowError> - + 'static, - >; - let try_peek = Rc::new({ - let self_ = self.clone(); - let mapping = mapping.clone(); - move || { - self_ - .try_peek_unchecked() - .map(|ref_| ::map(ref_, |r| mapping(r))) - } - }) - as Rc< - dyn Fn() -> Result, generational_box::BorrowError> - + 'static, - >; - MappedSignal::new(try_read, try_peek) - } + /// NOTE: This method is completely safe because borrow checking is done at runtime. + fn try_peek_unchecked( + &self, + ) -> Result, generational_box::BorrowError>; + /// Get the underlying subscriber list for this readable. This is used to track when the value changes and notify subscribers. + fn subscribers(&self) -> Option; +} + +/// An extension trait for `Readable` types that provides some convenience methods. +pub trait ReadableExt: Readable { /// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. /// If the value has been dropped, this will panic. Calling this on a Signal is the same as /// using the signal() syntax to read and subscribe to its value @@ -122,13 +86,6 @@ pub trait Readable { self.try_read_unchecked().unwrap() } - /// Try to get a reference to the value without checking the lifetime. This will subscribe the current scope to the signal. - /// - /// NOTE: This method is completely safe because borrow checking is done at runtime. - fn try_read_unchecked( - &self, - ) -> Result, generational_box::BorrowError>; - /// Get the current value of the state without subscribing to updates. If the value has been dropped, this will panic. /// /// # Example @@ -184,13 +141,37 @@ pub trait Readable { self.try_peek_unchecked().unwrap() } - /// Try to peek the current value of the signal without subscribing to updates. If the value has - /// been dropped, this will return an error. + /// Map the readable type to a new type. This lets you provide a view into a readable type without needing to clone the inner value. /// - /// NOTE: This method is completely safe because borrow checking is done at runtime. - fn try_peek_unchecked( - &self, - ) -> Result, generational_box::BorrowError>; + /// Anything that subscribes to the readable value will be rerun whenever the original value changes, even if the view does not change. If you want to memorize the view, you can use a [`crate::Memo`] instead. + /// + /// # Example + /// ```rust + /// # use dioxus::prelude::*; + /// fn List(list: Signal>) -> Element { + /// rsx! { + /// for index in 0..list.len() { + /// // We can use the `map` method to provide a view into the single item in the list that the child component will render + /// Item { item: list.map(move |v| &v[index]) } + /// } + /// } + /// } + /// + /// // The child component doesn't need to know that the mapped value is coming from a list + /// #[component] + /// fn Item(item: ReadSignal) -> Element { + /// rsx! { + /// div { "Item: {item}" } + /// } + /// } + /// ``` + fn map(self, f: F) -> MappedSignal + where + Self: Clone + Sized + 'static, + F: Fn(&Self::Target) -> &O + 'static, + { + MappedSignal::new(self, f) + } /// Clone the inner value and return it. If the value has been dropped, this will panic. #[track_caller] @@ -263,6 +244,19 @@ pub trait Readable { } } +impl ReadableExt for R {} + +/// An extension trait for `Readable` types that can be boxed into a trait object. +pub trait ReadableBoxExt: Readable { + /// Box the readable value into a trait object. This is useful for passing around readable values without knowing their concrete type. + fn boxed(self) -> ReadSignal + where + Self: Sized + 'static, + { + ReadSignal::new(self) + } +} + /// An extension trait for `Readable>` that provides some convenience methods. pub trait ReadableVecExt: Readable> { /// Returns the length of the inner vector. diff --git a/packages/signals/src/read_only_signal.rs b/packages/signals/src/read_only_signal.rs deleted file mode 100644 index 668aedc04a..0000000000 --- a/packages/signals/src/read_only_signal.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::{read::Readable, ReadableRef, Signal, SignalData}; -use dioxus_core::IntoDynNode; -use std::ops::Deref; - -use crate::{default_impl, read_impls}; -use dioxus_core::{IntoAttributeValue, ScopeId}; -use generational_box::{BorrowResult, Storage, UnsyncStorage}; - -/// A signal that can only be read from. -pub struct ReadOnlySignal> = UnsyncStorage> { - inner: Signal, -} - -/// A signal that can only be read from. -pub type ReadSignal = ReadOnlySignal; - -impl>> From> for ReadOnlySignal { - fn from(inner: Signal) -> Self { - Self { inner } - } -} - -impl ReadOnlySignal { - /// Create a new read-only signal. - #[track_caller] - pub fn new(signal: Signal) -> Self { - Self::new_maybe_sync(signal) - } -} - -impl>> ReadOnlySignal { - /// Create a new read-only signal that is maybe sync. - #[track_caller] - pub fn new_maybe_sync(signal: Signal) -> Self { - Self { inner: signal } - } - - /// Get the scope that the signal was created in. - pub fn origin_scope(&self) -> ScopeId { - self.inner.origin_scope() - } - - /// Get the id of the signal. - pub fn id(&self) -> generational_box::GenerationalBoxId { - self.inner.id() - } - - /// Point to another signal - pub fn point_to(&self, other: Self) -> BorrowResult { - self.inner.point_to(other.inner) - } - - #[doc(hidden)] - /// This is only used by the `props` macro. - /// Mark any readers of the signal as dirty - pub fn mark_dirty(&mut self) { - use crate::write::Writable; - _ = self.inner.try_write(); - } -} - -impl>> Readable for ReadOnlySignal { - type Target = T; - type Storage = S; - - #[track_caller] - fn try_read_unchecked( - &self, - ) -> Result, generational_box::BorrowError> { - self.inner.try_read_unchecked() - } - - /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.** - /// - /// If the signal has been dropped, this will panic. - #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { - self.inner.try_peek_unchecked() - } -} - -#[cfg(feature = "serialize")] -impl>> serde::Serialize - for ReadOnlySignal -{ - fn serialize(&self, serializer: S) -> Result { - self.read().serialize(serializer) - } -} - -#[cfg(feature = "serialize")] -impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage>> - serde::Deserialize<'de> for ReadOnlySignal -{ - fn deserialize>(deserializer: D) -> Result { - Ok(Self::new_maybe_sync(Signal::new_maybe_sync( - T::deserialize(deserializer)?, - ))) - } -} - -impl IntoAttributeValue for ReadOnlySignal -where - T: Clone + IntoAttributeValue, -{ - fn into_value(self) -> dioxus_core::AttributeValue { - self.with(|f| f.clone().into_value()) - } -} - -impl IntoDynNode for ReadOnlySignal -where - T: Clone + IntoDynNode, -{ - fn into_dyn_node(self) -> dioxus_core::DynamicNode { - self().into_dyn_node() - } -} - -impl>> PartialEq for ReadOnlySignal { - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner - } -} - -impl> + 'static> Deref for ReadOnlySignal { - type Target = dyn Fn() -> T; - - fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } - } -} - -read_impls!( - ReadOnlySignal where - S: Storage> -); -default_impl!( - ReadOnlySignal where - S: Storage> -); - -impl>> Clone for ReadOnlySignal { - fn clone(&self) -> Self { - *self - } -} - -impl>> Copy for ReadOnlySignal {} diff --git a/packages/signals/src/set_compare.rs b/packages/signals/src/set_compare.rs index 0630e9e56f..f01bfcfb01 100644 --- a/packages/signals/src/set_compare.rs +++ b/packages/signals/src/set_compare.rs @@ -1,12 +1,11 @@ -use crate::write::Writable; +use crate::{write::Writable, ReadableExt}; use std::hash::Hash; -use crate::read::Readable; use dioxus_core::ReactiveContext; use futures_util::StreamExt; use generational_box::{Storage, UnsyncStorage}; -use crate::{CopyValue, ReadOnlySignal, Signal, SignalData}; +use crate::{CopyValue, ReadSignal, Signal, SignalData}; use rustc_hash::FxHashMap; /// An object that can efficiently compare a value to a set of values. @@ -66,9 +65,11 @@ impl>> SetCompare { SetCompare { subscribers } } +} +impl SetCompare { /// Returns a signal which is true when the value is equal to the value passed to this function. - pub fn equal(&mut self, value: R) -> ReadOnlySignal { + pub fn equal(&mut self, value: R) -> ReadSignal { let subscribers = self.subscribers.write(); match subscribers.get(&value) { diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index d492ab26e1..2e0f54fcc5 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -2,15 +2,9 @@ use crate::{ default_impl, fmt_impls, read::*, write::*, write_impls, CopyValue, Global, GlobalMemo, GlobalSignal, Memo, ReadableRef, WritableRef, }; -use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId}; -use generational_box::{AnyStorage, BorrowResult, Storage, SyncStorage, UnsyncStorage}; -use std::{ - any::Any, - collections::HashSet, - ops::{Deref, DerefMut}, - sync::Arc, - sync::Mutex, -}; +use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, Subscribers}; +use generational_box::{BorrowResult, Storage, SyncStorage, UnsyncStorage}; +use std::{any::Any, collections::HashSet, ops::Deref, sync::Arc, sync::Mutex}; #[doc = include_str!("../docs/signals.md")] #[doc(alias = "State")] @@ -261,7 +255,7 @@ impl>> Signal { self.inner.id() } - /// **This pattern is no longer recommended. Prefer [`peek`](Signal::peek) or creating new signals instead.** + /// **This pattern is no longer recommended. Prefer [`peek`](ReadableExt::peek) or creating new signals instead.** /// /// This function is the equivalent of the [write_silent](https://docs.rs/dioxus/latest/dioxus/prelude/struct.UseRef.html#method.write_silent) method on use_ref. /// @@ -289,7 +283,7 @@ impl>> Signal { /// *signal.write_silent() += 1; /// ``` /// - /// Instead you can use the [`peek`](Signal::peek) and [`write`](Signal::write) methods instead. The peek method will not subscribe to the current scope which will avoid an infinite loop if you are reading and writing to the same signal in the same scope. + /// Instead you can use the [`peek`](ReadableExt::peek) and [`write`](Signal::write) methods instead. The peek method will not subscribe to the current scope which will avoid an infinite loop if you are reading and writing to the same signal in the same scope. /// ```rust, no_run /// # use dioxus::prelude::*; /// let mut signal = use_signal(|| 0); @@ -342,7 +336,7 @@ impl>> Signal { /// } /// ``` /// - /// Instead [`peek`](Signal::peek) locally opts out of automatic state updates explicitly for a specific read which is easier to reason about. + /// Instead [`peek`](ReadableExt::peek) locally opts out of automatic state updates explicitly for a specific read which is easier to reason about. /// /// Here is the same example using peek: /// main.rs: @@ -378,8 +372,10 @@ impl>> Signal { /// ``` #[track_caller] #[deprecated = "This pattern is no longer recommended. Prefer `peek` or creating new signals instead."] - pub fn write_silent(&self) -> S::Mut<'static, T> { - S::map_mut(self.inner.write_unchecked(), |inner| &mut inner.value) + pub fn write_silent(&self) -> WriteLock<'static, T, S> { + WriteLock::map(self.inner.write_unchecked(), |inner: &mut SignalData| { + &mut inner.value + }) } } @@ -408,34 +404,14 @@ impl>> Readable for Signal { .try_read_unchecked() .map(|inner| S::map(inner, |v| &v.value)) } -} - -impl>> Writable for Signal { - type Mut<'a, R: ?Sized + 'static> = Write<'a, R, S>; - - fn map_mut &mut U>( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Self::Mut<'_, U> { - Write::map(ref_, f) - } - fn try_map_mut< - I: ?Sized + 'static, - U: ?Sized + 'static, - F: FnOnce(&mut I) -> Option<&mut U>, - >( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Option> { - Write::filter_map(ref_, f) + fn subscribers(&self) -> Option { + Some(self.inner.read().subscribers.clone().into()) } +} - fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>( - mut_: Self::Mut<'a, R>, - ) -> Self::Mut<'b, R> { - Write::downcast_lifetime(mut_) - } +impl>> Writable for Signal { + type WriteMetadata = Box; #[track_caller] fn try_write_unchecked( @@ -444,15 +420,15 @@ impl>> Writable for Signal { #[cfg(debug_assertions)] let origin = std::panic::Location::caller(); self.inner.try_write_unchecked().map(|inner| { - let borrow = S::map_mut(inner, |v| &mut v.value); - Write { - write: borrow, - drop_signal: Box::new(SignalSubscriberDrop { + let borrow = S::map_mut(inner.into_inner(), |v| &mut v.value); + WriteLock::new_with_metadata( + borrow, + Box::new(SignalSubscriberDrop { signal: *self, #[cfg(debug_assertions)] origin, - }), - } + }) as Box, + ) }) } } @@ -490,7 +466,7 @@ impl> + 'static> Deref for Signal { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - unsafe { Readable::deref_impl(self) } + unsafe { ReadableExt::deref_impl(self) } } } @@ -512,172 +488,6 @@ impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage>> } } -/// A mutable reference to a signal's value. This reference acts similarly to [`std::cell::RefMut`], but it has extra debug information -/// and integrates with the reactive system to automatically update dependents. -/// -/// [`Write`] implements [`DerefMut`] which means you can call methods on the inner value just like you would on a mutable reference -/// to the inner value. If you need to get the inner reference directly, you can call [`Write::deref_mut`]. -/// -/// # Example -/// ```rust -/// # use dioxus::prelude::*; -/// fn app() -> Element { -/// let mut value = use_signal(|| String::from("hello")); -/// -/// rsx! { -/// button { -/// onclick: move |_| { -/// let mut mutable_reference = value.write(); -/// -/// // You call methods like `push_str` on the reference just like you would with the inner String -/// mutable_reference.push_str("world"); -/// }, -/// "Click to add world to the string" -/// } -/// div { "{value}" } -/// } -/// } -/// ``` -/// -/// ## Matching on Write -/// -/// You need to get the inner mutable reference with [`Write::deref_mut`] before you match the inner value. If you try to match -/// without calling [`Write::deref_mut`], you will get an error like this: -/// -/// ```compile_fail -/// # use dioxus::prelude::*; -/// #[derive(Debug)] -/// enum Colors { -/// Red(u32), -/// Green -/// } -/// fn app() -> Element { -/// let mut value = use_signal(|| Colors::Red(0)); -/// -/// rsx! { -/// button { -/// onclick: move |_| { -/// let mut mutable_reference = value.write(); -/// -/// match mutable_reference { -/// // Since we are matching on the `Write` type instead of &mut Colors, we can't match on the enum directly -/// Colors::Red(brightness) => *brightness += 1, -/// Colors::Green => {} -/// } -/// }, -/// "Click to add brightness to the red color" -/// } -/// div { "{value:?}" } -/// } -/// } -/// ``` -/// -/// ```text -/// error[E0308]: mismatched types -/// --> src/main.rs:18:21 -/// | -/// 16 | match mutable_reference { -/// | ----------------- this expression has type `dioxus::prelude::Write<'_, Colors>` -/// 17 | // Since we are matching on the `Write` t... -/// 18 | Colors::Red(brightness) => *brightness += 1, -/// | ^^^^^^^^^^^^^^^^^^^^^^^ expected `Write<'_, Colors>`, found `Colors` -/// | -/// = note: expected struct `dioxus::prelude::Write<'_, Colors, >` -/// found enum `Colors` -/// ``` -/// -/// Instead, you need to call deref mut on the reference to get the inner value **before** you match on it: -/// -/// ```rust -/// use std::ops::DerefMut; -/// # use dioxus::prelude::*; -/// #[derive(Debug)] -/// enum Colors { -/// Red(u32), -/// Green -/// } -/// fn app() -> Element { -/// let mut value = use_signal(|| Colors::Red(0)); -/// -/// rsx! { -/// button { -/// onclick: move |_| { -/// let mut mutable_reference = value.write(); -/// -/// // DerefMut converts the `Write` into a `&mut Colors` -/// match mutable_reference.deref_mut() { -/// // Now we can match on the inner value -/// Colors::Red(brightness) => *brightness += 1, -/// Colors::Green => {} -/// } -/// }, -/// "Click to add brightness to the red color" -/// } -/// div { "{value:?}" } -/// } -/// } -/// ``` -/// -/// ## Generics -/// - T is the current type of the write -/// - S is the storage type of the signal. This type determines if the signal is local to the current thread, or it can be shared across threads. -pub struct Write<'a, T: ?Sized + 'static, S: AnyStorage = UnsyncStorage> { - write: S::Mut<'a, T>, - drop_signal: Box, -} - -impl<'a, T: ?Sized + 'static, S: AnyStorage> Write<'a, T, S> { - /// Map the mutable reference to the signal's value to a new type. - pub fn map(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, S> { - let Self { - write, drop_signal, .. - } = myself; - Write { - write: S::map_mut(write, f), - drop_signal, - } - } - - /// Try to map the mutable reference to the signal's value to a new type - pub fn filter_map( - myself: Self, - f: impl FnOnce(&mut T) -> Option<&mut O>, - ) -> Option> { - let Self { - write, drop_signal, .. - } = myself; - let write = S::try_map_mut(write, f); - write.map(|write| Write { write, drop_signal }) - } - - /// Downcast the lifetime of the mutable reference to the signal's value. - /// - /// This function enforces the variance of the lifetime parameter `'a` in Mut. Rust will typically infer this cast with a concrete type, but it cannot with a generic type. - pub fn downcast_lifetime<'b>(mut_: Self) -> Write<'b, T, S> - where - 'a: 'b, - { - Write { - write: S::downcast_lifetime_mut(mut_.write), - drop_signal: mut_.drop_signal, - } - } -} - -impl Deref for Write<'_, T, S> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.write - } -} - -impl DerefMut for Write<'_, T, S> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.write - } -} - struct SignalSubscriberDrop>> { signal: Signal, #[cfg(debug_assertions)] diff --git a/packages/signals/src/write.rs b/packages/signals/src/write.rs index 919e0fe6ed..fe5046423b 100644 --- a/packages/signals/src/write.rs +++ b/packages/signals/src/write.rs @@ -1,10 +1,13 @@ -use std::ops::{DerefMut, IndexMut}; +use std::ops::{Deref, DerefMut, IndexMut}; -use crate::read::Readable; +use generational_box::{AnyStorage, UnsyncStorage}; -/// A reference to a value that can be read from. +use crate::{read::Readable, read::ReadableExt, MappedMutSignal, WriteSignal}; + +/// A reference to a value that can be written to. #[allow(type_alias_bounds)] -pub type WritableRef<'a, T: Writable, O = ::Target> = T::Mut<'a, O>; +pub type WritableRef<'a, T: Writable, O = ::Target> = + WriteLock<'a, O, ::Storage, ::WriteMetadata>; /// A trait for states that can be written to like [`crate::Signal`]. You may choose to accept this trait as a parameter instead of the concrete type to allow for more flexibility in your API. /// @@ -32,27 +35,8 @@ pub type WritableRef<'a, T: Writable, O = ::Target> = T::Mut<'a, /// } /// ``` pub trait Writable: Readable { - /// The type of the reference. - type Mut<'a, R: ?Sized + 'static>: DerefMut; - - /// Map the reference to a new type. - fn map_mut &mut U>( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Self::Mut<'_, U>; - - /// Try to map the reference to a new type. - fn try_map_mut Option<&mut U>>( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Option>; - - /// Downcast a mutable reference in a RefMut to a more specific lifetime - /// - /// This function enforces the variance of the lifetime parameter `'a` in Ref. - fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>( - mut_: Self::Mut<'a, T>, - ) -> Self::Mut<'b, T>; + /// Additional data associated with the write reference. + type WriteMetadata: 'static; /// Get a mutable reference to the value. If the value has been dropped, this will panic. #[track_caller] @@ -63,7 +47,7 @@ pub trait Writable: Readable { /// Try to get a mutable reference to the value. #[track_caller] fn try_write(&mut self) -> Result, generational_box::BorrowMutError> { - self.try_write_unchecked().map(Self::downcast_lifetime_mut) + self.try_write_unchecked().map(WriteLock::downcast_lifetime) } /// Try to get a mutable reference to the value without checking the lifetime. This will update any subscribers. @@ -80,6 +64,255 @@ pub trait Writable: Readable { fn write_unchecked(&self) -> WritableRef<'static, Self> { self.try_write_unchecked().unwrap() } +} + +/// A mutable reference to a writable value. This reference acts similarly to [`std::cell::RefMut`], but it has extra debug information +/// and integrates with the reactive system to automatically update dependents. +/// +/// [`WriteLock`] implements [`DerefMut`] which means you can call methods on the inner value just like you would on a mutable reference +/// to the inner value. If you need to get the inner reference directly, you can call [`WriteLock::deref_mut`]. +/// +/// # Example +/// ```rust +/// # use dioxus::prelude::*; +/// fn app() -> Element { +/// let mut value = use_signal(|| String::from("hello")); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// let mut mutable_reference = value.write(); +/// +/// // You call methods like `push_str` on the reference just like you would with the inner String +/// mutable_reference.push_str("world"); +/// }, +/// "Click to add world to the string" +/// } +/// div { "{value}" } +/// } +/// } +/// ``` +/// +/// ## Matching on WriteLock +/// +/// You need to get the inner mutable reference with [`WriteLock::deref_mut`] before you match the inner value. If you try to match +/// without calling [`WriteLock::deref_mut`], you will get an error like this: +/// +/// ```compile_fail +/// # use dioxus::prelude::*; +/// #[derive(Debug)] +/// enum Colors { +/// Red(u32), +/// Green +/// } +/// fn app() -> Element { +/// let mut value = use_signal(|| Colors::Red(0)); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// let mut mutable_reference = value.write(); +/// +/// match mutable_reference { +/// // Since we are matching on the `Write` type instead of &mut Colors, we can't match on the enum directly +/// Colors::Red(brightness) => *brightness += 1, +/// Colors::Green => {} +/// } +/// }, +/// "Click to add brightness to the red color" +/// } +/// div { "{value:?}" } +/// } +/// } +/// ``` +/// +/// ```text +/// error[E0308]: mismatched types +/// --> src/main.rs:18:21 +/// | +/// 16 | match mutable_reference { +/// | ----------------- this expression has type `dioxus::prelude::Write<'_, Colors>` +/// 17 | // Since we are matching on the `Write` t... +/// 18 | Colors::Red(brightness) => *brightness += 1, +/// | ^^^^^^^^^^^^^^^^^^^^^^^ expected `Write<'_, Colors>`, found `Colors` +/// | +/// = note: expected struct `dioxus::prelude::Write<'_, Colors, >` +/// found enum `Colors` +/// ``` +/// +/// Instead, you need to call deref mut on the reference to get the inner value **before** you match on it: +/// +/// ```rust +/// use std::ops::DerefMut; +/// # use dioxus::prelude::*; +/// #[derive(Debug)] +/// enum Colors { +/// Red(u32), +/// Green +/// } +/// fn app() -> Element { +/// let mut value = use_signal(|| Colors::Red(0)); +/// +/// rsx! { +/// button { +/// onclick: move |_| { +/// let mut mutable_reference = value.write(); +/// +/// // DerefMut converts the `Write` into a `&mut Colors` +/// match mutable_reference.deref_mut() { +/// // Now we can match on the inner value +/// Colors::Red(brightness) => *brightness += 1, +/// Colors::Green => {} +/// } +/// }, +/// "Click to add brightness to the red color" +/// } +/// div { "{value:?}" } +/// } +/// } +/// ``` +/// +/// ## Generics +/// - T is the current type of the write +/// - S is the storage type of the signal. This type determines if the signal is local to the current thread, or it can be shared across threads. +/// - D is the additional data associated with the write reference. This is used by signals to track when the write is dropped +pub struct WriteLock<'a, T: ?Sized + 'static, S: AnyStorage = UnsyncStorage, D = ()> { + write: S::Mut<'a, T>, + data: D, +} + +impl<'a, T: ?Sized, S: AnyStorage> WriteLock<'a, T, S> { + /// Create a new write reference + pub fn new(write: S::Mut<'a, T>) -> Self { + Self { write, data: () } + } +} + +impl<'a, T: ?Sized, S: AnyStorage, D> WriteLock<'a, T, S, D> { + /// Create a new write reference with additional data. + pub fn new_with_metadata(write: S::Mut<'a, T>, data: D) -> Self { + Self { write, data } + } + + /// Get the inner value of the write reference. + pub fn into_inner(self) -> S::Mut<'a, T> { + self.write + } + + /// Get the additional data associated with the write reference. + pub fn data(&self) -> &D { + &self.data + } + + /// Split into the inner value and the additional data. + pub fn into_parts(self) -> (S::Mut<'a, T>, D) { + (self.write, self.data) + } + + /// Map the metadata of the write reference to a new type. + pub fn map_metadata(self, f: impl FnOnce(D) -> O) -> WriteLock<'a, T, S, O> { + WriteLock { + write: self.write, + data: f(self.data), + } + } + + /// Map the mutable reference to the signal's value to a new type. + pub fn map( + myself: Self, + f: impl FnOnce(&mut T) -> &mut O, + ) -> WriteLock<'a, O, S, D> { + let Self { write, data, .. } = myself; + WriteLock { + write: S::map_mut(write, f), + data, + } + } + + /// Try to map the mutable reference to the signal's value to a new type + pub fn filter_map( + myself: Self, + f: impl FnOnce(&mut T) -> Option<&mut O>, + ) -> Option> { + let Self { write, data, .. } = myself; + let write = S::try_map_mut(write, f); + write.map(|write| WriteLock { write, data }) + } + + /// Downcast the lifetime of the mutable reference to the signal's value. + /// + /// This function enforces the variance of the lifetime parameter `'a` in Mut. Rust will typically infer this cast with a concrete type, but it cannot with a generic type. + pub fn downcast_lifetime<'b>(mut_: Self) -> WriteLock<'b, T, S, D> + where + 'a: 'b, + { + WriteLock { + write: S::downcast_lifetime_mut(mut_.write), + data: mut_.data, + } + } +} + +impl Deref for WriteLock<'_, T, S, D> +where + S: AnyStorage, + T: ?Sized + 'static, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.write + } +} + +impl DerefMut for WriteLock<'_, T, S, D> +where + S: AnyStorage, + T: ?Sized + 'static, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.write + } +} + +/// An extension trait for [`Writable`] that provides some convenience methods. +pub trait WritableExt: Writable { + /// Map the readable type to a new type. This lets you provide a view into a readable type without needing to clone the inner value. + /// + /// Anything that subscribes to the readable value will be rerun whenever the original value changes, even if the view does not change. If you want to memorize the view, you can use a [`crate::Memo`] instead. + /// + /// # Example + /// ```rust + /// # use dioxus::prelude::*; + /// fn List(list: Signal>) -> Element { + /// rsx! { + /// for index in 0..list.len() { + /// // We can use the `map` method to provide a view into the single item in the list that the child component will render + /// Item { item: list.map_mut(move |v| &v[index], move |v| &mut v[index]) } + /// } + /// } + /// } + /// + /// // The child component doesn't need to know that the mapped value is coming from a list + /// #[component] + /// fn Item(item: WriteSignal) -> Element { + /// rsx! { + /// button { + /// onclick: move |_| *item.write() += 1, + /// "{item}" + /// } + /// } + /// } + /// ``` + fn map_mut(self, f: F, f_mut: FMut) -> MappedMutSignal + where + Self: Clone + Sized + 'static, + O: ?Sized + 'static, + F: Fn(&Self::Target) -> &O + 'static, + FMut: Fn(&mut Self::Target) -> &mut O + 'static, + { + MappedMutSignal::new(self, f, f_mut) + } /// Run a function with a mutable reference to the value. If the value has been dropped, this will panic. #[track_caller] @@ -115,7 +348,7 @@ pub trait Writable: Readable { where Self::Target: std::ops::IndexMut, { - Self::map_mut(self.write(), |v| v.index_mut(index)) + WriteLock::map(self.write(), |v| v.index_mut(index)) } /// Takes the value out of the Signal, leaving a Default in its place. @@ -137,6 +370,25 @@ pub trait Writable: Readable { } } +impl WritableExt for W {} + +/// An extension trait for [`Writable`] values that can be boxed into a trait object. +pub trait WritableBoxedExt: Writable { + /// Box the writable value into a trait object. This is useful for passing around writable values without knowing their concrete type. + fn boxed_mut(self) -> WriteSignal + where + Self: Sized + 'static, + { + WriteSignal::new(self) + } +} + +impl + 'static> WritableBoxedExt for T { + fn boxed_mut(self) -> WriteSignal { + WriteSignal::new(self) + } +} + /// An extension trait for [`Writable>`]` that provides some convenience methods. pub trait WritableOptionExt: Writable> { /// Gets the value out of the Option, or inserts the given value if the Option is empty. @@ -151,16 +403,16 @@ pub trait WritableOptionExt: Writable> { let is_none = self.read().is_none(); if is_none { self.with_mut(|v| *v = Some(default())); - Self::map_mut(self.write(), |v| v.as_mut().unwrap()) + WriteLock::map(self.write(), |v| v.as_mut().unwrap()) } else { - Self::map_mut(self.write(), |v| v.as_mut().unwrap()) + WriteLock::map(self.write(), |v| v.as_mut().unwrap()) } } /// Attempts to write the inner value of the Option. #[track_caller] fn as_mut(&mut self) -> Option> { - Self::try_map_mut(self.write(), |v: &mut Option| v.as_mut()) + WriteLock::filter_map(self.write(), |v: &mut Option| v.as_mut()) } } @@ -236,7 +488,7 @@ pub trait WritableVecExt: Writable> { /// Try to mutably get an element from the vector. #[track_caller] fn get_mut(&mut self, index: usize) -> Option> { - Self::try_map_mut(self.write(), |v: &mut Vec| v.get_mut(index)) + WriteLock::filter_map(self.write(), |v: &mut Vec| v.get_mut(index)) } /// Gets an iterator over the values of the vector. @@ -264,11 +516,11 @@ impl<'a, T: 'static, R: Writable>> Iterator for WritableValueIte fn next(&mut self) -> Option { let index = self.index; self.index += 1; - R::try_map_mut( + WriteLock::filter_map( self.value.try_write_unchecked().unwrap(), |v: &mut Vec| v.get_mut(index), ) - .map(R::downcast_lifetime_mut) + .map(WriteLock::downcast_lifetime) } }