Boxed and mapped signals preparing for stores (#4413)

This PR contains just the breaking changes required for the upcoming store implementation. It adds two new primitives:

Mutable mapped signals (without a different subscriber list). This lets you scope a writable value and pass it to a child component. It requires two closures to map the immutable and mutable references to the inner signal. With stores, these closures will be derived
Boxed readable and writable values (ReadSignal, and WriteSignal). Any readable or writable types can be converted to a read signal or write signal with the boxed method or built in types are automatically converted into the boxed variants when passing into props


* map signals mutably

* boxed signals

* remove mutable borrow type from boxed writable

* improve map mut example

* restore boxed_mut helper

* make boxed readable/writable types copy

* Move away from the ReadOnlySignal alias in examples

* automatically convert Write

* Fix some tests

* ReadSignal and WriteSignal

* move read only signal alias

* make maps copy

* store prototype

* implement derive macro

* Handle generics in the derive macro

* Make Selector boxable

* derive boxed conversion

* Get rid of store type

* Remove hardcoded type from derive macro

* selector -> store

* Refactor store implementation

* more vec methods

* todo mvc store example

* Implement Storable for HashMap

* fix write

* restore sorting logic in todomvc

* derive readable and writable

* improve todomvc store example

* Result store

* Option store

* collect dead subscribers

* fix clippy and formatting

* recognize some common foreign types automatically

* fix foreign stores in props

* slice and array selectors

* clean up store example

* remove stores

* writable is child owned

* fix typo

* fix clippy

* fix doc tests
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-07-31 13:19:55 -07:00
1 parent 6919f72abc
commit d8d11db403
64 files changed
+1303 -722

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@ members = [
"packages/playwright-tests/nested-suspense", "packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization", "packages/playwright-tests/cli-optimization",
"packages/playwright-tests/wasm-split-harness", "packages/playwright-tests/wasm-split-harness",
"packages/playwright-tests/default-features-disabled", "packages/playwright-tests/default-features-disabled"
] ]
[workspace.package] [workspace.package]
@@ -36,7 +36,7 @@ impl FromStr for Size {
} }
#[component] #[component]
pub fn product_page(product_id: ReadOnlySignal<usize>) -> Element { pub fn product_page(product_id: ReadSignal<usize>) -> Element {
let mut quantity = use_signal(|| 1); let mut quantity = use_signal(|| 1);
let mut size = use_signal(Size::default); let mut size = use_signal(Size::default);
@@ -32,7 +32,7 @@ pub fn App() -> Element {
} }
#[component] #[component]
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element { fn Homepage(story: ReadSignal<PreviewState>) -> Element {
rsx! { rsx! {
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") } document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
div { display: "flex", flex_direction: "row", width: "100%", div { display: "flex", flex_direction: "row", width: "100%",
@@ -84,7 +84,7 @@ fn Stories() -> Element {
} }
#[component] #[component]
fn StoryListing(story: ReadOnlySignal<i64>) -> Element { fn StoryListing(story: ReadSignal<i64>) -> Element {
let story = use_server_future(move || get_story(story()))?; let story = use_server_future(move || get_story(story()))?;
let StoryItem { let StoryItem {
@@ -167,7 +167,7 @@ impl Display for PreviewState {
} }
#[component] #[component]
fn Preview(story: ReadOnlySignal<PreviewState>) -> Element { fn Preview(story: ReadSignal<PreviewState>) -> Element {
let PreviewState { let PreviewState {
active_story: Some(id), active_story: Some(id),
} = story() } = story()
+1 -1
View File
@@ -36,7 +36,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn Child(count: Signal<i32>) -> Element { fn Child(count: WriteSignal<i32>) -> Element {
let mut early_return = use_signal(|| false); let mut early_return = use_signal(|| false);
let early = rsx! { let early = rsx! {
+1 -1
View File
@@ -75,7 +75,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element { fn ColorControl(label: &'static str, color_str: WriteSignal<String>) -> Element {
rsx!(div { rsx!(div {
class: "color-control", class: "color-control",
{ label }, { label },
+1 -1
View File
@@ -60,7 +60,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn BreedPic(breed: Signal<String>) -> Element { fn BreedPic(breed: WriteSignal<String>) -> Element {
// This resource will restart whenever the breed changes // This resource will restart whenever the breed changes
let mut fut = use_resource(move || async move { let mut fut = use_resource(move || async move {
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
+1 -1
View File
@@ -81,7 +81,7 @@ impl FromStr for State {
} }
#[component] #[component]
fn Home(url_hash: ReadOnlySignal<State>) -> Element { fn Home(url_hash: ReadSignal<State>) -> Element {
// The initial state of the state comes from the url hash // The initial state of the state comes from the url hash
let mut state = use_signal(&*url_hash); let mut state = use_signal(&*url_hash);
+1 -1
View File
@@ -28,7 +28,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn Child(state: Memo<isize>, items: Memo<Vec<isize>>, depth: ReadOnlySignal<usize>) -> Element { fn Child(state: Memo<isize>, items: Memo<Vec<isize>>, depth: ReadSignal<usize>) -> Element {
// These memos don't get re-computed when early returns happen // These memos don't get re-computed when early returns happen
let state = use_memo(move || state() + 1); let state = use_memo(move || state() + 1);
let item = use_memo(move || items()[depth() - 1]); let item = use_memo(move || items()[depth() - 1]);
+2 -2
View File
@@ -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] #[component]
fn Search(query: ReadOnlySignal<String>, word_count: ReadOnlySignal<usize>) -> Element { fn Search(query: ReadSignal<String>, word_count: ReadSignal<usize>) -> Element {
const ITEMS: &[&str] = &[ const ITEMS: &[&str] = &[
"hello", "hello",
"world", "world",
+5 -5
View File
@@ -1,7 +1,7 @@
//! Example: Updating components with use_resource //! 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 //! when linking to it from the same component, when using use_resource
use dioxus::prelude::*; use dioxus::prelude::*;
@@ -25,15 +25,15 @@ fn App() -> Element {
} }
} }
// We use id: ReadOnlySignal<i32> instead of id: i32 to make id work with reactive hooks // We use id: ReadSignal<i32> instead of id: i32 to make id work with reactive hooks
// Any i32 we pass in will automatically be converted into a ReadOnlySignal<i32> // Any i32 we pass in will automatically be converted into a ReadSignal<i32>
#[component] #[component]
fn Blog(id: ReadOnlySignal<i32>) -> Element { fn Blog(id: ReadSignal<i32>) -> Element {
async fn future(n: i32) -> i32 { async fn future(n: i32) -> i32 {
n n
} }
// Because we accept ReadOnlySignal<i32> instead of i32, the resource will automatically subscribe to the id when we read it // Because we accept ReadSignal<i32> instead of i32, the resource will automatically subscribe to the id when we read it
let res = use_resource(move || future(id())); let res = use_resource(move || future(id()));
match res() { match res() {
+2 -2
View File
@@ -3,7 +3,7 @@
//! This simple example implements a counter that can be incremented, decremented, and paused. It also demonstrates //! 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. //! that background tasks in use_futures can modify the value as well.
//! //!
//! Most signals implement Into<ReadOnlySignal<T>>, making ReadOnlySignal a good default type when building new //! Most signals implement Into<ReadSignal<T>>, making ReadSignal a good default type when building new
//! library components that don't need to modify their values. //! library components that don't need to modify their values.
use async_std::task::sleep; use async_std::task::sleep;
@@ -81,7 +81,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn Child(mut count: ReadOnlySignal<i32>) -> Element { fn Child(mut count: ReadSignal<i32>) -> Element {
println!("rendering child with count {count}"); println!("rendering child with count {count}");
rsx! { rsx! {
+5 -5
View File
@@ -110,7 +110,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element { fn TodoHeader(mut todos: WriteSignal<HashMap<u32, TodoItem>>) -> Element {
let mut draft = use_signal(|| "".to_string()); let mut draft = use_signal(|| "".to_string());
let mut todo_id = use_signal(|| 0); let mut todo_id = use_signal(|| 0);
@@ -146,7 +146,7 @@ fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
/// This takes the ID of the todo and the todos signal as props /// 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 /// We can use these together to memoize the todo contents and checked state
#[component] #[component]
fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, id: u32) -> Element { fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element {
let mut is_editing = use_signal(|| false); let mut is_editing = use_signal(|| false);
// To avoid re-rendering this component when the todo list changes, we isolate our reads to memos // 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<HashMap<u32, TodoItem>>, id: u32) -> Element {
#[component] #[component]
fn ListFooter( fn ListFooter(
mut todos: Signal<HashMap<u32, TodoItem>>, mut todos: WriteSignal<HashMap<u32, TodoItem>>,
active_todo_count: ReadOnlySignal<usize>, active_todo_count: ReadSignal<usize>,
mut filter: Signal<FilterState>, mut filter: WriteSignal<FilterState>,
) -> Element { ) -> Element {
// We use a memoized signal to calculate whether we should show the "Clear completed" button. // 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. // This will recompute whenever the todos change, and if the value is true, the button will be shown.
+1 -1
View File
@@ -108,7 +108,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
} }
#[component] #[component]
fn SearchBox(mut country: Signal<WeatherLocation>) -> Element { fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
let mut input = use_signal(|| "".to_string()); let mut input = use_signal(|| "".to_string());
let locations = use_resource(move || async move { get_locations(&input()).await }); let locations = use_resource(move || async move { get_locations(&input()).await });
+1 -1
View File
@@ -87,7 +87,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element { fn ColorControl(label: &'static str, color_str: WriteSignal<String>) -> Element {
rsx!(div { rsx!(div {
class: "color-control", class: "color-control",
{ label }, { label },
+4 -4
View File
@@ -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: Props also act slightly differently when used with:
- [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`. - [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`.
- [`ReadOnlySignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal<T>` when it is passed as a prop. - [`ReadSignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal<T>` 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`. - [`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. - [`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: 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 ```rust, no_run
# use dioxus::prelude::*; # use dioxus::prelude::*;
#[component] #[component]
fn Counter(count: ReadOnlySignal<i32>) -> Element { fn Counter(count: ReadSignal<i32>) -> Element {
// Since we made count reactive, the memo will automatically rerun when count changes. // Since we made count reactive, the memo will automatically rerun when count changes.
let doubled_count = use_memo(move || count() * 2); let doubled_count = use_memo(move || count() * 2);
rsx! { rsx! {
+4 -4
View File
@@ -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: Props also act slightly differently when used with:
- [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`. - [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`.
- [`ReadOnlySignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal<T>` when it is passed as a prop. - [`ReadSignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal<T>` 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`. - [`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. - [`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: 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 ```rust, no_run
# use dioxus::prelude::*; # use dioxus::prelude::*;
#[component] #[component]
fn Counter(count: ReadOnlySignal<i32>) -> Element { fn Counter(count: ReadSignal<i32>) -> Element {
// Since we made count reactive, the memo will automatically rerun when count changes. // Since we made count reactive, the memo will automatically rerun when count changes.
let doubled_count = use_memo(move || count() * 2); let doubled_count = use_memo(move || count() * 2);
rsx! { rsx! {
+37 -7
View File
@@ -172,7 +172,7 @@ mod util {
} }
mod field_info { 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 proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::spanned::Spanned; use syn::spanned::Spanned;
@@ -221,6 +221,11 @@ mod field_info {
builder_attr.auto_into = false; 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 // extended field is automatically empty
if !builder_attr.extends.is_empty() { if !builder_attr.extends.is_empty() {
builder_attr.default = Some( builder_attr.default = Some(
@@ -503,9 +508,9 @@ fn type_from_inside_option(ty: &Type) -> Option<&Type> {
let seg = path.segments.last()?; let seg = path.segments.last()?;
// If the segment is a supported optional type, provide the inner type. // If the segment is a supported optional type, provide the inner type.
// Return the inner type if the pattern is `Option<T>` or `ReadOnlySignal<Option<T>>`` // Return the inner type if the pattern is `Option<T>` or `ReadSignal<Option<T>>``
if seg.ident == "ReadOnlySignal" { if seg.ident == "ReadOnlySignal" || seg.ident == "ReadSignal" {
// Get the inner type. E.g. the `u16` in `ReadOnlySignal<u16>` or `Option` in `ReadOnlySignal<Option<bool>>` // Get the inner type. E.g. the `u16` in `ReadSignal<u16>` or `Option` in `ReadSignal<Option<bool>>`
let inner_type = extract_inner_type_from_segment(seg)?; let inner_type = extract_inner_type_from_segment(seg)?;
let Type::Path(inner_path) = inner_type else { let Type::Path(inner_path) = inner_type else {
// If it isn't a path, the inner type isn't option // If it isn't a path, the inner type isn't option
@@ -614,13 +619,13 @@ mod struct_info {
generics generics
} }
/// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadOnlySignal<T>`, 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<T>`, the new signal should be owned by the child
fn has_child_owned_fields(&self) -> bool { fn has_child_owned_fields(&self) -> bool {
self.fields.iter().any(|f| child_owned_type(f.ty)) self.fields.iter().any(|f| child_owned_type(f.ty))
} }
fn memoize_impl(&self) -> Result<TokenStream, Error> { fn memoize_impl(&self) -> Result<TokenStream, Error> {
// 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 let signal_fields: Vec<_> = self
.included_fields() .included_fields()
.filter(|f| looks_like_signal_type(f.ty)) .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 /// Check if a type should be owned by the child component after conversion
fn child_owned_type(ty: &Type) -> bool { 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 { 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!(dioxus_core::ReadOnlySignal)
|| path_without_generics == parse_quote!(prelude::ReadOnlySignal) || path_without_generics == parse_quote!(prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(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, None => false,
} }
@@ -1778,6 +1797,17 @@ fn test_looks_like_type() {
ReadOnlySignal<Option<i32>, UnsyncStorage> ReadOnlySignal<Option<i32>, UnsyncStorage>
))); )));
assert!(!looks_like_signal_type(&parse_quote!(
Option<ReadSignal<i32>>
)));
assert!(looks_like_signal_type(&parse_quote!(ReadSignal<i32>)));
assert!(looks_like_signal_type(
&parse_quote!(ReadSignal<i32, SyncStorage>)
));
assert!(looks_like_signal_type(&parse_quote!(
ReadSignal<Option<i32>, UnsyncStorage>
)));
assert!(looks_like_callback_type(&parse_quote!( assert!(looks_like_callback_type(&parse_quote!(
Option<EventHandler> Option<EventHandler>
))); )));
+8 -8
View File
@@ -52,13 +52,13 @@ mod test_default_into {
// Test default values for signals // Test default values for signals
#[props(default)] #[props(default)]
read_only_w_default: ReadOnlySignal<bool>, read_only_w_default: ReadSignal<bool>,
#[props(default = true)] #[props(default = true)]
read_only_w_default_val: ReadOnlySignal<bool>, read_only_w_default_val: ReadSignal<bool>,
#[props(default = ReadOnlySignal::new(Signal::new(true)))] #[props(default = ReadSignal::new(Signal::new(true)))]
read_only_w_default_val_explicit: ReadOnlySignal<bool>, read_only_w_default_val_explicit: ReadSignal<bool>,
// Test default values for callbacks/event handlers // Test default values for callbacks/event handlers
#[props(default)] #[props(default)]
@@ -108,7 +108,7 @@ mod test_optional_signals {
fn UsesComponents() -> Element { fn UsesComponents() -> Element {
rsx! { rsx! {
PropsStruct { PropsStruct {
regular_read_signal: ReadOnlySignal::new(Signal::new(1234)), regular_read_signal: ReadSignal::new(Signal::new(1234)),
} }
PropsStruct { PropsStruct {
optional_read_signal: 1234, optional_read_signal: 1234,
@@ -126,8 +126,8 @@ mod test_optional_signals {
// Test props as struct param. // Test props as struct param.
#[derive(Props, Clone, PartialEq)] #[derive(Props, Clone, PartialEq)]
struct MyTestProps { struct MyTestProps {
pub optional_read_signal: ReadOnlySignal<Option<u16>>, pub optional_read_signal: ReadSignal<Option<u16>>,
pub regular_read_signal: ReadOnlySignal<u16>, pub regular_read_signal: ReadSignal<u16>,
} }
#[component] #[component]
@@ -137,7 +137,7 @@ mod test_optional_signals {
// Test props as params. // Test props as params.
#[component] #[component]
fn PropParams(opt_read_sig: ReadOnlySignal<Option<u16>>) -> Element { fn PropParams(opt_read_sig: ReadSignal<Option<u16>>) -> Element {
rsx! { "hi!" } rsx! { "hi!" }
} }
@@ -124,7 +124,7 @@ fn TakesEventHandler(click: EventHandler<usize>, number: usize) -> Element {
} }
#[component] #[component]
fn TakesSignal(sig: ReadOnlySignal<usize>, number: usize) -> Element { fn TakesSignal(sig: ReadSignal<usize>, number: usize) -> Element {
let first_render_sig = use_hook(move || sig); let first_render_sig = use_hook(move || sig);
if generation() > 0 { if generation() > 0 {
// Make sure the signal is memoized in place and never gets dropped // 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>(T); struct NonCloneable<T>(T);
#[component] #[component]
fn TakesReadOnlySignalNum(sig: ReadOnlySignal<i32>) -> Element { fn TakesReadOnlySignalNum(sig: ReadSignal<i32>) -> Element {
rsx! {} rsx! {}
} }
#[component] #[component]
fn TakesReadOnlySignalNonClone(sig: ReadOnlySignal<NonCloneable<i32>>) -> Element { fn TakesReadOnlySignalNonClone(sig: ReadSignal<NonCloneable<i32>>) -> Element {
rsx! {} rsx! {}
} }
+3 -3
View File
@@ -74,13 +74,13 @@ fn MyComponent(string: String) -> Element {
You can fix this issue by either: You can fix this issue by either:
- Making your data `Copy` with `ReadOnlySignal`: - Making your data `Copy` with `ReadSignal`:
```rust, no_run ```rust, no_run
# use dioxus::prelude::*; # use dioxus::prelude::*;
// `MyComponent` accepts `ReadOnlySignal<String>` which implements `Copy` // `MyComponent` accepts `ReadSignal<String>` which implements `Copy`
#[component] #[component]
fn MyComponent(string: ReadOnlySignal<String>) -> Element { fn MyComponent(string: ReadSignal<String>) -> Element {
use_hook(move || { use_hook(move || {
// ✅ Because the `string` signal is `Copy`, we can copy it into the async task while still having access to it elsewhere // ✅ Because the `string` signal is `Copy`, we can copy it into the async task while still having access to it elsewhere
spawn(async move { spawn(async move {
+2 -2
View File
@@ -92,9 +92,9 @@ fn MyComponent(state: i32) -> Element {
todo!() todo!()
} }
// ✅ Wrap your props in ReadOnlySignal to make them reactive // ✅ Wrap your props in ReadSignal to make them reactive
#[component] #[component]
fn MyReactiveComponent(state: ReadOnlySignal<i32>) -> Element { fn MyReactiveComponent(state: ReadSignal<i32>) -> Element {
let doubled = use_memo(move || state() * 2); let doubled = use_memo(move || state() * 2);
todo!() todo!()
} }
+2 -2
View File
@@ -730,7 +730,7 @@ impl<
/// } /// }
/// ///
/// #[component] /// #[component]
/// fn Counter(multiplier: ReadOnlySignal<String>) -> Element { /// fn Counter(multiplier: ReadSignal<String>) -> Element {
/// let multiplier_parsed = multiplier().parse::<usize>()?; /// let multiplier_parsed = multiplier().parse::<usize>()?;
/// let mut count = use_signal(|| multiplier_parsed); /// let mut count = use_signal(|| multiplier_parsed);
/// rsx! { /// rsx! {
@@ -783,7 +783,7 @@ impl<
/// } /// }
/// ///
/// #[component] /// #[component]
/// fn Counter(multiplier: ReadOnlySignal<String>) -> Element { /// fn Counter(multiplier: ReadSignal<String>) -> Element {
/// let multiplier_parsed = multiplier().parse::<usize>()?; /// let multiplier_parsed = multiplier().parse::<usize>()?;
/// let mut count = use_signal(|| multiplier_parsed); /// let mut count = use_signal(|| multiplier_parsed);
/// rsx! { /// rsx! {
+4 -4
View File
@@ -87,10 +87,10 @@ pub use crate::innerlude::{
Element, ElementId, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, Element, ElementId, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes,
IntoAttributeValue, IntoDynNode, LaunchConfig, ListenerCallback, MarkerWrapper, Mutation, IntoAttributeValue, IntoDynNode, LaunchConfig, ListenerCallback, MarkerWrapper, Mutation,
Mutations, NoOpMutations, Ok, OptionStringFromMarker, Properties, ReactiveContext, RenderError, Mutations, NoOpMutations, Ok, OptionStringFromMarker, Properties, ReactiveContext, RenderError,
Result, Runtime, RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SuperFrom, SuperInto, Result, Runtime, RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SubscriberList, Subscribers,
SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps,
Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode,
VText, VirtualDom, WriteMutations, VComponent, VNode, VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations,
}; };
pub use const_format; pub use const_format;
+96 -16
View File
@@ -141,7 +141,7 @@ impl ReactiveContext {
#[allow(clippy::mutable_key_type)] #[allow(clippy::mutable_key_type)]
let old_subscribers = std::mem::take(&mut self.inner.write().subscribers); let old_subscribers = std::mem::take(&mut self.inner.write().subscribers);
for subscriber in old_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)] #[allow(clippy::mutable_key_type)]
let subscribers = &self.inner.read().subscribers; let subscribers = &self.inner.read().subscribers;
for subscriber in subscribers.iter() { 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. /// Subscribe to this context. The reactive context will automatically remove itself from the subscriptions when it is reset.
pub fn subscribe(&self, subscriptions: Arc<Mutex<HashSet<ReactiveContext>>>) { pub fn subscribe(&self, subscriptions: impl Into<Subscribers>) {
match self.inner.try_write() { match self.inner.try_write() {
Ok(mut inner) => { Ok(mut inner) => {
subscriptions.lock().unwrap().insert(*self); let subscriptions = subscriptions.into();
inner.subscribers.insert(PointerHash(subscriptions)); subscriptions.add(*self);
inner
.subscribers
.insert(PointerHash(subscriptions.inner.clone()));
} }
// If the context was dropped, we don't need to subscribe to it anymore // If the context was dropped, we don't need to subscribe to it anymore
Err(BorrowMutError::Dropped(_)) => {} Err(BorrowMutError::Dropped(_)) => {}
@@ -266,30 +269,28 @@ impl Hash for ReactiveContext {
} }
} }
struct PointerHash<T>(Arc<T>); struct PointerHash<T: ?Sized>(Arc<T>);
impl<T> Hash for PointerHash<T> { impl<T: ?Sized> Hash for PointerHash<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::sync::Arc::<T>::as_ptr(&self.0).hash(state); std::sync::Arc::<T>::as_ptr(&self.0).hash(state);
} }
} }
impl<T> PartialEq for PointerHash<T> { impl<T: ?Sized> PartialEq for PointerHash<T> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
std::sync::Arc::<T>::as_ptr(&self.0) == std::sync::Arc::<T>::as_ptr(&other.0) std::sync::Arc::ptr_eq(&self.0, &other.0)
} }
} }
impl<T> Eq for PointerHash<T> {} impl<T: ?Sized> Eq for PointerHash<T> {}
impl<T> Clone for PointerHash<T> { impl<T: ?Sized> Clone for PointerHash<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self(self.0.clone()) Self(self.0.clone())
} }
} }
type SubscriberMap = Mutex<HashSet<ReactiveContext>>;
struct Inner { struct Inner {
self_: Option<ReactiveContext>, self_: Option<ReactiveContext>,
@@ -297,7 +298,7 @@ struct Inner {
update: Box<dyn FnMut() + Send + Sync>, update: Box<dyn FnMut() + Send + Sync>,
// Subscribers to this context // Subscribers to this context
subscribers: HashSet<PointerHash<SubscriberMap>>, subscribers: HashSet<PointerHash<dyn SubscriberList + Send + Sync>>,
// Debug information for signal subscriptions // Debug information for signal subscriptions
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -315,9 +316,88 @@ impl Drop for Inner {
}; };
for subscriber in std::mem::take(&mut self.subscribers) { for subscriber in std::mem::take(&mut self.subscribers) {
if let Ok(mut subscriber) = subscriber.0.lock() { subscriber.0.remove(&self_);
subscriber.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<dyn SubscriberList + Send + Sync>,
}
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<S: SubscriberList + Send + Sync + 'static> From<Arc<S>> for Subscribers {
fn from(inner: Arc<S>) -> 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<HashSet<ReactiveContext>> {
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");
}
}
} }
+1 -1
View File
@@ -17,7 +17,7 @@
//! } //! }
//! //!
//! #[component] //! #[component]
//! fn ChildComponent(signal: Signal<Option<i32>>) -> Element { //! fn ChildComponent(signal: WriteSignal<Option<i32>>) -> Element {
//! // It feels safe to assume that signal is some because the parent component checked that it was some //! // It feels safe to assume that signal is some because the parent component checked that it was some
//! rsx! { "{signal.read().unwrap()}" } //! rsx! { "{signal.read().unwrap()}" }
//! } //! }
+1 -1
View File
@@ -44,7 +44,7 @@ async fn test_for_memory_leaks() {
#[component] #[component]
fn AcceptsEventHandlerAndReadOnlySignal( fn AcceptsEventHandlerAndReadOnlySignal(
event_handler: EventHandler<MouseEvent>, event_handler: EventHandler<MouseEvent>,
signal: ReadOnlySignal<i32>, signal: ReadSignal<i32>,
) -> Element { ) -> Element {
rsx! { rsx! {
div { div {
+1 -1
View File
@@ -1,6 +1,6 @@
use dioxus_core::internal::HotReloadedTemplate; use dioxus_core::internal::HotReloadedTemplate;
use dioxus_core::{ScopeId, VirtualDom}; use dioxus_core::{ScopeId, VirtualDom};
use dioxus_signals::{GlobalKey, Signal, Writable}; use dioxus_signals::{GlobalKey, Signal, WritableExt};
pub use dioxus_devtools_types::*; pub use dioxus_devtools_types::*;
pub use subsecond; pub use subsecond;
+2 -1
View File
@@ -32,6 +32,7 @@ Dioxus is crossplatform app framework that empowers developer to build beautiful
## Quick start ## 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. 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 ```shell
# skip if you already have cargo-binstall # skip if you already have cargo-binstall
cargo install 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. 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 ```rust, no_run
# use dioxus::prelude::*; # use dioxus::prelude::*;
@@ -1,6 +1,6 @@
use dioxus_core::{suspend, use_hook, RenderError}; use dioxus_core::{suspend, use_hook, RenderError};
use dioxus_hooks::*; use dioxus_hooks::*;
use dioxus_signals::Readable; use dioxus_signals::ReadableExt;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use std::future::Future; use std::future::Future;
+1 -1
View File
@@ -1,5 +1,5 @@
use dioxus_core::try_consume_context; use dioxus_core::try_consume_context;
use dioxus_signals::{Readable, Signal, Writable}; use dioxus_signals::{ReadableExt, Signal, WritableExt};
/// The status of the streaming response /// The status of the streaming response
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
+3 -3
View File
@@ -111,7 +111,7 @@ println!("{:?}", double_count.read()); // Prints "Some(8)"
## With non-reactive dependencies ## 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 ```rust
# use dioxus::prelude::*; # 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 // 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] #[component]
fn ReactiveComp(count: ReadOnlySignal<u32>) -> Element { fn ReactiveComp(count: ReadSignal<u32>) -> Element {
// Because `count` is reactive, the resource knows to rerun when `count` changes automatically // Because `count` is reactive, the resource knows to rerun when `count` changes automatically
let new_count = use_resource(move || async move { let new_count = use_resource(move || async move {
sleep(100).await; sleep(100).await;
+8 -4
View File
@@ -1,6 +1,6 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use crate::{use_callback, use_hook_did_run, use_signal}; 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 dioxus_signals::*;
use std::future::Future; use std::future::Future;
use std::ops::Deref; use std::ops::Deref;
@@ -152,12 +152,12 @@ impl UseFuture {
} }
/// Get the current state of the future. /// Get the current state of the future.
pub fn state(&self) -> ReadOnlySignal<UseFutureState> { pub fn state(&self) -> ReadSignal<UseFutureState> {
self.state.into() self.state.into()
} }
} }
impl From<UseFuture> for ReadOnlySignal<UseFutureState> { impl From<UseFuture> for ReadSignal<UseFutureState> {
fn from(val: UseFuture) -> Self { fn from(val: UseFuture) -> Self {
val.state.into() val.state.into()
} }
@@ -180,6 +180,10 @@ impl Readable for UseFuture {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.state.try_peek_unchecked() self.state.try_peek_unchecked()
} }
fn subscribers(&self) -> Option<Subscribers> {
self.state.subscribers()
}
} }
/// Allow calling a signal with signal() syntax /// Allow calling a signal with signal() syntax
@@ -189,6 +193,6 @@ impl Deref for UseFuture {
type Target = dyn Fn() -> UseFutureState; type Target = dyn Fn() -> UseFutureState;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) } unsafe { ReadableExt::deref_impl(self) }
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use dioxus_core::{use_after_render, use_before_render, use_hook}; 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 /// A hook that uses before/after lifecycle hooks to determine if the hook was run
#[doc = include_str!("../docs/rules_of_hooks.md")] #[doc = include_str!("../docs/rules_of_hooks.md")]
+1 -1
View File
@@ -1,4 +1,4 @@
use dioxus_signals::{Readable, Writable}; use dioxus_signals::{ReadableExt, WritableExt};
use crate::use_signal; use crate::use_signal;
+12 -8
View File
@@ -4,7 +4,7 @@ use crate::{use_callback, use_signal};
use dioxus_core::{ use dioxus_core::{
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, RenderError, spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, RenderError,
SuspendedFuture, Task, Subscribers, SuspendedFuture, Task,
}; };
use dioxus_signals::*; use dioxus_signals::*;
use futures_util::{future, pin_mut, FutureExt, StreamExt}; use futures_util::{future, pin_mut, FutureExt, StreamExt};
@@ -349,7 +349,7 @@ impl<T> Resource<T> {
) )
} }
/// 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 /// ## Example
/// ```rust, no_run /// ```rust, no_run
@@ -379,11 +379,11 @@ impl<T> Resource<T> {
/// } /// }
/// } /// }
/// ``` /// ```
pub fn state(&self) -> ReadOnlySignal<UseResourceState> { pub fn state(&self) -> ReadSignal<UseResourceState> {
self.state.into() 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 /// ## Example
/// ///
@@ -410,12 +410,12 @@ impl<T> Resource<T> {
/// } /// }
/// } /// }
/// ``` /// ```
pub fn value(&self) -> ReadOnlySignal<Option<T>> { pub fn value(&self) -> ReadSignal<Option<T>> {
self.value.into() self.value.into()
} }
/// Suspend the resource's future and only continue rendering when the future is ready /// Suspend the resource's future and only continue rendering when the future is ready
pub fn suspend(&self) -> std::result::Result<MappedSignal<T>, RenderError> { pub fn suspend(&self) -> std::result::Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
match self.state.cloned() { match self.state.cloned() {
UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => { UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => {
let task = self.task(); let task = self.task();
@@ -430,7 +430,7 @@ impl<T> Resource<T> {
} }
} }
impl<T> From<Resource<T>> for ReadOnlySignal<Option<T>> { impl<T> From<Resource<T>> for ReadSignal<Option<T>> {
fn from(val: Resource<T>) -> Self { fn from(val: Resource<T>) -> Self {
val.value.into() val.value.into()
} }
@@ -453,6 +453,10 @@ impl<T> Readable for Resource<T> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.value.try_peek_unchecked() self.value.try_peek_unchecked()
} }
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
}
} }
impl<T> IntoAttributeValue for Resource<T> impl<T> IntoAttributeValue for Resource<T>
@@ -480,6 +484,6 @@ impl<T: Clone> Deref for Resource<T> {
type Target = dyn Fn() -> Option<T>; type Target = dyn Fn() -> Option<T>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) } unsafe { ReadableExt::deref_impl(self) }
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
use dioxus_core::use_hook; use dioxus_core::use_hook;
use dioxus_signals::{ReadOnlySignal, SetCompare}; use dioxus_signals::{ReadSignal, SetCompare};
use std::hash::Hash; 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. /// 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<R: Eq + Hash>(f: impl FnMut() -> R + 'static) -> SetCompa
pub fn use_set_compare_equal<R: Eq + Hash>( pub fn use_set_compare_equal<R: Eq + Hash>(
value: R, value: R,
mut compare: SetCompare<R>, mut compare: SetCompare<R>,
) -> ReadOnlySignal<bool> { ) -> ReadSignal<bool> {
use_hook(move || compare.equal(value)) use_hook(move || compare.equal(value))
} }
+3 -3
View File
@@ -2,12 +2,12 @@ use std::cmp::Ordering;
use std::ops::DerefMut; use std::ops::DerefMut;
use crate::use_memo; use crate::use_memo;
use dioxus_signals::{ReadOnlySignal, Signal}; use dioxus_signals::{ReadSignal, Signal};
pub fn use_sorted<V: 'static, T: PartialEq>( pub fn use_sorted<V: 'static, T: PartialEq>(
collection: impl FnMut() -> Signal<V>, collection: impl FnMut() -> Signal<V>,
) -> ReadOnlySignal<Vec<T>> ) -> ReadSignal<Vec<T>>
// pub fn use_sorted<S, I, T>(iterable: impl FnMut() -> Signal<V>) -> ReadOnlySignal<T> // pub fn use_sorted<S, I, T>(iterable: impl FnMut() -> Signal<V>) -> ReadSignal<T>
// where // where
// S: Into<MaybeSignal<I>>, // S: Into<MaybeSignal<I>>,
// T: Ord, // T: Ord,
@@ -81,13 +81,13 @@ fn MyComponent(string: String) -> Element {
You can fix this issue by either: You can fix this issue by either:
- Making your data `Copy` with `ReadOnlySignal`: - Making your data `Copy` with `ReadSignal`:
```rust, no_run ```rust, no_run
# use dioxus::prelude::*; # use dioxus::prelude::*;
// `MyComponent` accepts `ReadOnlySignal<String>` which implements `Copy` // `MyComponent` accepts `ReadSignal<String>` which implements `Copy`
#[component] #[component]
fn MyComponent(string: ReadOnlySignal<String>) -> Element { fn MyComponent(string: ReadSignal<String>) -> Element {
rsx! { rsx! {
button { button {
// ✅ Because the `string` signal is `Copy`, we can copy it into the closure while still having access to it elsewhere // ✅ Because the `string` signal is `Copy`, we can copy it into the closure while still having access to it elsewhere
@@ -1,7 +1,7 @@
use dioxus_core::Element; use dioxus_core::Element;
use dioxus_core_macro::rsx; use dioxus_core_macro::rsx;
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use dioxus_signals::{GlobalSignal, Readable}; use dioxus_signals::{GlobalSignal, ReadableExt};
#[allow(deprecated)] #[allow(deprecated)]
use crate::hooks::use_router; use crate::hooks::use_router;
@@ -1,7 +1,7 @@
use dioxus_core::{Element, VNode}; use dioxus_core::{Element, VNode};
use dioxus_core_macro::{rsx, Props}; use dioxus_core_macro::{rsx, Props};
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use dioxus_signals::{GlobalSignal, Readable}; use dioxus_signals::{GlobalSignal, ReadableExt};
use tracing::error; use tracing::error;
+1 -1
View File
@@ -7,7 +7,7 @@ use dioxus_core_macro::{rsx, Props};
use dioxus_html::{ use dioxus_html::{
self as dioxus_elements, ModifiersInteraction, MountedEvent, MouseEvent, PointerInteraction, self as dioxus_elements, ModifiersInteraction, MountedEvent, MouseEvent, PointerInteraction,
}; };
use dioxus_signals::{GlobalSignal, Owner, Readable}; use dioxus_signals::{GlobalSignal, Owner, ReadableExt};
use tracing::error; use tracing::error;
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::{provide_router_context, routable::Routable, router_cfg::RouterConfig, Outlet}; use crate::{provide_router_context, routable::Routable, router_cfg::RouterConfig, Outlet};
use dioxus_core::{provide_context, use_hook, Callback, Element}; use dioxus_core::{provide_context, use_hook, Callback, Element};
use dioxus_core_macro::{rsx, Props}; use dioxus_core_macro::{rsx, Props};
use dioxus_signals::{GlobalSignal, Owner, Readable}; use dioxus_signals::{GlobalSignal, Owner, ReadableExt};
/// The props for [`Router`]. /// The props for [`Router`].
#[derive(Props)] #[derive(Props)]
+1 -1
View File
@@ -7,7 +7,7 @@ use std::{
use dioxus_core::{provide_context, Element, ReactiveContext, ScopeId}; use dioxus_core::{provide_context, Element, ReactiveContext, ScopeId};
use dioxus_history::history; use dioxus_history::history;
use dioxus_signals::{CopyValue, Readable, Signal, Writable}; use dioxus_signals::{CopyValue, ReadableExt, Signal, Writable};
use crate::{ use crate::{
components::child_router::consume_child_route_mapping, navigation::NavigationTarget, components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
+1 -1
View File
@@ -10,7 +10,7 @@ use dioxus_document::{
Document, Eval, LinkProps, MetaProps, NoOpDocument, ScriptProps, StyleProps, Document, Eval, LinkProps, MetaProps, NoOpDocument, ScriptProps, StyleProps,
}; };
use dioxus_html as dioxus_elements; use dioxus_html as dioxus_elements;
use dioxus_signals::{GlobalSignal, Readable}; use dioxus_signals::{GlobalSignal, ReadableExt};
use dioxus_ssr::Renderer; use dioxus_ssr::Renderer;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::LazyLock; use std::sync::LazyLock;
+1 -1
View File
@@ -119,7 +119,7 @@ fn App() -> Element {
} }
#[component] #[component]
fn Child(signal: ReadOnlySignal<usize>) -> Element { fn Child(signal: ReadSignal<usize>) -> Element {
rsx! { rsx! {
"{signal}" "{signal}"
} }
+3 -11
View File
@@ -27,24 +27,16 @@ fn app() -> Element {
} }
for i in 0..vec.len() { 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] #[component]
fn Child(count: MappedSignal<i32>) -> Element { fn Child(count: WriteSignal<i32>) -> Element {
use_memo({
to_owned![count];
move || {
let value = count.read();
println!("Child value: {value}");
}
});
rsx! { rsx! {
div { div {
"Child: {count}" "{count}"
} }
} }
} }
@@ -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 //! This is done thanks to a conversion by the #[component] macro
use dioxus::prelude::*; use dioxus::prelude::*;
@@ -22,7 +22,7 @@ fn app() -> Element {
} }
#[component] #[component]
fn Child(count: ReadOnlySignal<i32>, children: Element) -> Element { fn Child(count: ReadSignal<i32>, children: Element) -> Element {
rsx! { rsx! {
div { "Count: {count}" } div { "Count: {count}" }
{children} {children}
+1 -1
View File
@@ -18,6 +18,6 @@ fn app() -> Element {
} }
#[component] #[component]
fn Child(signal: ReadOnlySignal<usize>) -> Element { fn Child(signal: ReadSignal<usize>) -> Element {
rsx! { "{signal}" } rsx! { "{signal}" }
} }
+381
View File
@@ -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<T> = ReadSignal<T>;
/// A boxed version of [Readable] that can be used to store any readable type.
pub struct ReadSignal<T: ?Sized + 'static> {
value: CopyValue<Box<dyn Readable<Target = T, Storage = UnsyncStorage>>>,
}
impl<T: ?Sized + 'static> ReadSignal<T> {
/// Create a new boxed readable value.
pub fn new(value: impl Readable<Target = T, Storage = UnsyncStorage> + '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<T: ?Sized + 'static> Clone for ReadSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized + 'static> Copy for ReadSignal<T> {}
impl<T: ?Sized + 'static> PartialEq for ReadSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl<T: Default + 'static> Default for ReadSignal<T> {
fn default() -> Self {
Self::new(Signal::new(T::default()))
}
}
read_impls!(ReadSignal<T>);
impl<T> IntoAttributeValue for ReadSignal<T>
where
T: Clone + IntoAttributeValue + 'static,
{
fn into_value(self) -> dioxus_core::AttributeValue {
self.with(|f| f.clone().into_value())
}
}
impl<T> IntoDynNode for ReadSignal<T>
where
T: Clone + IntoDynNode + 'static,
{
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
self.with(|f| f.clone().into_dyn_node())
}
}
impl<T: Clone + 'static> Deref for ReadSignal<T> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
unsafe { ReadableExt::deref_impl(self) }
}
}
impl<T: 'static> Readable for ReadSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.value
.try_peek_unchecked()
.unwrap()
.try_read_unchecked()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
}
}
// We can't implement From<impl Readable<Target = T, Storage = S> + 'static> for ReadSignal<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for ReadSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T: PartialEq> From<Memo<T>> for ReadSignal<T> {
fn from(value: Memo<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for ReadSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for ReadSignal<R>
where
T: Readable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
}
}
impl<V, O, F> From<MappedSignal<O, V, F>> for ReadSignal<O>
where
O: ?Sized,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
{
fn from(value: MappedSignal<O, V, F>) -> Self {
Self::new(value)
}
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for ReadSignal<O>
where
O: ?Sized,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: 'static,
{
fn from(value: MappedMutSignal<O, V, F, FMut>) -> Self {
Self::new(value)
}
}
/// A boxed version of [Writable] that can be used to store any writable type.
pub struct WriteSignal<T: ?Sized + 'static> {
value: CopyValue<
Box<dyn Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = Box<dyn Any>>>,
>,
}
impl<T: ?Sized> WriteSignal<T> {
/// Create a new boxed writable value.
pub fn new<M>(
value: impl Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = M> + 'static,
) -> Self {
Self {
value: CopyValue::new(Box::new(BoxWriteMetadata::new(value))),
}
}
}
struct BoxWriteMetadata<W> {
value: W,
}
impl<W: Writable> BoxWriteMetadata<W> {
fn new(value: W) -> Self {
Self { value }
}
}
impl<W: Readable> Readable for BoxWriteMetadata<W> {
type Target = W::Target;
type Storage = W::Storage;
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.value.try_read_unchecked()
}
fn try_peek_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.value.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
}
}
impl<W: Writable> Writable for BoxWriteMetadata<W> {
type WriteMetadata = Box<dyn Any>;
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
self.value
.try_write_unchecked()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write(&mut self) -> crate::WritableRef<'_, Self> {
self.value
.write()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
fn try_write(
&mut self,
) -> Result<crate::WritableRef<'_, Self>, generational_box::BorrowMutError> {
self.value
.try_write()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value
.write_unchecked()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
}
impl<T: ?Sized> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized> Copy for WriteSignal<T> {}
impl<T: ?Sized> PartialEq for WriteSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
read_impls!(WriteSignal<T>);
write_impls!(WriteSignal<T>);
impl<T> IntoAttributeValue for WriteSignal<T>
where
T: Clone + IntoAttributeValue + 'static,
{
fn into_value(self) -> dioxus_core::AttributeValue {
self.with(|f| f.clone().into_value())
}
}
impl<T> IntoDynNode for WriteSignal<T>
where
T: Clone + IntoDynNode + 'static,
{
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
self.with(|f| f.clone().into_dyn_node())
}
}
impl<T: Clone + 'static> Deref for WriteSignal<T> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
unsafe { ReadableExt::deref_impl(self) }
}
}
impl<T: 'static> Readable for WriteSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.value
.try_peek_unchecked()
.unwrap()
.try_read_unchecked()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
}
}
impl<T: 'static> Writable for WriteSignal<T> {
type WriteMetadata = Box<dyn Any>;
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value.try_peek_unchecked().unwrap().write_unchecked()
}
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
self.value
.try_peek_unchecked()
.unwrap()
.try_write_unchecked()
}
}
// We can't implement From<impl Writable<Target = T, Storage = S> + 'static> for Write<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for WriteSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for WriteSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for WriteSignal<R>
where
T: Writable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
}
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for WriteSignal<O>
where
O: ?Sized,
V: Writable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: Fn(&mut V::Target) -> &mut O + 'static,
{
fn from(value: MappedMutSignal<O, V, F, FMut>) -> Self {
Self::new(value)
}
}
+11 -30
View File
@@ -1,16 +1,19 @@
#![allow(clippy::unnecessary_operation)] #![allow(clippy::unnecessary_operation)]
#![allow(clippy::no_effect)] #![allow(clippy::no_effect)]
use dioxus_core::Subscribers;
use dioxus_core::{current_owner, current_scope_id, ScopeId}; use dioxus_core::{current_owner, current_scope_id, ScopeId};
use generational_box::{BorrowResult, GenerationalBox, GenerationalBoxId, Storage, UnsyncStorage}; use generational_box::{BorrowResult, GenerationalBox, GenerationalBoxId, Storage, UnsyncStorage};
use std::ops::Deref; use std::ops::Deref;
use crate::read_impls; use crate::read_impls;
use crate::Readable; use crate::Readable;
use crate::ReadableExt;
use crate::ReadableRef; use crate::ReadableRef;
use crate::Writable; use crate::Writable;
use crate::WritableRef; 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. /// CopyValue is a wrapper around a value to make the value mutable and Copy.
/// ///
@@ -151,43 +154,21 @@ impl<T: 'static, S: Storage<T>> Readable for CopyValue<T, S> {
crate::warnings::copy_value_hoisted(self, std::panic::Location::caller()); crate::warnings::copy_value_hoisted(self, std::panic::Location::caller());
self.value.try_read() self.value.try_read()
} }
fn subscribers(&self) -> Option<Subscribers> {
None
}
} }
impl<T: 'static, S: Storage<T>> Writable for CopyValue<T, S> { impl<T: 'static, S: Storage<T>> Writable for CopyValue<T, S> {
type Mut<'a, R: ?Sized + 'static> = S::Mut<'a, R>; type WriteMetadata = ();
fn map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> &mut U>(
mut_: Self::Mut<'_, I>,
f: F,
) -> Self::Mut<'_, U> {
S::map_mut(mut_, f)
}
fn try_map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> Option<&mut U>>(
mut_: Self::Mut<'_, I>,
f: F,
) -> Option<Self::Mut<'_, U>> {
S::try_map_mut(mut_, f)
}
fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>(
mut_: Self::Mut<'a, R>,
) -> Self::Mut<'b, R> {
S::downcast_lifetime_mut(mut_)
}
#[track_caller] #[track_caller]
fn try_write_unchecked( fn try_write_unchecked(
&self, &self,
) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> { ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
crate::warnings::copy_value_hoisted(self, std::panic::Location::caller()); crate::warnings::copy_value_hoisted(self, std::panic::Location::caller());
self.value.try_write() self.value.try_write().map(WriteLock::new)
}
#[track_caller]
fn set(&mut self, value: T) {
crate::warnings::copy_value_hoisted(self, std::panic::Location::caller());
self.value.set(value);
} }
} }
@@ -202,7 +183,7 @@ impl<T: Copy, S: Storage<T>> Deref for CopyValue<T, S> {
type Target = dyn Fn() -> T; type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) } unsafe { ReadableExt::deref_impl(self) }
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use super::{Global, InitializeFromFunction}; use super::{Global, InitializeFromFunction};
use crate::read::Readable; use crate::read::ReadableExt;
use crate::read_impls; use crate::read_impls;
use crate::Memo; use crate::Memo;
+9 -29
View File
@@ -1,4 +1,4 @@
use dioxus_core::ScopeId; use dioxus_core::{ScopeId, Subscribers};
use generational_box::BorrowResult; use generational_box::BorrowResult;
use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc}; use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc};
@@ -8,7 +8,7 @@ pub use memo::*;
mod signal; mod signal;
pub use 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 /// A trait for an item that can be constructed from an initialization function
pub trait InitializeFromFunction<T> { pub trait InitializeFromFunction<T> {
@@ -39,7 +39,7 @@ where
type Target = dyn Fn() -> R; type Target = dyn Fn() -> R;
fn deref(&self) -> &Self::Target { 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<ReadableRef<'static, Self>> { fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.resolve().try_peek_unchecked() self.resolve().try_peek_unchecked()
} }
fn subscribers(&self) -> Option<Subscribers> {
self.resolve().subscribers()
}
} }
impl<T: Clone + 'static, R: 'static> Writable for Global<T, R> impl<T: Clone + 'static, R: 'static> Writable for Global<T, R>
where where
T: Writable<Target = R> + InitializeFromFunction<R>, T: Writable<Target = R> + InitializeFromFunction<R>,
{ {
type Mut<'a, Read: ?Sized + 'static> = T::Mut<'a, Read>; type WriteMetadata = T::WriteMetadata;
fn map_mut<I: ?Sized, U: ?Sized + 'static, F: FnOnce(&mut I) -> &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<Self::Mut<'_, U>> {
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_)
}
#[track_caller] #[track_caller]
fn try_write_unchecked( fn try_write_unchecked(
@@ -106,7 +86,7 @@ where
T: Writable<Target = R> + InitializeFromFunction<R>, T: Writable<Target = R> + InitializeFromFunction<R>,
{ {
/// Write this value /// 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() self.resolve().try_write_unchecked().unwrap()
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use super::{Global, InitializeFromFunction}; use super::{Global, InitializeFromFunction};
use crate::read::Readable; use crate::read::ReadableExt;
use crate::read_impls; use crate::read_impls;
use crate::Signal; use crate::Signal;
+52 -1
View File
@@ -4,6 +4,7 @@
/// ```rust /// ```rust
/// use generational_box::*; /// use generational_box::*;
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_core::Subscribers;
/// ///
/// struct MyCopyValue<T: 'static, S: Storage<T>> { /// struct MyCopyValue<T: 'static, S: Storage<T>> {
/// value: CopyValue<T, S>, /// value: CopyValue<T, S>,
@@ -30,6 +31,10 @@
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { /// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// self.value.try_read_unchecked() /// self.value.try_read_unchecked()
/// } /// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// self.value.subscribers()
/// }
/// } /// }
/// ///
/// default_impl!(MyCopyValue<T, S: Storage<T>>); /// default_impl!(MyCopyValue<T, S: Storage<T>>);
@@ -72,6 +77,7 @@ macro_rules! default_impl {
/// ```rust /// ```rust
/// use generational_box::*; /// use generational_box::*;
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_core::Subscribers;
/// ///
/// struct MyCopyValue<T: 'static, S: Storage<T>> { /// struct MyCopyValue<T: 'static, S: Storage<T>> {
/// value: CopyValue<T, S>, /// value: CopyValue<T, S>,
@@ -92,6 +98,10 @@ macro_rules! default_impl {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { /// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// self.value.try_read_unchecked() /// self.value.try_read_unchecked()
/// } /// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// self.value.subscribers()
/// }
/// } /// }
/// ///
/// read_impls!(MyCopyValue<T, S: Storage<T>>); /// read_impls!(MyCopyValue<T, S: Storage<T>>);
@@ -145,6 +155,7 @@ macro_rules! read_impls {
/// ```rust /// ```rust
/// use generational_box::*; /// use generational_box::*;
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_core::Subscribers;
/// ///
/// struct MyCopyValue<T: 'static, S: Storage<T>> { /// struct MyCopyValue<T: 'static, S: Storage<T>> {
/// value: CopyValue<T, S>, /// value: CopyValue<T, S>,
@@ -165,6 +176,10 @@ macro_rules! read_impls {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { /// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// self.value.try_read_unchecked() /// self.value.try_read_unchecked()
/// } /// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// self.value.subscribers()
/// }
/// } /// }
/// ///
/// fmt_impls!(MyCopyValue<T, S: Storage<T>>); /// fmt_impls!(MyCopyValue<T, S: Storage<T>>);
@@ -221,6 +236,7 @@ macro_rules! fmt_impls {
/// ```rust /// ```rust
/// use generational_box::*; /// use generational_box::*;
/// use dioxus::prelude::*; /// use dioxus::prelude::*;
/// use dioxus_core::Subscribers;
/// ///
/// struct MyCopyValue<T: 'static, S: Storage<T>> { /// struct MyCopyValue<T: 'static, S: Storage<T>> {
/// value: CopyValue<T, S>, /// value: CopyValue<T, S>,
@@ -241,6 +257,10 @@ macro_rules! fmt_impls {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { /// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// self.value.try_read_unchecked() /// self.value.try_read_unchecked()
/// } /// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// self.value.subscribers()
/// }
/// } /// }
/// ///
/// eq_impls!(MyCopyValue<T, S: Storage<T>>); /// eq_impls!(MyCopyValue<T, S: Storage<T>>);
@@ -307,7 +327,6 @@ macro_rules! eq_impls {
/// &self, /// &self,
/// ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> { /// ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
/// self.value.try_write_unchecked() /// self.value.try_write_unchecked()
///
/// } /// }
/// ///
/// //... /// //...
@@ -332,6 +351,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::Add<T> > std::ops::Add<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
type Output = T; type Output = T;
@@ -345,6 +368,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::AddAssign<T> > std::ops::AddAssign<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
#[track_caller] #[track_caller]
fn add_assign(&mut self, rhs: T) { fn add_assign(&mut self, rhs: T) {
@@ -356,6 +383,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::SubAssign<T> > std::ops::SubAssign<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
#[track_caller] #[track_caller]
fn sub_assign(&mut self, rhs: T) { fn sub_assign(&mut self, rhs: T) {
@@ -367,6 +398,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::Sub<T> > std::ops::Sub<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
type Output = T; type Output = T;
@@ -380,6 +415,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::MulAssign<T> > std::ops::MulAssign<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
#[track_caller] #[track_caller]
fn mul_assign(&mut self, rhs: T) { fn mul_assign(&mut self, rhs: T) {
@@ -391,6 +430,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::Mul<T> > std::ops::Mul<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
type Output = T; type Output = T;
@@ -404,6 +447,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::DivAssign<T> > std::ops::DivAssign<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
#[track_caller] #[track_caller]
fn div_assign(&mut self, rhs: T) { fn div_assign(&mut self, rhs: T) {
@@ -415,6 +462,10 @@ macro_rules! write_impls {
$(, $gen $(: $gen_bound)?)* $(, $gen $(: $gen_bound)?)*
> std::ops::Div<T> > std::ops::Div<T>
for $ty<T $(, $gen)*> for $ty<T $(, $gen)*>
$(
where
$($extra_bound_ty: $extra_bound,)*
)?
{ {
type Output = T; type Output = T;
+6 -3
View File
@@ -10,12 +10,12 @@ pub use copy_value::*;
pub(crate) mod signal; pub(crate) mod signal;
pub use signal::*; pub use signal::*;
mod read_only_signal;
pub use read_only_signal::*;
mod map; mod map;
pub use map::*; pub use map::*;
mod map_mut;
pub use map_mut::*;
mod set_compare; mod set_compare;
pub use set_compare::*; pub use set_compare::*;
@@ -41,3 +41,6 @@ mod props;
pub use props::*; pub use props::*;
pub mod warnings; pub mod warnings;
mod boxed;
pub use boxed::*;
+57 -37
View File
@@ -1,94 +1,114 @@
use crate::{read::Readable, read_impls, ReadableRef}; use crate::{read::Readable, read_impls, ReadableExt, ReadableRef};
use dioxus_core::IntoAttributeValue; use dioxus_core::{IntoAttributeValue, Subscribers};
use generational_box::{AnyStorage, BorrowResult, UnsyncStorage}; use generational_box::{AnyStorage, BorrowResult};
use std::{ops::Deref, rc::Rc}; use std::ops::Deref;
/// A read only signal that has been mapped to a new type. /// A read only signal that has been mapped to a new type.
pub struct MappedSignal<O: ?Sized + 'static, S: AnyStorage = UnsyncStorage> { pub struct MappedSignal<O: ?Sized + 'static, V: Readable, F = fn(&<V as Readable>::Target) -> &O> {
try_read: Rc<dyn Fn() -> Result<S::Ref<'static, O>, generational_box::BorrowError> + 'static>, value: V,
try_peek: Rc<dyn Fn() -> Result<S::Ref<'static, O>, generational_box::BorrowError> + 'static>, map_fn: F,
_marker: std::marker::PhantomData<O>,
} }
impl<O: ?Sized, S: AnyStorage> Clone for MappedSignal<O, S> { impl<V, O, F> Clone for MappedSignal<O, V, F>
where
V: Readable + Clone,
F: Clone,
{
fn clone(&self) -> Self { fn clone(&self) -> Self {
MappedSignal { MappedSignal {
try_read: self.try_read.clone(), value: self.value.clone(),
try_peek: self.try_peek.clone(), map_fn: self.map_fn.clone(),
_marker: std::marker::PhantomData,
} }
} }
} }
impl<O, S> MappedSignal<O, S> impl<V, O, F> Copy for MappedSignal<O, V, F>
where
V: Readable + Copy,
F: Copy,
{
}
impl<V, O, F> MappedSignal<O, V, F>
where where
O: ?Sized, O: ?Sized,
S: AnyStorage, V: Readable,
F: Fn(&V::Target) -> &O,
{ {
/// Create a new mapped signal. /// Create a new mapped signal.
pub(crate) fn new( pub(crate) fn new(value: V, map_fn: F) -> Self {
try_read: Rc< MappedSignal {
dyn Fn() -> Result<S::Ref<'static, O>, generational_box::BorrowError> + 'static, value,
>, map_fn,
try_peek: Rc< _marker: std::marker::PhantomData,
dyn Fn() -> Result<S::Ref<'static, O>, generational_box::BorrowError> + 'static, }
>,
) -> Self {
MappedSignal { try_read, try_peek }
} }
} }
impl<O, S> Readable for MappedSignal<O, S> impl<V, O, F> Readable for MappedSignal<O, V, F>
where where
O: ?Sized, O: ?Sized,
S: AnyStorage, V: Readable,
F: Fn(&V::Target) -> &O,
{ {
type Target = O; type Target = O;
type Storage = S; type Storage = V::Storage;
fn try_read_unchecked( fn try_read_unchecked(
&self, &self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> { ) -> Result<ReadableRef<'static, Self>, 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<ReadableRef<'static, Self>> { fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
(self.try_peek)() let value = self.value.try_peek_unchecked()?;
Ok(V::Storage::map(value, |v| (self.map_fn)(v)))
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
} }
} }
impl<O, S> IntoAttributeValue for MappedSignal<O, S> impl<V, O, F> IntoAttributeValue for MappedSignal<O, V, F>
where where
O: Clone + IntoAttributeValue, O: Clone + IntoAttributeValue,
S: AnyStorage, V: Readable,
F: Fn(&V::Target) -> &O,
{ {
fn into_value(self) -> dioxus_core::AttributeValue { fn into_value(self) -> dioxus_core::AttributeValue {
self.with(|f| f.clone().into_value()) self.with(|f| f.clone().into_value())
} }
} }
impl<O, S> PartialEq for MappedSignal<O, S> impl<V, O, F> PartialEq for MappedSignal<O, V, F>
where where
O: ?Sized, O: ?Sized,
S: AnyStorage, V: Readable + PartialEq,
F: PartialEq,
{ {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.try_peek, &other.try_peek) self.value == other.value && self.map_fn == other.map_fn
&& std::ptr::eq(&self.try_read, &other.try_read)
} }
} }
/// Allow calling a signal with signal() syntax /// Allow calling a signal with signal() syntax
/// ///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc /// Currently only limited to clone types, though could probably specialize for string/arc/rc
impl<O, S> Deref for MappedSignal<O, S> impl<V, O, F> Deref for MappedSignal<O, V, F>
where where
O: Clone, O: Clone,
S: AnyStorage + 'static, V: Readable + 'static,
F: Fn(&V::Target) -> &O + 'static,
{ {
type Target = dyn Fn() -> O; type Target = dyn Fn() -> O;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) } unsafe { ReadableExt::deref_impl(self) }
} }
} }
read_impls!(MappedSignal<T, S: AnyStorage>); read_impls!(MappedSignal<T, V, F> where V: Readable, F: Fn(&V::Target) -> &T);
+150
View File
@@ -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(&<V as Readable>::Target) -> &O,
FMut = fn(&mut <V as Readable>::Target) -> &mut O,
> {
value: V,
map_fn: F,
map_fn_mut: FMut,
_marker: std::marker::PhantomData<O>,
}
impl<V, O, F, FMut> Clone for MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> Copy for MappedMutSignal<O, V, F, FMut>
where
V: Readable + Copy,
F: Copy,
FMut: Copy,
{
}
impl<V, O, F, FMut> MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> Readable for MappedMutSignal<O, V, F, FMut>
where
O: ?Sized,
V: Readable,
F: Fn(&V::Target) -> &O,
{
type Target = O;
type Storage = V::Storage;
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, 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<ReadableRef<'static, Self>> {
let value = self.value.try_peek_unchecked()?;
Ok(V::Storage::map(value, |v| (self.map_fn)(v)))
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
}
}
impl<V, O, F, FMut> Writable for MappedMutSignal<O, V, F, FMut>
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<WritableRef<'static, Self>, generational_box::BorrowMutError> {
let value = self.value.try_write_unchecked()?;
Ok(WriteLock::map(value, |v| (self.map_fn_mut)(v)))
}
}
impl<V, O, F, FMut> IntoAttributeValue for MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> PartialEq for MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> Deref for MappedMutSignal<O, V, F, FMut>
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<T, V, F, FMut> where V: Readable, F: Fn(&V::Target) -> &T);
write_impls!(MappedMutSignal<T, V, F, FMut> where V: Writable, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T);
+8 -12
View File
@@ -1,7 +1,7 @@
use crate::write::Writable; use crate::write::Writable;
use crate::CopyValue;
use crate::{read::Readable, ReadableRef, Signal}; use crate::{read::Readable, ReadableRef, Signal};
use crate::{read_impls, GlobalMemo}; use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt};
use crate::{CopyValue, ReadOnlySignal};
use std::{ use std::{
cell::RefCell, cell::RefCell,
ops::Deref, ops::Deref,
@@ -10,6 +10,7 @@ use std::{
use dioxus_core::{ use dioxus_core::{
current_scope_id, spawn_isomorphic, IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, current_scope_id, spawn_isomorphic, IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId,
Subscribers,
}; };
use futures_util::StreamExt; use futures_util::StreamExt;
use generational_box::{AnyStorage, BorrowResult, UnsyncStorage}; use generational_box::{AnyStorage, BorrowResult, UnsyncStorage};
@@ -28,15 +29,6 @@ pub struct Memo<T: 'static> {
update: CopyValue<UpdateInformation<T>>, update: CopyValue<UpdateInformation<T>>,
} }
impl<T> From<Memo<T>> for ReadOnlySignal<T>
where
T: PartialEq,
{
fn from(val: Memo<T>) -> Self {
ReadOnlySignal::new(val.inner)
}
}
impl<T: 'static> Memo<T> { impl<T: 'static> Memo<T> {
/// Create a new memo /// Create a new memo
#[track_caller] #[track_caller]
@@ -203,6 +195,10 @@ where
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> { fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.inner.try_peek_unchecked() self.inner.try_peek_unchecked()
} }
fn subscribers(&self) -> Option<Subscribers> {
self.inner.subscribers()
}
} }
impl<T> IntoAttributeValue for Memo<T> impl<T> IntoAttributeValue for Memo<T>
@@ -236,7 +232,7 @@ where
type Target = dyn Fn() -> T; type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) } unsafe { ReadableExt::deref_impl(self) }
} }
} }
+7 -7
View File
@@ -1,15 +1,15 @@
use crate::{ReadOnlySignal, Signal}; use crate::{ReadSignal, Signal};
use dioxus_core::SuperFrom; use dioxus_core::SuperFrom;
#[doc(hidden)] #[doc(hidden)]
pub struct SignalFromMarker<M>(std::marker::PhantomData<M>); pub struct ReadFromMarker<M>(std::marker::PhantomData<M>);
impl<T, O, M> SuperFrom<T, SignalFromMarker<M>> for ReadOnlySignal<O> impl<T, O, M> SuperFrom<T, ReadFromMarker<M>> for ReadSignal<O>
where where
O: SuperFrom<T, M>, O: SuperFrom<T, M> + 'static,
{ {
fn super_from(input: T) -> Self { 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)] #[allow(unused)]
fn into_signal_compiles() { fn into_signal_compiles() {
use dioxus_core::SuperInto; use dioxus_core::SuperInto;
fn takes_signal_string<M>(_: impl SuperInto<ReadOnlySignal<String>, M>) {} fn takes_signal_string<M>(_: impl SuperInto<ReadSignal<String>, M>) {}
fn takes_option_signal_string<M>(_: impl SuperInto<ReadOnlySignal<Option<String>>, M>) {} fn takes_option_signal_string<M>(_: impl SuperInto<ReadSignal<Option<String>>, M>) {}
fn don_t_run() { fn don_t_run() {
takes_signal_string("hello world"); takes_signal_string("hello world");
+66 -72
View File
@@ -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. /// A reference to a value that can be read from.
#[allow(type_alias_bounds)] #[allow(type_alias_bounds)]
pub type ReadableRef<'a, T: Readable, O = <T as Readable>::Target> = pub type ReadableRef<'a, T: Readable, O = <T as Readable>::Target> =
<T::Storage as AnyStorage>::Ref<'a, O>; <T::Storage as AnyStorage>::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 /// # Example
/// ```rust /// ```rust
@@ -21,7 +22,7 @@ pub type ReadableRef<'a, T: Readable, O = <T as Readable>::Target> =
/// static COUNT: GlobalSignal<i32> = Signal::global(|| 0); /// static COUNT: GlobalSignal<i32> = Signal::global(|| 0);
/// ///
/// fn MyComponent(count: Signal<i32>) -> Element { /// fn MyComponent(count: Signal<i32>) -> 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 doubled = use_memo(move || double(&count));
/// let global_count_doubled = use_memo(|| double(&COUNT)); /// let global_count_doubled = use_memo(|| double(&COUNT));
/// rsx! { /// rsx! {
@@ -41,64 +42,27 @@ pub trait Readable {
/// The type of the storage this readable uses. /// The type of the storage this readable uses.
type Storage: AnyStorage; 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. /// Try to get a reference to the value without checking the lifetime. This will subscribe the current scope to the signal.
/// ///
/// 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. /// NOTE: This method is completely safe because borrow checking is done at runtime.
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, 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.
/// ///
/// # Example /// NOTE: This method is completely safe because borrow checking is done at runtime.
/// ```rust fn try_peek_unchecked(
/// # use dioxus::prelude::*; &self,
/// fn List(list: Signal<Vec<i32>>) -> Element { ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>;
/// rsx! {
/// for index in 0..list.len() { /// Get the underlying subscriber list for this readable. This is used to track when the value changes and notify subscribers.
/// // We can use the `map` method to provide a view into the single item in the list that the child component will render fn subscribers(&self) -> Option<Subscribers>;
/// 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: MappedSignal<i32>) -> Element {
/// rsx! {
/// div { "Item: {item}" }
/// }
/// }
/// ```
fn map<O>(self, f: impl Fn(&Self::Target) -> &O + 'static) -> MappedSignal<O, Self::Storage>
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_| <Self::Storage as AnyStorage>::map(ref_, |r| mapping(r)))
}
})
as Rc<
dyn Fn() -> Result<ReadableRef<'static, Self, O>, generational_box::BorrowError>
+ 'static,
>;
let try_peek = Rc::new({
let self_ = self.clone();
let mapping = mapping.clone();
move || {
self_
.try_peek_unchecked()
.map(|ref_| <Self::Storage as AnyStorage>::map(ref_, |r| mapping(r)))
}
})
as Rc<
dyn Fn() -> Result<ReadableRef<'static, Self, O>, generational_box::BorrowError>
+ 'static,
>;
MappedSignal::new(try_read, try_peek)
} }
/// 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. /// 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 /// 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 /// using the signal() syntax to read and subscribe to its value
@@ -122,13 +86,6 @@ pub trait Readable {
self.try_read_unchecked().unwrap() 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<ReadableRef<'static, Self>, generational_box::BorrowError>;
/// Get the current value of the state without subscribing to updates. If the value has been dropped, this will panic. /// Get the current value of the state without subscribing to updates. If the value has been dropped, this will panic.
/// ///
/// # Example /// # Example
@@ -184,13 +141,37 @@ pub trait Readable {
self.try_peek_unchecked().unwrap() self.try_peek_unchecked().unwrap()
} }
/// Try to peek the current value of the signal without subscribing to updates. If the value has /// 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.
/// been dropped, this will return an error.
/// ///
/// NOTE: This method is completely safe because borrow checking is done at runtime. /// 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.
fn try_peek_unchecked( ///
&self, /// # Example
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>; /// ```rust
/// # use dioxus::prelude::*;
/// fn List(list: Signal<Vec<i32>>) -> 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<i32>) -> Element {
/// rsx! {
/// div { "Item: {item}" }
/// }
/// }
/// ```
fn map<F, O>(self, f: F) -> MappedSignal<O, Self, F>
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. /// Clone the inner value and return it. If the value has been dropped, this will panic.
#[track_caller] #[track_caller]
@@ -263,6 +244,19 @@ pub trait Readable {
} }
} }
impl<R: Readable + ?Sized> ReadableExt for R {}
/// An extension trait for `Readable` types that can be boxed into a trait object.
pub trait ReadableBoxExt: Readable<Storage = UnsyncStorage> {
/// 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<Self::Target>
where
Self: Sized + 'static,
{
ReadSignal::new(self)
}
}
/// An extension trait for `Readable<Vec<T>>` that provides some convenience methods. /// An extension trait for `Readable<Vec<T>>` that provides some convenience methods.
pub trait ReadableVecExt<T: 'static>: Readable<Target = Vec<T>> { pub trait ReadableVecExt<T: 'static>: Readable<Target = Vec<T>> {
/// Returns the length of the inner vector. /// Returns the length of the inner vector.
-149
View File
@@ -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<T: 'static, S: Storage<SignalData<T>> = UnsyncStorage> {
inner: Signal<T, S>,
}
/// A signal that can only be read from.
pub type ReadSignal<T, S = UnsyncStorage> = ReadOnlySignal<T, S>;
impl<T: 'static, S: Storage<SignalData<T>>> From<Signal<T, S>> for ReadOnlySignal<T, S> {
fn from(inner: Signal<T, S>) -> Self {
Self { inner }
}
}
impl<T: 'static> ReadOnlySignal<T> {
/// Create a new read-only signal.
#[track_caller]
pub fn new(signal: Signal<T>) -> Self {
Self::new_maybe_sync(signal)
}
}
impl<T: 'static, S: Storage<SignalData<T>>> ReadOnlySignal<T, S> {
/// Create a new read-only signal that is maybe sync.
#[track_caller]
pub fn new_maybe_sync(signal: Signal<T, S>) -> 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<T, S: Storage<SignalData<T>>> Readable for ReadOnlySignal<T, S> {
type Target = T;
type Storage = S;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, 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<S::Ref<'static, T>> {
self.inner.try_peek_unchecked()
}
}
#[cfg(feature = "serialize")]
impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>>> serde::Serialize
for ReadOnlySignal<T, Store>
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.read().serialize(serializer)
}
}
#[cfg(feature = "serialize")]
impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>>>
serde::Deserialize<'de> for ReadOnlySignal<T, Store>
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new_maybe_sync(Signal::new_maybe_sync(
T::deserialize(deserializer)?,
)))
}
}
impl<T> IntoAttributeValue for ReadOnlySignal<T>
where
T: Clone + IntoAttributeValue,
{
fn into_value(self) -> dioxus_core::AttributeValue {
self.with(|f| f.clone().into_value())
}
}
impl<T> IntoDynNode for ReadOnlySignal<T>
where
T: Clone + IntoDynNode,
{
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
self().into_dyn_node()
}
}
impl<T: 'static, S: Storage<SignalData<T>>> PartialEq for ReadOnlySignal<T, S> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T: Clone, S: Storage<SignalData<T>> + 'static> Deref for ReadOnlySignal<T, S> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) }
}
}
read_impls!(
ReadOnlySignal<T, S> where
S: Storage<SignalData<T>>
);
default_impl!(
ReadOnlySignal<T, S> where
S: Storage<SignalData<T>>
);
impl<T: 'static, S: Storage<SignalData<T>>> Clone for ReadOnlySignal<T, S> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static, S: Storage<SignalData<T>>> Copy for ReadOnlySignal<T, S> {}
+5 -4
View File
@@ -1,12 +1,11 @@
use crate::write::Writable; use crate::{write::Writable, ReadableExt};
use std::hash::Hash; use std::hash::Hash;
use crate::read::Readable;
use dioxus_core::ReactiveContext; use dioxus_core::ReactiveContext;
use futures_util::StreamExt; use futures_util::StreamExt;
use generational_box::{Storage, UnsyncStorage}; use generational_box::{Storage, UnsyncStorage};
use crate::{CopyValue, ReadOnlySignal, Signal, SignalData}; use crate::{CopyValue, ReadSignal, Signal, SignalData};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
/// An object that can efficiently compare a value to a set of values. /// An object that can efficiently compare a value to a set of values.
@@ -66,9 +65,11 @@ impl<R: Eq + Hash, S: Storage<SignalData<bool>>> SetCompare<R, S> {
SetCompare { subscribers } SetCompare { subscribers }
} }
}
impl<R: Eq + Hash> SetCompare<R> {
/// Returns a signal which is true when the value is equal to the value passed to this function. /// 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<bool, S> { pub fn equal(&mut self, value: R) -> ReadSignal<bool> {
let subscribers = self.subscribers.write(); let subscribers = self.subscribers.write();
match subscribers.get(&value) { match subscribers.get(&value) {
+22 -212
View File
@@ -2,15 +2,9 @@ use crate::{
default_impl, fmt_impls, read::*, write::*, write_impls, CopyValue, Global, GlobalMemo, default_impl, fmt_impls, read::*, write::*, write_impls, CopyValue, Global, GlobalMemo,
GlobalSignal, Memo, ReadableRef, WritableRef, GlobalSignal, Memo, ReadableRef, WritableRef,
}; };
use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId}; use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, Subscribers};
use generational_box::{AnyStorage, BorrowResult, Storage, SyncStorage, UnsyncStorage}; use generational_box::{BorrowResult, Storage, SyncStorage, UnsyncStorage};
use std::{ use std::{any::Any, collections::HashSet, ops::Deref, sync::Arc, sync::Mutex};
any::Any,
collections::HashSet,
ops::{Deref, DerefMut},
sync::Arc,
sync::Mutex,
};
#[doc = include_str!("../docs/signals.md")] #[doc = include_str!("../docs/signals.md")]
#[doc(alias = "State")] #[doc(alias = "State")]
@@ -261,7 +255,7 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
self.inner.id() 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. /// 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
/// *signal.write_silent() += 1; /// *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 /// ```rust, no_run
/// # use dioxus::prelude::*; /// # use dioxus::prelude::*;
/// let mut signal = use_signal(|| 0); /// let mut signal = use_signal(|| 0);
@@ -342,7 +336,7 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
/// } /// }
/// ``` /// ```
/// ///
/// 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: /// Here is the same example using peek:
/// main.rs: /// main.rs:
@@ -378,8 +372,10 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
/// ``` /// ```
#[track_caller] #[track_caller]
#[deprecated = "This pattern is no longer recommended. Prefer `peek` or creating new signals instead."] #[deprecated = "This pattern is no longer recommended. Prefer `peek` or creating new signals instead."]
pub fn write_silent(&self) -> S::Mut<'static, T> { pub fn write_silent(&self) -> WriteLock<'static, T, S> {
S::map_mut(self.inner.write_unchecked(), |inner| &mut inner.value) WriteLock::map(self.inner.write_unchecked(), |inner: &mut SignalData<T>| {
&mut inner.value
})
} }
} }
@@ -408,34 +404,14 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
.try_read_unchecked() .try_read_unchecked()
.map(|inner| S::map(inner, |v| &v.value)) .map(|inner| S::map(inner, |v| &v.value))
} }
fn subscribers(&self) -> Option<Subscribers> {
Some(self.inner.read().subscribers.clone().into())
}
} }
impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> { impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
type Mut<'a, R: ?Sized + 'static> = Write<'a, R, S>; type WriteMetadata = Box<dyn Any>;
fn map_mut<I: ?Sized, U: ?Sized + 'static, F: FnOnce(&mut I) -> &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<Self::Mut<'_, U>> {
Write::filter_map(ref_, f)
}
fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>(
mut_: Self::Mut<'a, R>,
) -> Self::Mut<'b, R> {
Write::downcast_lifetime(mut_)
}
#[track_caller] #[track_caller]
fn try_write_unchecked( fn try_write_unchecked(
@@ -444,15 +420,15 @@ impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let origin = std::panic::Location::caller(); let origin = std::panic::Location::caller();
self.inner.try_write_unchecked().map(|inner| { self.inner.try_write_unchecked().map(|inner| {
let borrow = S::map_mut(inner, |v| &mut v.value); let borrow = S::map_mut(inner.into_inner(), |v| &mut v.value);
Write { WriteLock::new_with_metadata(
write: borrow, borrow,
drop_signal: Box::new(SignalSubscriberDrop { Box::new(SignalSubscriberDrop {
signal: *self, signal: *self,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
origin, origin,
}), }) as Box<dyn Any>,
} )
}) })
} }
} }
@@ -490,7 +466,7 @@ impl<T: Clone, S: Storage<SignalData<T>> + 'static> Deref for Signal<T, S> {
type Target = dyn Fn() -> T; type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target { 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<SignalData<T>>>
} }
} }
/// 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<dyn Any>,
}
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<O: ?Sized>(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<O: ?Sized>(
myself: Self,
f: impl FnOnce(&mut T) -> Option<&mut O>,
) -> Option<Write<'a, O, S>> {
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<T: ?Sized + 'static, S: AnyStorage> Deref for Write<'_, T, S> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.write
}
}
impl<T: ?Sized, S: AnyStorage> DerefMut for Write<'_, T, S> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.write
}
}
struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> { struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
signal: Signal<T, S>, signal: Signal<T, S>,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
+285 -33
View File
@@ -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)] #[allow(type_alias_bounds)]
pub type WritableRef<'a, T: Writable, O = <T as Readable>::Target> = T::Mut<'a, O>; pub type WritableRef<'a, T: Writable, O = <T as Readable>::Target> =
WriteLock<'a, O, <T as Readable>::Storage, <T as Writable>::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. /// 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 = <T as Readable>::Target> = T::Mut<'a,
/// } /// }
/// ``` /// ```
pub trait Writable: Readable { pub trait Writable: Readable {
/// The type of the reference. /// Additional data associated with the write reference.
type Mut<'a, R: ?Sized + 'static>: DerefMut<Target = R>; type WriteMetadata: 'static;
/// Map the reference to a new type.
fn map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> &mut U>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Self::Mut<'_, U>;
/// Try to map the reference to a new type.
fn try_map_mut<I: ?Sized, U: ?Sized, F: FnOnce(&mut I) -> Option<&mut U>>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Option<Self::Mut<'_, U>>;
/// 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>;
/// Get a mutable reference to the value. If the value has been dropped, this will panic. /// Get a mutable reference to the value. If the value has been dropped, this will panic.
#[track_caller] #[track_caller]
@@ -63,7 +47,7 @@ pub trait Writable: Readable {
/// Try to get a mutable reference to the value. /// Try to get a mutable reference to the value.
#[track_caller] #[track_caller]
fn try_write(&mut self) -> Result<WritableRef<'_, Self>, generational_box::BorrowMutError> { fn try_write(&mut self) -> Result<WritableRef<'_, Self>, 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. /// 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> { fn write_unchecked(&self) -> WritableRef<'static, Self> {
self.try_write_unchecked().unwrap() 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<O>(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<O: ?Sized>(
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<O: ?Sized>(
myself: Self,
f: impl FnOnce(&mut T) -> Option<&mut O>,
) -> Option<WriteLock<'a, O, S, D>> {
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<T, S, D> Deref for WriteLock<'_, T, S, D>
where
S: AnyStorage,
T: ?Sized + 'static,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.write
}
}
impl<T, S, D> 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<Vec<i32>>) -> 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<i32>) -> Element {
/// rsx! {
/// button {
/// onclick: move |_| *item.write() += 1,
/// "{item}"
/// }
/// }
/// }
/// ```
fn map_mut<O, F, FMut>(self, f: F, f_mut: FMut) -> MappedMutSignal<O, Self, F, FMut>
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. /// Run a function with a mutable reference to the value. If the value has been dropped, this will panic.
#[track_caller] #[track_caller]
@@ -115,7 +348,7 @@ pub trait Writable: Readable {
where where
Self::Target: std::ops::IndexMut<I>, Self::Target: std::ops::IndexMut<I>,
{ {
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. /// Takes the value out of the Signal, leaving a Default in its place.
@@ -137,6 +370,25 @@ pub trait Writable: Readable {
} }
} }
impl<W: Writable + ?Sized> WritableExt for W {}
/// An extension trait for [`Writable`] values that can be boxed into a trait object.
pub trait WritableBoxedExt: Writable<Storage = UnsyncStorage> {
/// 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<Self::Target>
where
Self: Sized + 'static,
{
WriteSignal::new(self)
}
}
impl<T: Writable<Storage = UnsyncStorage> + 'static> WritableBoxedExt for T {
fn boxed_mut(self) -> WriteSignal<Self::Target> {
WriteSignal::new(self)
}
}
/// An extension trait for [`Writable<Option<T>>`]` that provides some convenience methods. /// An extension trait for [`Writable<Option<T>>`]` that provides some convenience methods.
pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> { pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> {
/// Gets the value out of the Option, or inserts the given value if the Option is empty. /// Gets the value out of the Option, or inserts the given value if the Option is empty.
@@ -151,16 +403,16 @@ pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> {
let is_none = self.read().is_none(); let is_none = self.read().is_none();
if is_none { if is_none {
self.with_mut(|v| *v = Some(default())); 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 { } 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. /// Attempts to write the inner value of the Option.
#[track_caller] #[track_caller]
fn as_mut(&mut self) -> Option<WritableRef<'_, Self, T>> { fn as_mut(&mut self) -> Option<WritableRef<'_, Self, T>> {
Self::try_map_mut(self.write(), |v: &mut Option<T>| v.as_mut()) WriteLock::filter_map(self.write(), |v: &mut Option<T>| v.as_mut())
} }
} }
@@ -236,7 +488,7 @@ pub trait WritableVecExt<T: 'static>: Writable<Target = Vec<T>> {
/// Try to mutably get an element from the vector. /// Try to mutably get an element from the vector.
#[track_caller] #[track_caller]
fn get_mut(&mut self, index: usize) -> Option<WritableRef<'_, Self, T>> { fn get_mut(&mut self, index: usize) -> Option<WritableRef<'_, Self, T>> {
Self::try_map_mut(self.write(), |v: &mut Vec<T>| v.get_mut(index)) WriteLock::filter_map(self.write(), |v: &mut Vec<T>| v.get_mut(index))
} }
/// Gets an iterator over the values of the vector. /// Gets an iterator over the values of the vector.
@@ -264,11 +516,11 @@ impl<'a, T: 'static, R: Writable<Target = Vec<T>>> Iterator for WritableValueIte
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let index = self.index; let index = self.index;
self.index += 1; self.index += 1;
R::try_map_mut( WriteLock::filter_map(
self.value.try_write_unchecked().unwrap(), self.value.try_write_unchecked().unwrap(),
|v: &mut Vec<T>| v.get_mut(index), |v: &mut Vec<T>| v.get_mut(index),
) )
.map(R::downcast_lifetime_mut) .map(WriteLock::downcast_lifetime)
} }
} }