diff --git a/Cargo.lock b/Cargo.lock index e9fdffa294..1806e8b672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4985,6 +4985,7 @@ dependencies = [ "dioxus-server", "dioxus-signals", "dioxus-ssr", + "dioxus-stores", "dioxus-web", "dioxus_server_macro", "env_logger 0.11.8", @@ -5405,6 +5406,7 @@ dependencies = [ "ciborium", "dioxus", "dioxus-ssr", + "dioxus-stores", "form_urlencoded", "futures-util", "getrandom 0.3.3", @@ -5935,6 +5937,28 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "dioxus-stores" +version = "0.7.0-alpha.3" +dependencies = [ + "dioxus", + "dioxus-core", + "dioxus-signals", + "dioxus-stores-macro", +] + +[[package]] +name = "dioxus-stores-macro" +version = "0.7.0-alpha.3" +dependencies = [ + "convert_case 0.8.0", + "dioxus", + "dioxus-stores", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "dioxus-tailwind" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 8a0746396c..88a124e705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ members = [ "packages/rsx", "packages/server-macro", "packages/signals", + "packages/stores", + "packages/stores-macro", "packages/ssr", "packages/lazy-js-bundle", "packages/cli-config", @@ -161,6 +163,8 @@ dioxus-rsx = { path = "packages/rsx", version = "0.7.0-alpha.3" } dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.0-alpha.3" } dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.0-alpha.3" } dioxus-signals = { path = "packages/signals", version = "0.7.0-alpha.3" } +dioxus-stores = { path = "packages/stores", version = "0.7.0-alpha.3" } +dioxus-stores-macro = { path = "packages/stores-macro", version = "0.7.0-alpha.3" } dioxus-cli-config = { path = "packages/cli-config", version = "0.7.0-alpha.3" } dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.0-alpha.3" } dioxus-devtools = { path = "packages/devtools", version = "0.7.0-alpha.3" } @@ -427,6 +431,7 @@ wasm-splitter = { workspace = true, package = "wasm-split" } [dev-dependencies] dioxus = { workspace = true, features = ["router"] } +dioxus-stores = { workspace = true } dioxus-ssr = { workspace = true } futures-util = { workspace = true } separator = { workspace = true } diff --git a/examples/todomvc_store.rs b/examples/todomvc_store.rs new file mode 100644 index 0000000000..a35e941505 --- /dev/null +++ b/examples/todomvc_store.rs @@ -0,0 +1,313 @@ +//! The typical TodoMVC app, implemented in Dioxus with stores. Stores let us +//! share nested reactive state between components. They let us keep our todomvc +//! state in a single struct without wrapping every type in a signal while still +//! maintaining fine grained reactivity. + +use dioxus::prelude::*; +use std::{collections::HashMap, vec}; + +const STYLE: Asset = asset!("/examples/assets/todomvc.css"); + +/// Deriving the store macro on a struct will automatically generate an extension trait +/// for Store with method to zoom into the fields of the struct. +/// +/// For this struct, the macro derives the following methods for Store: +/// - `todos(self) -> Store, _>` +/// - `filter(self) -> Store` +#[derive(Store, PartialEq, Clone, Debug)] +struct TodoState { + todos: HashMap, + filter: FilterState, +} + +// We can also add custom methods to the store by using the `store` attribute on an impl block. +// The store attribute turns the impl block into an extension trait for Store. +// Methods that take &self will automatically get a bound that Lens: Readable +// Methods that take &mut self will automatically get a bound that Lens: Writable +#[store] +impl Store { + fn active_items(&self) -> Vec { + let filter = self.filter().cloned(); + let mut active_ids: Vec = self + .todos() + .iter() + .filter_map(|(id, item)| item.active(filter).then_some(id)) + .collect(); + active_ids.sort_unstable(); + active_ids + } + + fn incomplete_count(&self) -> usize { + self.todos() + .values() + .filter(|item| item.incomplete()) + .count() + } + + fn toggle_all(&mut self) { + let check = self.incomplete_count() != 0; + for item in self.todos().values() { + item.checked().set(check); + } + } + + fn has_todos(&self) -> bool { + !self.todos().is_empty() + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum FilterState { + All, + Active, + Completed, +} + +#[derive(Store, PartialEq, Clone, Debug)] +struct TodoItem { + checked: bool, + contents: String, +} + +impl TodoItem { + fn new(contents: impl ToString) -> Self { + Self { + checked: false, + contents: contents.to_string(), + } + } +} + +#[store] +impl Store { + fn complete(&self) -> bool { + self.checked().cloned() + } + + fn incomplete(&self) -> bool { + !self.complete() + } + + fn active(&self, filter: FilterState) -> bool { + match filter { + FilterState::All => true, + FilterState::Active => self.incomplete(), + FilterState::Completed => self.complete(), + } + } +} + +fn main() { + dioxus::launch(app); +} + +fn app() -> Element { + // We store the state of our todo list in a store to use throughout the app. + let mut todos = use_store(|| TodoState { + todos: HashMap::new(), + filter: FilterState::All, + }); + + // We use a simple memo to calculate the number of active todos. + // Whenever the todos change, the active_todo_count will be recalculated. + let active_todo_count = use_memo(move || todos.incomplete_count()); + + // We use a memo to filter the todos based on the current filter state. + // Whenever the todos or filter change, the filtered_todos will be recalculated. + // Note that we're only storing the IDs of the todos, not the todos themselves. + let filtered_todos = use_memo(move || todos.active_items()); + + // Toggle all the todos to the opposite of the current state. + // If all todos are checked, uncheck them all. If any are unchecked, check them all. + let toggle_all = move |_| { + todos.toggle_all(); + }; + + rsx! { + document::Link { rel: "stylesheet", href: STYLE } + section { class: "todoapp", + TodoHeader { todos } + section { class: "main", + if todos.has_todos() { + input { + id: "toggle-all", + class: "toggle-all", + r#type: "checkbox", + onchange: toggle_all, + checked: active_todo_count() == 0 + } + label { r#for: "toggle-all" } + } + + // Render the todos using the filtered_todos memo + // We pass the ID along with the hashmap into the TodoEntry component so it can access the todo from the todos store. + ul { class: "todo-list", + for id in filtered_todos() { + TodoEntry { key: "{id}", id, todos } + } + } + + // We only show the footer if there are todos. + if todos.has_todos() { + ListFooter { active_todo_count, todos } + } + } + } + + // A simple info footer + footer { class: "info", + p { "Double-click to edit a todo" } + p { + "Created by " + a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } + } + p { + "Part of " + a { href: "http://todomvc.com", "TodoMVC" } + } + } + } +} + +#[component] +fn TodoHeader(mut todos: Store) -> Element { + let mut draft = use_signal(|| "".to_string()); + let mut todo_id = use_signal(|| 0); + + let onkeydown = move |evt: KeyboardEvent| { + if evt.key() == Key::Enter && !draft.read().is_empty() { + let id = todo_id(); + let todo = TodoItem::new(draft.take()); + todos.todos().insert(id, todo); + todo_id += 1; + } + }; + + rsx! { + header { class: "header", + h1 { "todos" } + input { + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + autofocus: "true", + oninput: move |evt| draft.set(evt.value()), + onkeydown + } + } + } +} + +/// A single todo entry +/// This takes the ID of the todo and the todos store as props +/// We can use these together to memoize the todo contents and checked state +#[component] +fn TodoEntry(mut todos: Store, id: u32) -> Element { + let mut is_editing = use_signal(|| false); + + // When we get an item out of the store, it will only subscribe to that specific item. + // Since we only get the single todo item, the component will only rerender when that item changes. + let entry = todos.todos().get(id).unwrap(); + let checked = entry.checked(); + let contents = entry.contents(); + + rsx! { + li { + // Dioxus lets you use if statements in rsx to conditionally render attributes + // These will get merged into a single class attribute + class: if checked() { "completed" }, + class: if is_editing() { "editing" }, + + // Some basic controls for the todo + div { class: "view", + input { + class: "toggle", + r#type: "checkbox", + id: "cbg-{id}", + checked: "{checked}", + oninput: move |evt| entry.checked().set(evt.checked()) + } + label { + r#for: "cbg-{id}", + ondoubleclick: move |_| is_editing.set(true), + onclick: |evt| evt.prevent_default(), + "{contents}" + } + button { + class: "destroy", + onclick: move |evt| { + evt.prevent_default(); + todos.todos().remove(&id); + }, + } + } + + // Only render the actual input if we're editing + if is_editing() { + input { + class: "edit", + value: "{contents}", + oninput: move |evt| entry.contents().set(evt.value()), + autofocus: "true", + onfocusout: move |_| is_editing.set(false), + onkeydown: move |evt| { + match evt.key() { + Key::Enter | Key::Escape | Key::Tab => is_editing.set(false), + _ => {} + } + } + } + } + } + } +} + +#[component] +fn ListFooter(mut todos: Store, active_todo_count: ReadSignal) -> Element { + // We use a memo to calculate whether we should show the "Clear completed" button. + // This will recompute whenever the number of todos change or the checked state of an existing + // todo changes + let show_clear_completed = use_memo(move || todos.todos().values().any(|todo| todo.complete())); + let mut filter = todos.filter(); + + rsx! { + footer { class: "footer", + span { class: "todo-count", + strong { "{active_todo_count} " } + span { + match active_todo_count() { + 1 => "item", + _ => "items", + } + " left" + } + } + ul { class: "filters", + for (state , state_text , url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), + ] { + li { + a { + href: url, + class: if filter() == state { "selected" }, + onclick: move |evt| { + evt.prevent_default(); + filter.set(state) + }, + {state_text} + } + } + } + } + if show_clear_completed() { + button { + class: "clear-completed", + onclick: move |_| todos.todos().retain(|_, todo| !todo.checked), + "Clear completed" + } + } + } + } +} diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 400705797a..eb31ed2962 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -13,7 +13,7 @@ use syn::spanned::Spanned; use syn::{parse::Error, PathArguments}; use quote::quote; -use syn::{parse_quote, GenericArgument, PathSegment, Type}; +use syn::{parse_quote, GenericArgument, Ident, PathSegment, Type}; pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { let data = match &ast.data { @@ -172,7 +172,7 @@ mod util { } mod field_info { - use crate::props::{looks_like_write_type, type_from_inside_option}; + use crate::props::{looks_like_store_type, looks_like_write_type, type_from_inside_option}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::spanned::Spanned; @@ -221,8 +221,8 @@ mod field_info { builder_attr.auto_into = false; } - // Write fields automatically use impl Into - if looks_like_write_type(&field.ty) { + // Write and Store fields automatically use impl Into + if looks_like_write_type(&field.ty) || looks_like_store_type(&field.ty) { builder_attr.auto_into = true; } @@ -1744,44 +1744,34 @@ fn child_owned_type(ty: &Type) -> bool { looks_like_signal_type(ty) || looks_like_write_type(ty) || looks_like_callback_type(ty) } +/// Check if the path without generics matches the type we are looking for +fn last_segment_matches(ty: &Type, expected: &Ident) -> bool { + extract_base_type_without_generics(ty).is_some_and(|path_without_generics| { + path_without_generics + .segments + .last() + .is_some_and(|seg| seg.ident == *expected) + }) +} + fn looks_like_signal_type(ty: &Type) -> bool { - match extract_base_type_without_generics(ty) { - Some(path_without_generics) => { - path_without_generics == parse_quote!(dioxus_core::ReadOnlySignal) - || path_without_generics == parse_quote!(prelude::ReadOnlySignal) - || path_without_generics == parse_quote!(ReadOnlySignal) - || path_without_generics == parse_quote!(dioxus_core::prelude::ReadSignal) - || path_without_generics == parse_quote!(prelude::ReadSignal) - || path_without_generics == parse_quote!(ReadSignal) - } - None => false, - } + last_segment_matches(ty, &parse_quote!(ReadOnlySignal)) + || last_segment_matches(ty, &parse_quote!(ReadSignal)) } 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, - } + last_segment_matches(ty, &parse_quote!(WriteSignal)) +} + +fn looks_like_store_type(ty: &Type) -> bool { + last_segment_matches(ty, &parse_quote!(Store)) + || last_segment_matches(ty, &parse_quote!(ReadStore)) } fn looks_like_callback_type(ty: &Type) -> bool { let type_without_option = remove_option_wrapper(ty.clone()); - match extract_base_type_without_generics(&type_without_option) { - Some(path_without_generics) => { - path_without_generics == parse_quote!(dioxus_core::EventHandler) - || path_without_generics == parse_quote!(prelude::EventHandler) - || path_without_generics == parse_quote!(EventHandler) - || path_without_generics == parse_quote!(dioxus_core::Callback) - || path_without_generics == parse_quote!(prelude::Callback) - || path_without_generics == parse_quote!(Callback) - } - None => false, - } + last_segment_matches(&type_without_option, &parse_quote!(EventHandler)) + || last_segment_matches(&type_without_option, &parse_quote!(Callback)) } #[test] diff --git a/packages/core/src/reactive_context.rs b/packages/core/src/reactive_context.rs index b54d475712..7006bdae5a 100644 --- a/packages/core/src/reactive_context.rs +++ b/packages/core/src/reactive_context.rs @@ -335,6 +335,21 @@ impl Default for Subscribers { } impl Subscribers { + /// Create a new no-op list of subscribers. + pub fn new_noop() -> Self { + struct NoopSubscribers; + impl SubscriberList for NoopSubscribers { + fn add(&self, _subscriber: ReactiveContext) {} + + fn remove(&self, _subscriber: &ReactiveContext) {} + + fn visit(&self, _f: &mut dyn FnMut(&ReactiveContext)) {} + } + Subscribers { + inner: Arc::new(NoopSubscribers), + } + } + /// Create a new list of subscribers. pub fn new() -> Self { Subscribers { diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index bbe8c71625..7c62efe342 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -18,6 +18,7 @@ dioxus-document = { workspace = true, optional = true } dioxus-history = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-config-macro = { workspace = true, optional = true } +dioxus-stores = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } dioxus-signals = { workspace = true, optional = true } dioxus-router = { workspace = true, optional = true } @@ -61,7 +62,7 @@ lib = [ ] # The minimal set of features required to use dioxus renderers for minimal binary size minimal = ["macro", "html", "signals", "hooks", "launch"] -signals = ["dep:dioxus-signals"] +signals = ["dep:dioxus-signals", "dep:dioxus-stores"] macro = ["dep:dioxus-core-macro"] html = ["dep:dioxus-html"] hooks = ["dep:dioxus-hooks"] diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 1324844c40..9a200476f6 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -47,6 +47,10 @@ pub use dioxus_hooks as hooks; #[cfg_attr(docsrs, doc(cfg(feature = "signals")))] pub use dioxus_signals as signals; +#[cfg(feature = "signals")] +#[cfg_attr(docsrs, doc(cfg(feature = "signals")))] +pub use dioxus_stores as stores; + pub mod events { #[cfg(feature = "html")] #[cfg_attr(docsrs, doc(cfg(feature = "html")))] @@ -151,6 +155,9 @@ pub mod prelude { #[doc(inline)] pub use dioxus_signals::*; + #[cfg(feature = "signals")] + pub use dioxus_stores::{self, store, use_store, ReadStore, Store, WriteStore}; + #[cfg(feature = "macro")] #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] #[allow(deprecated)] diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 38bc539de7..6c1592679e 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -117,11 +117,6 @@ impl> GenerationalBox { *self.write() = value; } - /// Returns true if the pointer is equal to the other pointer. - pub fn ptr_eq(&self, other: &Self) -> bool { - self.raw == other.raw - } - /// Drop the value out of the generational box and invalidate the generational box. pub fn manually_drop(&self) where @@ -130,11 +125,6 @@ impl> GenerationalBox { self.raw.recycle(); } - /// Try to get the location the generational box was created at. In release mode this will always return None. - pub fn created_at(&self) -> Option<&'static std::panic::Location<'static>> { - self.raw.location.created_at() - } - /// Get a reference to the value #[track_caller] pub fn leak_reference(&self) -> BorrowResult> { @@ -150,6 +140,21 @@ impl> GenerationalBox { } } +impl GenerationalBox { + /// Returns true if the pointer is equal to the other pointer. + pub fn ptr_eq(&self, other: &Self) -> bool + where + S: AnyStorage, + { + self.raw == other.raw + } + + /// Try to get the location the generational box was created at. In release mode this will always return None. + pub fn created_at(&self) -> Option<&'static std::panic::Location<'static>> { + self.raw.location.created_at() + } +} + impl Copy for GenerationalBox {} impl Clone for GenerationalBox { @@ -159,7 +164,7 @@ impl Clone for GenerationalBox { } /// A trait for a storage backing type. (RefCell, RwLock, etc.) -pub trait Storage: AnyStorage + 'static { +pub trait Storage: AnyStorage { /// Try to read the value. Returns None if the value is no longer valid. fn try_read(pointer: GenerationalPointer) -> BorrowResult>; @@ -193,34 +198,34 @@ pub trait Storage: AnyStorage + 'static { } /// A trait for any storage backing type. -pub trait AnyStorage: Default + 'static { +pub trait AnyStorage: Default { /// The reference this storage type returns. - type Ref<'a, T: ?Sized + 'static>: Deref; + type Ref<'a, T: ?Sized + 'a>: Deref; /// The mutable reference this storage type returns. - type Mut<'a, T: ?Sized + 'static>: DerefMut; + type Mut<'a, T: ?Sized + 'a>: DerefMut; /// Downcast a reference in a Ref to a more specific lifetime /// /// This function enforces the variance of the lifetime parameter `'a` in Ref. Rust will typically infer this cast with a concrete type, but it cannot with a generic type. - fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'a>( ref_: Self::Ref<'a, T>, ) -> Self::Ref<'b, T>; /// Downcast a mutable reference in a RefMut to a more specific lifetime /// /// 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. - fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>( mut_: Self::Mut<'a, T>, ) -> Self::Mut<'b, T>; /// Try to map the mutable ref. - fn try_map_mut( + fn try_map_mut( mut_ref: Self::Mut<'_, T>, f: impl FnOnce(&mut T) -> Option<&mut U>, ) -> Option>; /// Map the mutable ref. - fn map_mut( + fn map_mut( mut_ref: Self::Mut<'_, T>, f: impl FnOnce(&mut T) -> &mut U, ) -> Self::Mut<'_, U> { @@ -228,13 +233,13 @@ pub trait AnyStorage: Default + 'static { } /// Try to map the ref. - fn try_map( + fn try_map( ref_: Self::Ref<'_, T>, f: impl FnOnce(&T) -> Option<&U>, ) -> Option>; /// Map the ref. - fn map( + fn map( ref_: Self::Ref<'_, T>, f: impl FnOnce(&T) -> &U, ) -> Self::Ref<'_, U> { @@ -248,7 +253,10 @@ pub trait AnyStorage: Default + 'static { fn recycle(location: GenerationalPointer); /// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates. - fn owner() -> Owner { + fn owner() -> Owner + where + Self: 'static, + { Owner(Arc::new(Mutex::new(OwnerInner { owned: Default::default(), }))) @@ -374,7 +382,7 @@ impl Clone for Owner { impl Owner { /// Insert a value into the store. The value will be dropped when the owner is dropped. #[track_caller] - pub fn insert(&self, value: T) -> GenerationalBox + pub fn insert(&self, value: T) -> GenerationalBox where S: Storage, { @@ -383,7 +391,7 @@ impl Owner { /// Create a new reference counted box. The box will be dropped when all references are dropped. #[track_caller] - pub fn insert_rc(&self, value: T) -> GenerationalBox + pub fn insert_rc(&self, value: T) -> GenerationalBox where S: Storage, { @@ -391,7 +399,7 @@ impl Owner { } /// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped. - pub fn insert_rc_with_caller( + pub fn insert_rc_with_caller( &self, value: T, caller: &'static std::panic::Location<'static>, @@ -408,7 +416,7 @@ impl Owner { } /// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped. - pub fn insert_with_caller( + pub fn insert_with_caller( &self, value: T, caller: &'static std::panic::Location<'static>, @@ -428,7 +436,7 @@ impl Owner { /// /// This method may return an error if the other box is no longer valid or it is already borrowed mutably. #[track_caller] - pub fn insert_reference( + pub fn insert_reference( &self, other: GenerationalBox, ) -> BorrowResult> diff --git a/packages/generational-box/src/references.rs b/packages/generational-box/src/references.rs index c6a5a49af7..25c3b81fbb 100644 --- a/packages/generational-box/src/references.rs +++ b/packages/generational-box/src/references.rs @@ -84,7 +84,7 @@ pub struct GenerationalRef { guard: GenerationalRefBorrowGuard, } -impl> GenerationalRef { +impl> GenerationalRef { pub(crate) fn new(inner: R, guard: GenerationalRefBorrowGuard) -> Self { Self { inner, guard } } @@ -118,7 +118,7 @@ impl> Display for GenerationalRef { } } -impl> Deref for GenerationalRef { +impl> Deref for GenerationalRef { type Target = T; fn deref(&self) -> &Self::Target { @@ -218,7 +218,7 @@ pub struct GenerationalRefMut { pub(crate) borrow: GenerationalRefBorrowMutGuard, } -impl> GenerationalRefMut { +impl> GenerationalRefMut { pub(crate) fn new(inner: R, borrow: GenerationalRefBorrowMutGuard) -> Self { Self { inner, borrow } } diff --git a/packages/generational-box/src/sync.rs b/packages/generational-box/src/sync.rs index fab0373a75..9e816dc1b8 100644 --- a/packages/generational-box/src/sync.rs +++ b/packages/generational-box/src/sync.rs @@ -178,43 +178,43 @@ fn sync_runtime() -> &'static Arc>> { } impl AnyStorage for SyncStorage { - type Ref<'a, R: ?Sized + 'static> = GenerationalRef>; - type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut>; + type Ref<'a, R: ?Sized + 'a> = GenerationalRef>; + type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut>; - fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'b>( ref_: Self::Ref<'a, T>, ) -> Self::Ref<'b, T> { ref_ } - fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>( mut_: Self::Mut<'a, T>, ) -> Self::Mut<'b, T> { mut_ } - fn map( + fn map( ref_: Self::Ref<'_, T>, f: impl FnOnce(&T) -> &U, ) -> Self::Ref<'_, U> { ref_.map(|inner| MappedRwLockReadGuard::map(inner, f)) } - fn map_mut( + fn map_mut( mut_ref: Self::Mut<'_, T>, f: impl FnOnce(&mut T) -> &mut U, ) -> Self::Mut<'_, U> { mut_ref.map(|inner| MappedRwLockWriteGuard::map(inner, f)) } - fn try_map( + fn try_map( ref_: Self::Ref<'_, I>, f: impl FnOnce(&I) -> Option<&U>, ) -> Option> { ref_.try_map(|inner| MappedRwLockReadGuard::try_map(inner, f).ok()) } - fn try_map_mut( + fn try_map_mut( mut_ref: Self::Mut<'_, I>, f: impl FnOnce(&mut I) -> Option<&mut U>, ) -> Option> { diff --git a/packages/generational-box/src/unsync.rs b/packages/generational-box/src/unsync.rs index 553264857c..7709407634 100644 --- a/packages/generational-box/src/unsync.rs +++ b/packages/generational-box/src/unsync.rs @@ -174,43 +174,43 @@ impl UnsyncStorage { } impl AnyStorage for UnsyncStorage { - type Ref<'a, R: ?Sized + 'static> = GenerationalRef>; - type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut>; + type Ref<'a, R: ?Sized + 'a> = GenerationalRef>; + type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut>; - fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'a>( ref_: Self::Ref<'a, T>, ) -> Self::Ref<'b, T> { ref_ } - fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>( + fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>( mut_: Self::Mut<'a, T>, ) -> Self::Mut<'b, T> { mut_ } - fn map( + fn map( ref_: Self::Ref<'_, T>, f: impl FnOnce(&T) -> &U, ) -> Self::Ref<'_, U> { ref_.map(|inner| Ref::map(inner, f)) } - fn map_mut( + fn map_mut( mut_ref: Self::Mut<'_, T>, f: impl FnOnce(&mut T) -> &mut U, ) -> Self::Mut<'_, U> { mut_ref.map(|inner| RefMut::map(inner, f)) } - fn try_map( + fn try_map( _self: Self::Ref<'_, I>, f: impl FnOnce(&I) -> Option<&U>, ) -> Option> { _self.try_map(|inner| Ref::filter_map(inner, f).ok()) } - fn try_map_mut( + fn try_map_mut( mut_ref: Self::Mut<'_, I>, f: impl FnOnce(&mut I) -> Option<&mut U>, ) -> Option> { diff --git a/packages/generational-box/tests/basic.rs b/packages/generational-box/tests/basic.rs index 30b72cfc12..ef38445564 100644 --- a/packages/generational-box/tests/basic.rs +++ b/packages/generational-box/tests/basic.rs @@ -14,7 +14,7 @@ fn compile_fail() {} #[test] fn leaking_is_ok() { - fn leaking_is_ok_test>() { + fn leaking_is_ok_test + 'static>() { let data = String::from("hello world"); let key; { @@ -37,7 +37,7 @@ fn leaking_is_ok() { #[test] fn drops() { - fn drops_test>() { + fn drops_test + 'static>() { let data = String::from("hello world"); let key; { @@ -56,7 +56,7 @@ fn drops() { #[test] fn works() { - fn works_test>() { + fn works_test + 'static>() { let owner = S::owner(); let key = owner.insert(1); @@ -69,7 +69,7 @@ fn works() { #[test] fn insert_while_reading() { - fn insert_while_reading_test + Storage<&'static i32>>() { + fn insert_while_reading_test + Storage<&'static i32> + 'static>() { let owner = S::owner(); let key; { @@ -88,7 +88,7 @@ fn insert_while_reading() { #[test] #[should_panic] fn panics() { - fn panics_test>() { + fn panics_test + 'static>() { let owner = S::owner(); let key = owner.insert(1); @@ -103,7 +103,7 @@ fn panics() { #[test] fn fuzz() { - fn maybe_owner_scope>( + fn maybe_owner_scope + 'static>( valid_keys: &mut Vec>, invalid_keys: &mut Vec>, path: &mut Vec, diff --git a/packages/generational-box/tests/errors.rs b/packages/generational-box/tests/errors.rs index 5aed15ef9c..c748a6100c 100644 --- a/packages/generational-box/tests/errors.rs +++ b/packages/generational-box/tests/errors.rs @@ -35,7 +35,7 @@ fn create_at_location>( #[test] fn read_while_writing_error() { - fn read_while_writing_error_test>() { + fn read_while_writing_error_test + 'static>() { let owner = S::owner(); let value = owner.insert(1); @@ -56,7 +56,7 @@ fn read_while_writing_error() { #[test] fn read_after_dropped_error() { - fn read_after_dropped_error_test>() { + fn read_after_dropped_error_test + 'static>() { let owner = S::owner(); let (value, location) = create_at_location(&owner); drop(owner); @@ -72,7 +72,7 @@ fn read_after_dropped_error() { #[test] fn write_while_writing_error() { - fn write_while_writing_error_test>() { + fn write_while_writing_error_test + 'static>() { let owner = S::owner(); let value = owner.insert(1); @@ -98,7 +98,7 @@ fn write_while_writing_error() { #[test] fn write_while_reading_error() { - fn write_while_reading_error_test>() { + fn write_while_reading_error_test + 'static>() { let owner = S::owner(); let value = owner.insert(1); diff --git a/packages/generational-box/tests/reference_counting.rs b/packages/generational-box/tests/reference_counting.rs index e5651ddf51..24db8fdefd 100644 --- a/packages/generational-box/tests/reference_counting.rs +++ b/packages/generational-box/tests/reference_counting.rs @@ -2,7 +2,7 @@ use generational_box::{Storage, SyncStorage, UnsyncStorage}; #[test] fn reference_counting() { - fn reference_counting>() { + fn reference_counting + 'static>() { let data = String::from("hello world"); let reference; { @@ -30,7 +30,7 @@ fn reference_counting() { #[test] fn move_reference_in_place() { - fn move_reference_in_place>() { + fn move_reference_in_place + 'static>() { let data1 = String::from("hello world"); let data2 = String::from("hello world 2"); diff --git a/packages/generational-box/tests/reused.rs b/packages/generational-box/tests/reused.rs index 58106b0298..cee30fe6ee 100644 --- a/packages/generational-box/tests/reused.rs +++ b/packages/generational-box/tests/reused.rs @@ -7,7 +7,7 @@ use generational_box::{Storage, SyncStorage, UnsyncStorage}; #[test] fn reused() { - fn reused_test>() { + fn reused_test + 'static>() { let first_ptr; { let owner = S::owner(); diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 388cd53514..74431a2bb7 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -181,7 +181,7 @@ impl Readable for UseFuture { self.state.try_peek_unchecked() } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers { self.state.subscribers() } } diff --git a/packages/hooks/src/use_memo.rs b/packages/hooks/src/use_memo.rs index 6c31504789..c439ffd9e8 100644 --- a/packages/hooks/src/use_memo.rs +++ b/packages/hooks/src/use_memo.rs @@ -6,7 +6,7 @@ use dioxus_signals::Memo; #[doc = include_str!("../docs/rules_of_hooks.md")] #[doc = include_str!("../docs/moving_state_around.md")] #[track_caller] -pub fn use_memo(mut f: impl FnMut() -> R + 'static) -> Memo { +pub fn use_memo(mut f: impl FnMut() -> R + 'static) -> Memo { let callback = use_callback(move |_| f()); let caller = std::panic::Location::caller(); #[allow(clippy::redundant_closure)] diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index 5b63e3ba6f..ad8fbe2473 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -454,7 +454,7 @@ impl Readable for Resource { self.value.try_peek_unchecked() } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers { self.value.subscribers() } } diff --git a/packages/hooks/src/use_set_compare.rs b/packages/hooks/src/use_set_compare.rs index faa3727df8..57428c3bea 100644 --- a/packages/hooks/src/use_set_compare.rs +++ b/packages/hooks/src/use_set_compare.rs @@ -40,7 +40,7 @@ use std::hash::Hash; #[doc = include_str!("../docs/rules_of_hooks.md")] #[doc = include_str!("../docs/moving_state_around.md")] #[must_use] -pub fn use_set_compare(f: impl FnMut() -> R + 'static) -> SetCompare { +pub fn use_set_compare(f: impl FnMut() -> R + 'static) -> SetCompare { use_hook(move || SetCompare::new(f)) } @@ -48,7 +48,7 @@ pub fn use_set_compare(f: impl FnMut() -> R + 'static) -> SetCompa #[doc = include_str!("../docs/rules_of_hooks.md")] #[doc = include_str!("../docs/moving_state_around.md")] #[must_use] -pub fn use_set_compare_equal( +pub fn use_set_compare_equal( value: R, mut compare: SetCompare, ) -> ReadSignal { diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index 54db4998cf..87451041f6 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -7,7 +7,7 @@ use std::{ use dioxus_core::{provide_context, Element, ReactiveContext, ScopeId}; use dioxus_history::history; -use dioxus_signals::{CopyValue, ReadableExt, Signal, Writable}; +use dioxus_signals::{CopyValue, ReadableExt, Signal, WritableExt}; use crate::{ components::child_router::consume_child_route_mapping, navigation::NavigationTarget, diff --git a/packages/signals/docs/memo.md b/packages/signals/docs/memo.md index ee45037fba..8af2d4a867 100644 --- a/packages/signals/docs/memo.md +++ b/packages/signals/docs/memo.md @@ -1,10 +1,10 @@ Memos are the result of computing a value from `use_memo`. -You may have noticed that this struct doesn't have many methods. Most methods for `Memo` are defined on the [`Readable`] and [`Writable`] traits. +You may have noticed that this struct doesn't have many methods. Most methods for `Memo` are defined on the [`ReadableExt`] trait. # Reading a Memo -You can use the methods on the `Readable` trait to read a memo: +You can use the methods on the `ReadableExt` trait to read a memo: ```rust, no_run # use dioxus::prelude::*; diff --git a/packages/signals/src/boxed.rs b/packages/signals/src/boxed.rs index d257941019..8618282921 100644 --- a/packages/signals/src/boxed.rs +++ b/packages/signals/src/boxed.rs @@ -12,7 +12,7 @@ use crate::{ pub type ReadOnlySignal = ReadSignal; /// A boxed version of [Readable] that can be used to store any readable type. -pub struct ReadSignal { +pub struct ReadSignal { value: CopyValue>>, } @@ -25,45 +25,42 @@ impl ReadSignal { } /// 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)] + pub fn point_to(&self, other: Self) -> BorrowResult { let this_subscribers = self.subscribers(); + let mut this_subscribers_vec = Vec::new(); + // Note we don't subscribe directly in the visit closure to avoid a deadlock when pointing to self + this_subscribers.visit(|subscriber| this_subscribers_vec.push(*subscriber)); 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()); - }); + for subscriber in this_subscribers_vec { + subscriber.subscribe(other_subscribers.clone()); } - self.value.point_to(other.value) + self.value.point_to(other.value)?; + Ok(()) } #[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(); - }); + let subscribers = self.subscribers(); + let mut this_subscribers_vec = Vec::new(); + subscribers.visit(|subscriber| this_subscribers_vec.push(*subscriber)); + for subscriber in this_subscribers_vec { + subscribers.remove(&subscriber); + subscriber.mark_dirty(); } } } -impl Clone for ReadSignal { +impl Clone for ReadSignal { fn clone(&self) -> Self { *self } } -impl Copy for ReadSignal {} +impl Copy for ReadSignal {} -impl PartialEq for ReadSignal { +impl PartialEq for ReadSignal { fn eq(&self, other: &Self) -> bool { self.value == other.value } @@ -103,14 +100,17 @@ impl Deref for ReadSignal { } } -impl Readable for ReadSignal { +impl Readable for ReadSignal { type Target = T; type Storage = UnsyncStorage; #[track_caller] fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + T: 'static, + { self.value .try_peek_unchecked() .unwrap() @@ -118,39 +118,46 @@ impl Readable for ReadSignal { } #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + T: 'static, + { self.value .try_peek_unchecked() .unwrap() .try_peek_unchecked() } - fn subscribers(&self) -> Option { - self.value.subscribers() + fn subscribers(&self) -> Subscribers + where + T: 'static, + { + self.value.try_peek_unchecked().unwrap().subscribers() } } -// We can't implement From + 'static> for ReadSignal +// We can't implement From > for ReadSignal // because it would conflict with the From for T implementation, but we can implement it for // all specific readable types -impl From> for ReadSignal { +impl From> for ReadSignal { fn from(value: Signal) -> Self { Self::new(value) } } -impl From> for ReadSignal { +impl From> for ReadSignal { fn from(value: Memo) -> Self { Self::new(value) } } -impl From> for ReadSignal { +impl From> for ReadSignal { fn from(value: CopyValue) -> Self { Self::new(value) } } -impl From> for ReadSignal +impl From> for ReadSignal where - T: Readable + InitializeFromFunction, + T: Readable + InitializeFromFunction + Clone + 'static, + R: 'static, { fn from(value: Global) -> Self { Self::new(value) @@ -158,7 +165,7 @@ where } impl From> for ReadSignal where - O: ?Sized, + O: ?Sized + 'static, V: Readable + 'static, F: Fn(&V::Target) -> &O + 'static, { @@ -168,7 +175,7 @@ where } impl From> for ReadSignal where - O: ?Sized, + O: ?Sized + 'static, V: Readable + 'static, F: Fn(&V::Target) -> &O + 'static, FMut: 'static, @@ -177,18 +184,23 @@ where Self::new(value) } } +impl From> for ReadSignal { + fn from(value: WriteSignal) -> Self { + Self::new(value) + } +} /// A boxed version of [Writable] that can be used to store any writable type. -pub struct WriteSignal { +pub struct WriteSignal { value: CopyValue< Box>>, >, } -impl WriteSignal { +impl WriteSignal { /// Create a new boxed writable value. - pub fn new( - value: impl Writable + 'static, + pub fn new( + value: impl Writable + 'static, ) -> Self { Self { value: CopyValue::new(Box::new(BoxWriteMetadata::new(value))), @@ -213,51 +225,47 @@ impl Readable for BoxWriteMetadata { fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + W::Target: 'static, + { self.value.try_read_unchecked() } fn try_peek_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + W::Target: 'static, + { self.value.try_peek_unchecked() } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers + where + W::Target: 'static, + { self.value.subscribers() } } -impl Writable for BoxWriteMetadata { +impl Writable for BoxWriteMetadata +where + W: Writable, + W::WriteMetadata: 'static, +{ type WriteMetadata = Box; fn try_write_unchecked( &self, - ) -> Result, generational_box::BorrowMutError> { + ) -> Result, generational_box::BorrowMutError> + where + W::Target: 'static, + { self.value .try_write_unchecked() .map(|w| w.map_metadata(|data| Box::new(data) as Box)) } - - fn write(&mut self) -> crate::WritableRef<'_, Self> { - self.value - .write() - .map_metadata(|data| Box::new(data) as Box) - } - - fn try_write( - &mut self, - ) -> Result, generational_box::BorrowMutError> { - self.value - .try_write() - .map(|w| w.map_metadata(|data| Box::new(data) as Box)) - } - - fn write_unchecked(&self) -> crate::WritableRef<'static, Self> { - self.value - .write_unchecked() - .map_metadata(|data| Box::new(data) as Box) - } } impl Clone for WriteSignal { @@ -303,14 +311,17 @@ impl Deref for WriteSignal { } } -impl Readable for WriteSignal { +impl Readable for WriteSignal { type Target = T; type Storage = UnsyncStorage; #[track_caller] fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + T: 'static, + { self.value .try_peek_unchecked() .unwrap() @@ -318,28 +329,33 @@ impl Readable for WriteSignal { } #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + T: 'static, + { self.value .try_peek_unchecked() .unwrap() .try_peek_unchecked() } - fn subscribers(&self) -> Option { - self.value.subscribers() + fn subscribers(&self) -> Subscribers + where + T: 'static, + { + self.value.try_peek_unchecked().unwrap().subscribers() } } -impl Writable for WriteSignal { +impl Writable for WriteSignal { type WriteMetadata = Box; - fn write_unchecked(&self) -> crate::WritableRef<'static, Self> { - self.value.try_peek_unchecked().unwrap().write_unchecked() - } - fn try_write_unchecked( &self, - ) -> Result, generational_box::BorrowMutError> { + ) -> Result, generational_box::BorrowMutError> + where + T: 'static, + { self.value .try_peek_unchecked() .unwrap() @@ -347,22 +363,23 @@ impl Writable for WriteSignal { } } -// We can't implement From + 'static> for Write +// We can't implement From> for Write // because it would conflict with the From for T implementation, but we can implement it for // all specific readable types -impl From> for WriteSignal { +impl From> for WriteSignal { fn from(value: Signal) -> Self { Self::new(value) } } -impl From> for WriteSignal { +impl From> for WriteSignal { fn from(value: CopyValue) -> Self { Self::new(value) } } -impl From> for WriteSignal +impl From> for WriteSignal where - T: Writable + InitializeFromFunction, + T: Writable + InitializeFromFunction + Clone + 'static, + R: 'static, { fn from(value: Global) -> Self { Self::new(value) @@ -370,7 +387,7 @@ where } impl From> for WriteSignal where - O: ?Sized, + O: ?Sized + 'static, V: Writable + 'static, F: Fn(&V::Target) -> &O + 'static, FMut: Fn(&mut V::Target) -> &mut O + 'static, diff --git a/packages/signals/src/copy_value.rs b/packages/signals/src/copy_value.rs index b53543d776..4abca6d07e 100644 --- a/packages/signals/src/copy_value.rs +++ b/packages/signals/src/copy_value.rs @@ -3,7 +3,9 @@ use dioxus_core::Subscribers; use dioxus_core::{current_owner, current_scope_id, ScopeId}; -use generational_box::{BorrowResult, GenerationalBox, GenerationalBoxId, Storage, UnsyncStorage}; +use generational_box::{ + AnyStorage, BorrowResult, GenerationalBox, GenerationalBoxId, Storage, UnsyncStorage, +}; use std::ops::Deref; use crate::read_impls; @@ -18,15 +20,15 @@ use crate::{default_impl, write_impls, WritableExt}; /// CopyValue is a wrapper around a value to make the value mutable and Copy. /// /// It is internally backed by [`generational_box::GenerationalBox`]. -pub struct CopyValue = UnsyncStorage> { +pub struct CopyValue { pub(crate) value: GenerationalBox, pub(crate) origin_scope: ScopeId, } #[cfg(feature = "serialize")] -impl> serde::Serialize for CopyValue +impl> serde::Serialize for CopyValue where - T: serde::Serialize, + T: serde::Serialize + 'static, { fn serialize(&self, serializer: S) -> Result { self.value.read().serialize(serializer) @@ -34,9 +36,9 @@ where } #[cfg(feature = "serialize")] -impl<'de, T: 'static, Store: Storage> serde::Deserialize<'de> for CopyValue +impl<'de, T, Store: Storage> serde::Deserialize<'de> for CopyValue where - T: serde::Deserialize<'de>, + T: serde::Deserialize<'de> + 'static, { fn deserialize>(deserializer: D) -> Result { let value = T::deserialize(deserializer)?; @@ -61,17 +63,23 @@ impl CopyValue { } } -impl> CopyValue { +impl> CopyValue { /// Create a new CopyValue. The value will be stored in the current component. /// /// Once the component this value is created in is dropped, the value will be dropped. #[track_caller] - pub fn new_maybe_sync(value: T) -> Self { + pub fn new_maybe_sync(value: T) -> Self + where + T: 'static, + { Self::new_with_caller(value, std::panic::Location::caller()) } /// Create a new CopyValue without an owner. This will leak memory if you don't manually drop it. - pub fn leak_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self { + pub fn leak_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self + where + T: 'static, + { Self { value: GenerationalBox::leak(value, caller), origin_scope: current_scope_id().expect("in a virtual dom"), @@ -117,7 +125,10 @@ impl> CopyValue { } /// Manually drop the value in the CopyValue, invalidating the value in the process. - pub fn manually_drop(&self) { + pub fn manually_drop(&self) + where + T: 'static, + { self.value.manually_drop() } @@ -137,7 +148,7 @@ impl> CopyValue { } } -impl> Readable for CopyValue { +impl> Readable for CopyValue { type Target = T; type Storage = S; @@ -155,12 +166,12 @@ impl> Readable for CopyValue { self.value.try_read() } - fn subscribers(&self) -> Option { - None + fn subscribers(&self) -> Subscribers { + Subscribers::new_noop() } } -impl> Writable for CopyValue { +impl> Writable for CopyValue { type WriteMetadata = (); #[track_caller] @@ -172,14 +183,14 @@ impl> Writable for CopyValue { } } -impl> PartialEq for CopyValue { +impl PartialEq for CopyValue { fn eq(&self, other: &Self) -> bool { self.value.ptr_eq(&other.value) } } -impl> Eq for CopyValue {} +impl Eq for CopyValue {} -impl> Deref for CopyValue { +impl> Deref for CopyValue { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { @@ -187,13 +198,13 @@ impl> Deref for CopyValue { } } -impl> Clone for CopyValue { +impl Clone for CopyValue { fn clone(&self) -> Self { *self } } -impl> Copy for CopyValue {} +impl Copy for CopyValue {} read_impls!(CopyValue>); default_impl!(CopyValue>); diff --git a/packages/signals/src/global/memo.rs b/packages/signals/src/global/memo.rs index 5d67694962..c156aae1c1 100644 --- a/packages/signals/src/global/memo.rs +++ b/packages/signals/src/global/memo.rs @@ -3,7 +3,7 @@ use crate::read::ReadableExt; use crate::read_impls; use crate::Memo; -impl InitializeFromFunction for Memo { +impl InitializeFromFunction for Memo { fn initialize_from_function(f: fn() -> T) -> Self { Memo::new(f) } diff --git a/packages/signals/src/global/mod.rs b/packages/signals/src/global/mod.rs index b6f316e295..8b66a90a12 100644 --- a/packages/signals/src/global/mod.rs +++ b/packages/signals/src/global/mod.rs @@ -32,9 +32,10 @@ pub struct Global { /// Allow calling a signal with signal() syntax /// /// Currently only limited to copy types, though could probably specialize for string/arc/rc -impl Deref for Global +impl Deref for Global where - T: Readable + InitializeFromFunction, + T: Readable + InitializeFromFunction + 'static, + T::Target: 'static, { type Target = dyn Fn() -> R; @@ -43,9 +44,9 @@ where } } -impl Readable for Global +impl Readable for Global where - T: Readable + InitializeFromFunction, + T: Readable + InitializeFromFunction + Clone + 'static, { type Target = R; type Storage = T::Storage; @@ -53,23 +54,32 @@ where #[track_caller] fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + R: 'static, + { self.resolve().try_read_unchecked() } #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + R: 'static, + { self.resolve().try_peek_unchecked() } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers + where + R: 'static, + { self.resolve().subscribers() } } -impl Writable for Global +impl Writable for Global where - T: Writable + InitializeFromFunction, + T: Writable + InitializeFromFunction + 'static, { type WriteMetadata = T::WriteMetadata; @@ -81,9 +91,9 @@ where } } -impl Global +impl Global where - T: Writable + InitializeFromFunction, + T: Writable + InitializeFromFunction + 'static, { /// Write this value pub fn write(&self) -> WritableRef<'static, T, R> { @@ -93,12 +103,15 @@ where /// Run a closure with a mutable reference to the signal's value. /// If the signal has been dropped, this will panic. #[track_caller] - pub fn with_mut(&self, f: impl FnOnce(&mut R) -> O) -> O { + pub fn with_mut(&self, f: impl FnOnce(&mut R) -> O) -> O + where + T::Target: 'static, + { self.resolve().with_mut(f) } } -impl Global +impl Global where T: InitializeFromFunction, { @@ -160,7 +173,10 @@ where /// Resolve the global value. This will try to get the existing value from the current virtual dom, and if it doesn't exist, it will create a new one. // NOTE: This is not called "get" or "value" because those methods overlap with Readable and Writable - pub fn resolve(&self) -> T { + pub fn resolve(&self) -> T + where + T: 'static, + { let key = self.key(); let context = get_global_context(); @@ -237,7 +253,7 @@ impl From<&'static Location<'static>> for GlobalKey<'static> { impl GlobalLazyContext { /// Get a signal with the given string key /// The key will be converted to a UUID with the appropriate internal namespace - pub fn get_signal_with_key(&self, key: GlobalKey) -> Option> { + pub fn get_signal_with_key(&self, key: GlobalKey) -> Option> { self.map.borrow().get(&key).map(|f| { *f.downcast_ref::>().unwrap_or_else(|| { panic!( diff --git a/packages/signals/src/global/signal.rs b/packages/signals/src/global/signal.rs index b136ff2617..c8c3def41c 100644 --- a/packages/signals/src/global/signal.rs +++ b/packages/signals/src/global/signal.rs @@ -3,7 +3,7 @@ use crate::read::ReadableExt; use crate::read_impls; use crate::Signal; -impl InitializeFromFunction for Signal { +impl InitializeFromFunction for Signal { fn initialize_from_function(f: fn() -> T) -> Self { Signal::new(f()) } diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 560d472905..a196b9d4a3 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -6,33 +6,33 @@ /// use dioxus::prelude::*; /// use dioxus_core::Subscribers; /// -/// struct MyCopyValue> { +/// struct MyCopyValue { /// value: CopyValue, /// } /// -/// impl> MyCopyValue { +/// impl + 'static> MyCopyValue { /// fn new_maybe_sync(value: T) -> Self { /// Self { value: CopyValue::new_maybe_sync(value) } /// } /// } /// -/// impl> Readable for MyCopyValue { +/// impl + 'static> Readable for MyCopyValue { /// type Target = T; /// type Storage = S; /// /// fn try_read_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// /// fn try_peek_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// -/// fn subscribers(&self) -> Option { +/// fn subscribers(&self) -> Subscribers where T: 'static { /// self.value.subscribers() /// } /// } @@ -79,27 +79,27 @@ macro_rules! default_impl { /// use dioxus::prelude::*; /// use dioxus_core::Subscribers; /// -/// struct MyCopyValue> { +/// struct MyCopyValue { /// value: CopyValue, /// } /// -/// impl> Readable for MyCopyValue { +/// impl + 'static> Readable for MyCopyValue where T: 'static { /// type Target = T; /// type Storage = S; /// /// fn try_read_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// /// fn try_peek_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// -/// fn subscribers(&self) -> Option { +/// fn subscribers(&self) -> Subscribers where T: 'static { /// self.value.subscribers() /// } /// } @@ -157,27 +157,27 @@ macro_rules! read_impls { /// use dioxus::prelude::*; /// use dioxus_core::Subscribers; /// -/// struct MyCopyValue> { +/// struct MyCopyValue { /// value: CopyValue, /// } /// -/// impl> Readable for MyCopyValue { +/// impl + 'static> Readable for MyCopyValue { /// type Target = T; /// type Storage = S; /// /// fn try_read_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// /// fn try_peek_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// -/// fn subscribers(&self) -> Option { +/// fn subscribers(&self) -> Subscribers where T: 'static { /// self.value.subscribers() /// } /// } @@ -238,27 +238,27 @@ macro_rules! fmt_impls { /// use dioxus::prelude::*; /// use dioxus_core::Subscribers; /// -/// struct MyCopyValue> { +/// struct MyCopyValue { /// value: CopyValue, /// } /// -/// impl> Readable for MyCopyValue { +/// impl + 'static> Readable for MyCopyValue { /// type Target = T; /// type Storage = S; /// /// fn try_read_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// /// fn try_peek_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// -/// fn subscribers(&self) -> Option { +/// fn subscribers(&self) -> Subscribers where T: 'static { /// self.value.subscribers() /// } /// } @@ -303,29 +303,29 @@ macro_rules! eq_impls { /// use generational_box::*; /// use dioxus::prelude::*; /// -/// struct MyCopyValue> { +/// struct MyCopyValue { /// value: CopyValue, /// } /// -/// impl> Readable for MyCopyValue { +/// impl + 'static> Readable for MyCopyValue { /// type Target = T; /// type Storage = S; /// /// fn try_read_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowError> { +/// ) -> Result, generational_box::BorrowError> where T: 'static { /// self.value.try_read_unchecked() /// } /// -/// fn peek_unchecked(&self) -> ReadableRef<'static, Self> { +/// fn peek_unchecked(&self) -> ReadableRef<'static, Self> where T: 'static { /// self.value.read_unchecked() /// } /// } /// -/// impl> Writable for MyCopyValue { +/// impl + 'static> Writable for MyCopyValue { /// fn try_write_unchecked( /// &self, -/// ) -> Result, generational_box::BorrowMutError> { +/// ) -> Result, generational_box::BorrowMutError> where T: 'static { /// self.value.try_write_unchecked() /// } /// diff --git a/packages/signals/src/map.rs b/packages/signals/src/map.rs index 1e9dfdd2ad..580ee596e1 100644 --- a/packages/signals/src/map.rs +++ b/packages/signals/src/map.rs @@ -4,7 +4,7 @@ use generational_box::{AnyStorage, BorrowResult}; use std::ops::Deref; /// A read only signal that has been mapped to a new type. -pub struct MappedSignal::Target) -> &O> { +pub struct MappedSignal::Target) -> &O> { value: V, map_fn: F, _marker: std::marker::PhantomData, @@ -34,11 +34,9 @@ where impl MappedSignal where O: ?Sized, - V: Readable, - F: Fn(&V::Target) -> &O, { /// Create a new mapped signal. - pub(crate) fn new(value: V, map_fn: F) -> Self { + pub fn new(value: V, map_fn: F) -> Self { MappedSignal { value, map_fn, @@ -51,6 +49,7 @@ impl Readable for MappedSignal where O: ?Sized, V: Readable, + V::Target: 'static, F: Fn(&V::Target) -> &O, { type Target = O; @@ -68,15 +67,16 @@ where Ok(V::Storage::map(value, |v| (self.map_fn)(v))) } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers { self.value.subscribers() } } impl IntoAttributeValue for MappedSignal where - O: Clone + IntoAttributeValue, + O: Clone + IntoAttributeValue + 'static, V: Readable, + V::Target: 'static, F: Fn(&V::Target) -> &O, { fn into_value(self) -> dioxus_core::AttributeValue { @@ -100,7 +100,7 @@ where /// Currently only limited to clone types, though could probably specialize for string/arc/rc impl Deref for MappedSignal where - O: Clone, + O: Clone + 'static, V: Readable + 'static, F: Fn(&V::Target) -> &O + 'static, { @@ -111,4 +111,4 @@ where } } -read_impls!(MappedSignal where V: Readable, F: Fn(&V::Target) -> &T); +read_impls!(MappedSignal where V: Readable, F: Fn(&V::Target) -> &T); diff --git a/packages/signals/src/map_mut.rs b/packages/signals/src/map_mut.rs index 2e845a4458..a0e8442dcd 100644 --- a/packages/signals/src/map_mut.rs +++ b/packages/signals/src/map_mut.rs @@ -9,8 +9,8 @@ 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, + O: ?Sized, + V, F = fn(&::Target) -> &O, FMut = fn(&mut ::Target) -> &mut O, > { @@ -22,7 +22,7 @@ pub struct MappedMutSignal< impl Clone for MappedMutSignal where - V: Readable + Clone, + V: Clone, F: Clone, FMut: Clone, { @@ -38,7 +38,7 @@ where impl Copy for MappedMutSignal where - V: Readable + Copy, + V: Copy, F: Copy, FMut: Copy, { @@ -47,11 +47,9 @@ where impl MappedMutSignal where O: ?Sized, - V: Readable, - F: Fn(&V::Target) -> &O, { /// Create a new mapped signal. - pub(crate) fn new(value: V, map_fn: F, map_fn_mut: FMut) -> Self { + pub fn new(value: V, map_fn: F, map_fn_mut: FMut) -> Self { MappedMutSignal { value, map_fn, @@ -65,6 +63,7 @@ impl Readable for MappedMutSignal where O: ?Sized, V: Readable, + V::Target: 'static, F: Fn(&V::Target) -> &O, { type Target = O; @@ -72,17 +71,26 @@ where fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + O: 'static, + { let value = self.value.try_read_unchecked()?; Ok(V::Storage::map(value, |v| (self.map_fn)(v))) } - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + O: 'static, + { let value = self.value.try_peek_unchecked()?; Ok(V::Storage::map(value, |v| (self.map_fn)(v))) } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers + where + O: 'static, + { self.value.subscribers() } } @@ -91,6 +99,7 @@ impl Writable for MappedMutSignal where O: ?Sized, V: Writable, + V::Target: 'static, F: Fn(&V::Target) -> &O, FMut: Fn(&mut V::Target) -> &mut O, { @@ -106,8 +115,9 @@ where impl IntoAttributeValue for MappedMutSignal where - O: Clone + IntoAttributeValue, + O: Clone + IntoAttributeValue + 'static, V: Readable, + V::Target: 'static, F: Fn(&V::Target) -> &O, { fn into_value(self) -> dioxus_core::AttributeValue { @@ -134,8 +144,9 @@ where /// Currently only limited to clone types, though could probably specialize for string/arc/rc impl Deref for MappedMutSignal where - O: Clone, + O: Clone + 'static, V: Readable + 'static, + V::Target: 'static, F: Fn(&V::Target) -> &O + 'static, FMut: 'static, { @@ -146,5 +157,5 @@ where } } -read_impls!(MappedMutSignal where V: Readable, F: Fn(&V::Target) -> &T); -write_impls!(MappedMutSignal where V: Writable, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T); +read_impls!(MappedMutSignal where V: Readable, F: Fn(&V::Target) -> &T); +write_impls!(MappedMutSignal where V: Writable, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T); diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 21bc982051..9cdf71ec9a 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,4 +1,3 @@ -use crate::write::Writable; use crate::CopyValue; use crate::{read::Readable, ReadableRef, Signal}; use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt}; @@ -24,17 +23,17 @@ struct UpdateInformation { #[doc(alias = "Selector")] #[doc(alias = "UseMemo")] #[doc(alias = "Memorize")] -pub struct Memo { +pub struct Memo { inner: Signal, update: CopyValue>, } -impl Memo { +impl Memo { /// Create a new memo #[track_caller] pub fn new(f: impl FnMut() -> T + 'static) -> Self where - T: PartialEq, + T: PartialEq + 'static, { Self::new_with_location(f, std::panic::Location::caller()) } @@ -45,7 +44,7 @@ impl Memo { location: &'static std::panic::Location<'static>, ) -> Self where - T: PartialEq, + T: PartialEq + 'static, { let dirty = Arc::new(AtomicBool::new(false)); let (tx, mut rx) = futures_channel::mpsc::unbounded(); @@ -114,7 +113,7 @@ impl Memo { #[track_caller] pub const fn global(constructor: fn() -> T) -> GlobalMemo where - T: PartialEq, + T: PartialEq + 'static, { GlobalMemo::new(constructor) } @@ -123,7 +122,7 @@ impl Memo { #[tracing::instrument(skip(self))] fn recompute(&self) where - T: PartialEq, + T: PartialEq + 'static, { let mut update_copy = self.update; let update_write = update_copy.write(); @@ -141,12 +140,18 @@ impl Memo { } /// Get the scope that the signal was created in. - pub fn origin_scope(&self) -> ScopeId { + pub fn origin_scope(&self) -> ScopeId + where + T: 'static, + { self.inner.origin_scope() } /// Get the id of the signal. - pub fn id(&self) -> generational_box::GenerationalBoxId { + pub fn id(&self) -> generational_box::GenerationalBoxId + where + T: 'static, + { self.inner.id() } } @@ -161,7 +166,10 @@ where #[track_caller] fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError> { + ) -> Result, generational_box::BorrowError> + where + T: 'static, + { // Read the inner generational box instead of the signal so we have more fine grained control over exactly when the subscription happens let read = self.inner.inner.try_read_unchecked()?; @@ -192,18 +200,24 @@ where /// /// If the signal has been dropped, this will panic. #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + T: 'static, + { self.inner.try_peek_unchecked() } - fn subscribers(&self) -> Option { + fn subscribers(&self) -> Subscribers + where + T: 'static, + { self.inner.subscribers() } } impl IntoAttributeValue for Memo where - T: Clone + IntoAttributeValue + PartialEq, + T: Clone + IntoAttributeValue + PartialEq + 'static, { fn into_value(self) -> dioxus_core::AttributeValue { self.with(|f| f.clone().into_value()) @@ -212,7 +226,7 @@ where impl IntoDynNode for Memo where - T: Clone + IntoDynNode + PartialEq, + T: Clone + IntoDynNode + PartialEq + 'static, { fn into_dyn_node(self) -> dioxus_core::DynamicNode { self().into_dyn_node() @@ -227,7 +241,7 @@ impl PartialEq for Memo { impl Deref for Memo where - T: PartialEq, + T: PartialEq + 'static, { type Target = dyn Fn() -> T; @@ -238,10 +252,10 @@ where read_impls!(Memo where T: PartialEq); -impl Clone for Memo { +impl Clone for Memo { fn clone(&self) -> Self { *self } } -impl Copy for Memo {} +impl Copy for Memo {} diff --git a/packages/signals/src/props.rs b/packages/signals/src/props.rs index 3a7f17c1f2..3a8564c539 100644 --- a/packages/signals/src/props.rs +++ b/packages/signals/src/props.rs @@ -7,6 +7,7 @@ pub struct ReadFromMarker(std::marker::PhantomData); impl SuperFrom> for ReadSignal where O: SuperFrom + 'static, + T: 'static, { fn super_from(input: T) -> Self { ReadSignal::new(Signal::new(O::super_from(input))) diff --git a/packages/signals/src/read.rs b/packages/signals/src/read.rs index 9f8bb75621..31a27671a6 100644 --- a/packages/signals/src/read.rs +++ b/packages/signals/src/read.rs @@ -37,7 +37,7 @@ pub type ReadableRef<'a, T: Readable, O = ::Target> = /// ``` pub trait Readable { /// The target type of the reference. - type Target: ?Sized + 'static; + type Target: ?Sized; /// The type of the storage this readable uses. type Storage: AnyStorage; @@ -47,7 +47,9 @@ pub trait Readable { /// NOTE: This method is completely safe because borrow checking is done at runtime. fn try_read_unchecked( &self, - ) -> Result, generational_box::BorrowError>; + ) -> Result, generational_box::BorrowError> + where + Self::Target: 'static; /// Try to peek the current value of the signal without subscribing to updates. If the value has /// been dropped, this will return an error. @@ -55,10 +57,14 @@ pub trait Readable { /// NOTE: This method is completely safe because borrow checking is done at runtime. fn try_peek_unchecked( &self, - ) -> Result, generational_box::BorrowError>; + ) -> Result, generational_box::BorrowError> + where + Self::Target: 'static; /// Get the underlying subscriber list for this readable. This is used to track when the value changes and notify subscribers. - fn subscribers(&self) -> Option; + fn subscribers(&self) -> Subscribers + where + Self::Target: 'static; } /// An extension trait for `Readable` types that provides some convenience methods. @@ -67,13 +73,19 @@ pub trait ReadableExt: Readable { /// 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 #[track_caller] - fn read(&self) -> ReadableRef { + fn read(&self) -> ReadableRef + where + Self::Target: 'static, + { self.try_read().unwrap() } /// Try to get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. #[track_caller] - fn try_read(&self) -> Result, generational_box::BorrowError> { + fn try_read(&self) -> Result, generational_box::BorrowError> + where + Self::Target: 'static, + { self.try_read_unchecked() .map(Self::Storage::downcast_lifetime_ref) } @@ -82,7 +94,10 @@ pub trait ReadableExt: Readable { /// /// NOTE: This method is completely safe because borrow checking is done at runtime. #[track_caller] - fn read_unchecked(&self) -> ReadableRef<'static, Self> { + fn read_unchecked(&self) -> ReadableRef<'static, Self> + where + Self::Target: 'static, + { self.try_read_unchecked().unwrap() } @@ -121,14 +136,20 @@ pub trait ReadableExt: Readable { /// } /// ``` #[track_caller] - fn peek(&self) -> ReadableRef { + fn peek(&self) -> ReadableRef + where + Self::Target: 'static, + { Self::Storage::downcast_lifetime_ref(self.peek_unchecked()) } /// Try to peek the current value of the signal without subscribing to updates. If the value has /// been dropped, this will return an error. #[track_caller] - fn try_peek(&self) -> Result, generational_box::BorrowError> { + fn try_peek(&self) -> Result, generational_box::BorrowError> + where + Self::Target: 'static, + { self.try_peek_unchecked() .map(Self::Storage::downcast_lifetime_ref) } @@ -137,13 +158,19 @@ pub trait ReadableExt: Readable { /// /// If the signal has been dropped, this will panic. #[track_caller] - fn peek_unchecked(&self) -> ReadableRef<'static, Self> { + fn peek_unchecked(&self) -> ReadableRef<'static, Self> + where + Self::Target: 'static, + { self.try_peek_unchecked().unwrap() } - /// 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. + /// Map the references of the readable value to a new type. This lets you provide a view + /// into the readable value without creating a new signal or cloning the 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. + /// 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. For fine grained scoped updates, use + /// stores instead /// /// # Example /// ```rust @@ -167,8 +194,8 @@ pub trait ReadableExt: Readable { /// ``` fn map(self, f: F) -> MappedSignal where - Self: Clone + Sized + 'static, - F: Fn(&Self::Target) -> &O + 'static, + Self: Clone + Sized, + F: Fn(&Self::Target) -> &O, { MappedSignal::new(self, f) } @@ -177,20 +204,26 @@ pub trait ReadableExt: Readable { #[track_caller] fn cloned(&self) -> Self::Target where - Self::Target: Clone, + Self::Target: Clone + 'static, { self.read().clone() } /// Run a function with a reference to the value. If the value has been dropped, this will panic. #[track_caller] - fn with(&self, f: impl FnOnce(&Self::Target) -> O) -> O { + fn with(&self, f: impl FnOnce(&Self::Target) -> O) -> O + where + Self::Target: 'static, + { f(&*self.read()) } /// Run a function with a reference to the value. If the value has been dropped, this will panic. #[track_caller] - fn with_peek(&self, f: impl FnOnce(&Self::Target) -> O) -> O { + fn with_peek(&self, f: impl FnOnce(&Self::Target) -> O) -> O + where + Self::Target: 'static, + { f(&*self.peek()) } @@ -198,7 +231,7 @@ pub trait ReadableExt: Readable { #[track_caller] fn index(&self, index: I) -> ReadableRef>::Output> where - Self::Target: std::ops::Index, + Self::Target: std::ops::Index + 'static, { ::map(self.read(), |v| v.index(index)) } @@ -210,7 +243,7 @@ pub trait ReadableExt: Readable { unsafe fn deref_impl<'a>(&self) -> &'a dyn Fn() -> Self::Target where Self: Sized + 'a, - Self::Target: Clone, + Self::Target: Clone + 'static, { // https://github.com/dtolnay/case-studies/tree/master/callable-types @@ -256,36 +289,52 @@ pub trait ReadableBoxExt: Readable { ReadSignal::new(self) } } +impl + ?Sized> ReadableBoxExt for R {} /// An extension trait for `Readable>` that provides some convenience methods. -pub trait ReadableVecExt: Readable> { +pub trait ReadableVecExt: Readable> { /// Returns the length of the inner vector. #[track_caller] - fn len(&self) -> usize { + fn len(&self) -> usize + where + T: 'static, + { self.with(|v| v.len()) } /// Returns true if the inner vector is empty. #[track_caller] - fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool + where + T: 'static, + { self.with(|v| v.is_empty()) } /// Get the first element of the inner vector. #[track_caller] - fn first(&self) -> Option> { + fn first(&self) -> Option> + where + T: 'static, + { ::try_map(self.read(), |v| v.first()) } /// Get the last element of the inner vector. #[track_caller] - fn last(&self) -> Option> { + fn last(&self) -> Option> + where + T: 'static, + { ::try_map(self.read(), |v| v.last()) } /// Get the element at the given index of the inner vector. #[track_caller] - fn get(&self, index: usize) -> Option> { + fn get(&self, index: usize) -> Option> + where + T: 'static, + { ::try_map(self.read(), |v| v.get(index)) } @@ -318,45 +367,39 @@ impl<'a, T: 'static, R: Readable>> Iterator for ReadableValueIte } } -impl ReadableVecExt for R -where - T: 'static, - R: Readable>, -{ -} +impl ReadableVecExt for R where R: Readable> {} /// An extension trait for `Readable>` that provides some convenience methods. -pub trait ReadableOptionExt: Readable> { +pub trait ReadableOptionExt: Readable> { /// Unwraps the inner value and clones it. #[track_caller] fn unwrap(&self) -> T where - T: Clone, + T: Clone + 'static, { self.as_ref().unwrap().clone() } /// Attempts to read the inner value of the Option. #[track_caller] - fn as_ref(&self) -> Option> { + fn as_ref(&self) -> Option> + where + T: 'static, + { ::try_map(self.read(), |v| v.as_ref()) } } -impl ReadableOptionExt for R -where - T: 'static, - R: Readable>, -{ -} +impl ReadableOptionExt for R where R: Readable> {} /// An extension trait for `Readable>` that provides some convenience methods. -pub trait ReadableResultExt: Readable> { +pub trait ReadableResultExt: Readable> { /// Unwraps the inner value and clones it. #[track_caller] fn unwrap(&self) -> T where - T: Clone, + T: Clone + 'static, + E: 'static, { self.as_ref() .unwrap_or_else(|_| panic!("Tried to unwrap a Result that was an error")) @@ -365,17 +408,15 @@ pub trait ReadableResultExt: Readable Result, ReadableRef> { + fn as_ref(&self) -> Result, ReadableRef> + where + T: 'static, + E: 'static, + { ::try_map(self.read(), |v| v.as_ref().ok()).ok_or( ::map(self.read(), |v| v.as_ref().err().unwrap()), ) } } -impl ReadableResultExt for R -where - T: 'static, - E: 'static, - R: Readable>, -{ -} +impl ReadableResultExt for R where R: Readable> {} diff --git a/packages/signals/src/set_compare.rs b/packages/signals/src/set_compare.rs index f01bfcfb01..14e3156e92 100644 --- a/packages/signals/src/set_compare.rs +++ b/packages/signals/src/set_compare.rs @@ -1,4 +1,4 @@ -use crate::{write::Writable, ReadableExt}; +use crate::{write::WritableExt, ReadableExt}; use std::hash::Hash; use dioxus_core::ReactiveContext; @@ -9,12 +9,11 @@ use crate::{CopyValue, ReadSignal, Signal, SignalData}; use rustc_hash::FxHashMap; /// An object that can efficiently compare a value to a set of values. -#[derive(Debug)] -pub struct SetCompare> = UnsyncStorage> { +pub struct SetCompare { subscribers: CopyValue>>, } -impl SetCompare { +impl SetCompare { /// Creates a new [`SetCompare`] which efficiently tracks when a value changes to check if it is equal to a set of values. /// /// Generally, you shouldn't need to use this hook. Instead you can use [`crate::Memo`]. If you have many values that you need to compare to a single value, this hook will change updates from O(n) to O(1) where n is the number of values you are comparing to. @@ -24,13 +23,13 @@ impl SetCompare { } } -impl>> SetCompare { +impl> + 'static> SetCompare { /// Creates a new [`SetCompare`] that may be `Sync + Send` which efficiently tracks when a value changes to check if it is equal to a set of values. /// /// Generally, you shouldn't need to use this hook. Instead you can use [`crate::Memo`]. If you have many values that you need to compare to a single value, this hook will change updates from O(n) to O(1) where n is the number of values you are comparing to. #[track_caller] - pub fn new_maybe_sync(mut f: impl FnMut() -> R + 'static) -> SetCompare { - let subscribers: CopyValue>> = + pub fn new_maybe_sync(mut f: impl FnMut() -> R + 'static) -> SetCompare { + let subscribers: CopyValue>> = CopyValue::new(FxHashMap::default()); let mut previous = CopyValue::new(None); @@ -67,7 +66,7 @@ impl>> SetCompare { } } -impl SetCompare { +impl SetCompare { /// Returns a signal which is true when the value is equal to the value passed to this function. pub fn equal(&mut self, value: R) -> ReadSignal { let subscribers = self.subscribers.write(); @@ -85,7 +84,7 @@ impl SetCompare { } } -impl>> PartialEq for SetCompare { +impl>> PartialEq for SetCompare { fn eq(&self, other: &Self) -> bool { self.subscribers == other.subscribers } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 2e0f54fcc5..1e51a5ae5c 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -4,13 +4,13 @@ use crate::{ }; use dioxus_core::{IntoAttributeValue, IntoDynNode, ReactiveContext, ScopeId, Subscribers}; use generational_box::{BorrowResult, Storage, SyncStorage, UnsyncStorage}; -use std::{any::Any, collections::HashSet, ops::Deref, sync::Arc, sync::Mutex}; +use std::{collections::HashSet, ops::Deref, sync::Arc, sync::Mutex}; #[doc = include_str!("../docs/signals.md")] #[doc(alias = "State")] #[doc(alias = "UseState")] #[doc(alias = "UseRef")] -pub struct Signal> = UnsyncStorage> { +pub struct Signal { pub(crate) inner: CopyValue, S>, } @@ -139,11 +139,14 @@ impl Signal { } } -impl>> Signal { +impl>> Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. #[track_caller] #[tracing::instrument(skip(value))] - pub fn new_maybe_sync(value: T) -> Self { + pub fn new_maybe_sync(value: T) -> Self + where + T: 'static, + { Self { inner: CopyValue::, S>::new_maybe_sync(SignalData { subscribers: Default::default(), @@ -165,7 +168,10 @@ impl>> Signal { /// use_hook(move || Signal::new_with_caller(function(), caller)) /// } /// ``` - pub fn new_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self { + pub fn new_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self + where + T: 'static, + { Self { inner: CopyValue::new_with_caller( SignalData { @@ -178,7 +184,10 @@ impl>> Signal { } /// Create a new Signal without an owner. This will leak memory if you don't manually drop it. - pub fn leak_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self { + pub fn leak_with_caller(value: T, caller: &'static std::panic::Location<'static>) -> Self + where + T: 'static, + { Self { inner: CopyValue::leak_with_caller( SignalData { @@ -217,7 +226,10 @@ impl>> Signal { } /// Point to another signal. This will subscribe the other signal to all subscribers of this signal. - pub fn point_to(&self, other: Self) -> BorrowResult { + pub fn point_to(&self, other: Self) -> BorrowResult + where + T: 'static, + { #[allow(clippy::mutable_key_type)] let this_subscribers = self.inner.value.read().subscribers.lock().unwrap().clone(); let other_read = other.inner.value.read(); @@ -228,7 +240,10 @@ impl>> Signal { } /// Drop the value out of the signal, invalidating the signal in the process. - pub fn manually_drop(&self) { + pub fn manually_drop(&self) + where + T: 'static, + { self.inner.manually_drop() } @@ -237,7 +252,10 @@ impl>> Signal { self.inner.origin_scope() } - fn update_subscribers(&self) { + fn update_subscribers(&self) + where + T: 'static, + { { let inner = self.inner.read(); @@ -283,7 +301,7 @@ impl>> Signal { /// *signal.write_silent() += 1; /// ``` /// - /// 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. + /// Instead you can use the [`peek`](ReadableExt::peek) and [`write`](WritableExt::write) methods instead. The peek method will not subscribe to the current scope which will avoid an infinite loop if you are reading and writing to the same signal in the same scope. /// ```rust, no_run /// # use dioxus::prelude::*; /// let mut signal = use_signal(|| 0); @@ -384,7 +402,10 @@ impl>> Readable for Signal { type Storage = S; #[track_caller] - fn try_read_unchecked(&self) -> BorrowResult> { + fn try_read_unchecked(&self) -> BorrowResult> + where + T: 'static, + { let inner = self.inner.try_read_unchecked()?; if let Some(reactive_context) = ReactiveContext::current() { @@ -399,19 +420,25 @@ impl>> Readable for Signal { /// /// If the signal has been dropped, this will panic. #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { + fn try_peek_unchecked(&self) -> BorrowResult> + where + T: 'static, + { self.inner .try_read_unchecked() .map(|inner| S::map(inner, |v| &v.value)) } - fn subscribers(&self) -> Option { - Some(self.inner.read().subscribers.clone().into()) + fn subscribers(&self) -> Subscribers + where + T: 'static, + { + self.inner.read().subscribers.clone().into() } } impl>> Writable for Signal { - type WriteMetadata = Box; + type WriteMetadata = SignalSubscriberDrop; #[track_caller] fn try_write_unchecked( @@ -423,11 +450,11 @@ impl>> Writable for Signal { let borrow = S::map_mut(inner.into_inner(), |v| &mut v.value); WriteLock::new_with_metadata( borrow, - Box::new(SignalSubscriberDrop { + SignalSubscriberDrop { signal: *self, #[cfg(debug_assertions)] origin, - }) as Box, + }, ) }) } @@ -435,7 +462,7 @@ impl>> Writable for Signal { impl IntoAttributeValue for Signal where - T: Clone + IntoAttributeValue, + T: Clone + IntoAttributeValue + 'static, { fn into_value(self) -> dioxus_core::AttributeValue { self.with(|f| f.clone().into_value()) @@ -444,25 +471,25 @@ where impl IntoDynNode for Signal where - T: Clone + IntoDynNode, + T: Clone + IntoDynNode + 'static, { fn into_dyn_node(self) -> dioxus_core::DynamicNode { self().into_dyn_node() } } -impl>> PartialEq for Signal { +impl>> PartialEq for Signal { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } -impl>> Eq for Signal {} +impl>> Eq for Signal {} /// Allow calling a signal with signal() syntax /// /// Currently only limited to copy types, though could probably specialize for string/arc/rc -impl> + 'static> Deref for Signal { +impl> + 'static> Deref for Signal { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { @@ -471,7 +498,7 @@ impl> + 'static> Deref for Signal { } #[cfg(feature = "serialize")] -impl>> serde::Serialize +impl> + 'static> serde::Serialize for Signal { fn serialize(&self, serializer: S) -> Result { @@ -480,7 +507,7 @@ impl>> serde::Serial } #[cfg(feature = "serialize")] -impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage>> +impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage> + 'static> serde::Deserialize<'de> for Signal { fn deserialize>(deserializer: D) -> Result { @@ -488,14 +515,16 @@ impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage>> } } -struct SignalSubscriberDrop>> { +#[doc(hidden)] +/// A drop guard that will update the subscribers of the signal when it is dropped. +pub struct SignalSubscriberDrop> + 'static> { signal: Signal, #[cfg(debug_assertions)] origin: &'static std::panic::Location<'static>, } #[allow(clippy::no_effect)] -impl>> Drop for SignalSubscriberDrop { +impl> + 'static> Drop for SignalSubscriberDrop { fn drop(&mut self) { #[cfg(debug_assertions)] { @@ -512,10 +541,10 @@ fmt_impls!(Signal>>); default_impl!(Signal>>); write_impls!(Signal>>); -impl>> Clone for Signal { +impl Clone for Signal { fn clone(&self) -> Self { *self } } -impl>> Copy for Signal {} +impl Copy for Signal {} diff --git a/packages/signals/src/warnings.rs b/packages/signals/src/warnings.rs index 69c9e4fc6f..f6bbe6b0af 100644 --- a/packages/signals/src/warnings.rs +++ b/packages/signals/src/warnings.rs @@ -4,7 +4,7 @@ use warnings::warning; /// A warning that is triggered when a copy value is used in a higher scope that it is owned by #[warning] -pub fn copy_value_hoisted>( +pub fn copy_value_hoisted + 'static>( value: &crate::CopyValue, caller: &'static std::panic::Location<'static>, ) { diff --git a/packages/signals/src/write.rs b/packages/signals/src/write.rs index fe5046423b..461ca6ef95 100644 --- a/packages/signals/src/write.rs +++ b/packages/signals/src/write.rs @@ -36,34 +36,16 @@ pub type WritableRef<'a, T: Writable, O = ::Target> = /// ``` pub trait Writable: Readable { /// Additional data associated with the write reference. - type WriteMetadata: 'static; - - /// Get a mutable reference to the value. If the value has been dropped, this will panic. - #[track_caller] - fn write(&mut self) -> WritableRef<'_, Self> { - self.try_write().unwrap() - } - - /// Try to get a mutable reference to the value. - #[track_caller] - fn try_write(&mut self) -> Result, generational_box::BorrowMutError> { - self.try_write_unchecked().map(WriteLock::downcast_lifetime) - } + type WriteMetadata; /// Try to get a mutable reference to the value without checking the lifetime. This will update any subscribers. /// /// NOTE: This method is completely safe because borrow checking is done at runtime. fn try_write_unchecked( &self, - ) -> Result, generational_box::BorrowMutError>; - - /// Get a mutable reference to the value without checking the lifetime. This will update any subscribers. - /// - /// NOTE: This method is completely safe because borrow checking is done at runtime. - #[track_caller] - fn write_unchecked(&self) -> WritableRef<'static, Self> { - self.try_write_unchecked().unwrap() - } + ) -> Result, generational_box::BorrowMutError> + where + Self::Target: 'static; } /// A mutable reference to a writable value. This reference acts similarly to [`std::cell::RefMut`], but it has extra debug information @@ -176,7 +158,7 @@ pub trait Writable: Readable { /// - 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 = ()> { +pub struct WriteLock<'a, T: ?Sized + 'a, S: AnyStorage = UnsyncStorage, D = ()> { write: S::Mut<'a, T>, data: D, } @@ -256,7 +238,7 @@ impl<'a, T: ?Sized, S: AnyStorage, D> WriteLock<'a, T, S, D> { impl Deref for WriteLock<'_, T, S, D> where S: AnyStorage, - T: ?Sized + 'static, + T: ?Sized, { type Target = T; @@ -268,7 +250,7 @@ where impl DerefMut for WriteLock<'_, T, S, D> where S: AnyStorage, - T: ?Sized + 'static, + T: ?Sized, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.write @@ -277,9 +259,41 @@ where /// 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. + /// Get a mutable reference to the value. If the value has been dropped, this will panic. + #[track_caller] + fn write(&mut self) -> WritableRef<'_, Self> + where + Self::Target: 'static, + { + self.try_write().unwrap() + } + + /// Try to get a mutable reference to the value. + #[track_caller] + fn try_write(&mut self) -> Result, generational_box::BorrowMutError> + where + Self::Target: 'static, + { + self.try_write_unchecked().map(WriteLock::downcast_lifetime) + } + + /// Get a mutable reference to the value without checking the lifetime. This will update any subscribers. /// - /// 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. + #[track_caller] + fn write_unchecked(&self) -> WritableRef<'static, Self> + where + Self::Target: 'static, + { + self.try_write_unchecked().unwrap() + } + + /// Map the references and mutable references of the writable value to a new type. This lets you provide a view + /// into the writable value without creating a new signal or cloning the value. + /// + /// Anything that subscribes to the writable value will be rerun whenever the original value changes or you write to this + /// scoped value, even if the view does not change. If you want to memorize the view, you can use a [`crate::Memo`] instead. + /// For fine grained scoped updates, use stores instead /// /// # Example /// ```rust @@ -306,17 +320,20 @@ pub trait WritableExt: Writable { /// ``` fn map_mut(self, f: F, f_mut: FMut) -> MappedMutSignal where - Self: Clone + Sized + 'static, - O: ?Sized + 'static, - F: Fn(&Self::Target) -> &O + 'static, - FMut: Fn(&mut Self::Target) -> &mut O + 'static, + Self: Sized, + O: ?Sized, + F: Fn(&Self::Target) -> &O, + FMut: Fn(&mut Self::Target) -> &mut O, { MappedMutSignal::new(self, f, f_mut) } /// Run a function with a mutable reference to the value. If the value has been dropped, this will panic. #[track_caller] - fn with_mut(&mut self, f: impl FnOnce(&mut Self::Target) -> O) -> O { + fn with_mut(&mut self, f: impl FnOnce(&mut Self::Target) -> O) -> O + where + Self::Target: 'static, + { f(&mut *self.write()) } @@ -324,7 +341,7 @@ pub trait WritableExt: Writable { #[track_caller] fn set(&mut self, value: Self::Target) where - Self::Target: Sized, + Self::Target: Sized + 'static, { *self.write() = value; } @@ -333,7 +350,7 @@ pub trait WritableExt: Writable { #[track_caller] fn toggle(&mut self) where - Self::Target: std::ops::Not + Clone, + Self::Target: std::ops::Not + Clone + 'static, { let inverted = !(*self.peek()).clone(); self.set(inverted); @@ -346,7 +363,7 @@ pub trait WritableExt: Writable { index: I, ) -> WritableRef<'_, Self, >::Output> where - Self::Target: std::ops::IndexMut, + Self::Target: std::ops::IndexMut + 'static, { WriteLock::map(self.write(), |v| v.index_mut(index)) } @@ -355,7 +372,7 @@ pub trait WritableExt: Writable { #[track_caller] fn take(&mut self) -> Self::Target where - Self::Target: Default, + Self::Target: Default + 'static, { self.with_mut(std::mem::take) } @@ -364,7 +381,7 @@ pub trait WritableExt: Writable { #[track_caller] fn replace(&mut self, value: Self::Target) -> Self::Target where - Self::Target: Sized, + Self::Target: Sized + 'static, { self.with_mut(|v| std::mem::replace(v, value)) } @@ -390,16 +407,22 @@ impl + 'static> WritableBoxedExt for T { } /// An extension trait for [`Writable>`]` that provides some convenience methods. -pub trait WritableOptionExt: Writable> { +pub trait WritableOptionExt: Writable> { /// Gets the value out of the Option, or inserts the given value if the Option is empty. #[track_caller] - fn get_or_insert(&mut self, default: T) -> WritableRef<'_, Self, T> { + fn get_or_insert(&mut self, default: T) -> WritableRef<'_, Self, T> + where + T: 'static, + { self.get_or_insert_with(|| default) } /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty. #[track_caller] - fn get_or_insert_with(&mut self, default: impl FnOnce() -> T) -> WritableRef<'_, Self, T> { + fn get_or_insert_with(&mut self, default: impl FnOnce() -> T) -> WritableRef<'_, Self, T> + where + T: 'static, + { let is_none = self.read().is_none(); if is_none { self.with_mut(|v| *v = Some(default())); @@ -411,83 +434,114 @@ pub trait WritableOptionExt: Writable> { /// Attempts to write the inner value of the Option. #[track_caller] - fn as_mut(&mut self) -> Option> { + fn as_mut(&mut self) -> Option> + where + T: 'static, + { WriteLock::filter_map(self.write(), |v: &mut Option| v.as_mut()) } } -impl WritableOptionExt for W -where - T: 'static, - W: Writable>, -{ -} +impl WritableOptionExt for W where W: Writable> {} /// An extension trait for [`Writable>`] that provides some convenience methods. -pub trait WritableVecExt: Writable> { +pub trait WritableVecExt: Writable> { /// Pushes a new value to the end of the vector. #[track_caller] - fn push(&mut self, value: T) { + fn push(&mut self, value: T) + where + T: 'static, + { self.with_mut(|v| v.push(value)) } /// Pops the last value from the vector. #[track_caller] - fn pop(&mut self) -> Option { + fn pop(&mut self) -> Option + where + T: 'static, + { self.with_mut(|v| v.pop()) } /// Inserts a new value at the given index. #[track_caller] - fn insert(&mut self, index: usize, value: T) { + fn insert(&mut self, index: usize, value: T) + where + T: 'static, + { self.with_mut(|v| v.insert(index, value)) } /// Removes the value at the given index. #[track_caller] - fn remove(&mut self, index: usize) -> T { + fn remove(&mut self, index: usize) -> T + where + T: 'static, + { self.with_mut(|v| v.remove(index)) } /// Clears the vector, removing all values. #[track_caller] - fn clear(&mut self) { + fn clear(&mut self) + where + T: 'static, + { self.with_mut(|v| v.clear()) } /// Extends the vector with the given iterator. #[track_caller] - fn extend(&mut self, iter: impl IntoIterator) { + fn extend(&mut self, iter: impl IntoIterator) + where + T: 'static, + { self.with_mut(|v| v.extend(iter)) } /// Truncates the vector to the given length. #[track_caller] - fn truncate(&mut self, len: usize) { + fn truncate(&mut self, len: usize) + where + T: 'static, + { self.with_mut(|v| v.truncate(len)) } /// Swaps two values in the vector. #[track_caller] - fn swap_remove(&mut self, index: usize) -> T { + fn swap_remove(&mut self, index: usize) -> T + where + T: 'static, + { self.with_mut(|v| v.swap_remove(index)) } /// Retains only the values that match the given predicate. #[track_caller] - fn retain(&mut self, f: impl FnMut(&T) -> bool) { + fn retain(&mut self, f: impl FnMut(&T) -> bool) + where + T: 'static, + { self.with_mut(|v| v.retain(f)) } /// Splits the vector into two at the given index. #[track_caller] - fn split_off(&mut self, at: usize) -> Vec { + fn split_off(&mut self, at: usize) -> Vec + where + T: 'static, + { self.with_mut(|v| v.split_off(at)) } /// Try to mutably get an element from the vector. #[track_caller] - fn get_mut(&mut self, index: usize) -> Option> { + fn get_mut(&mut self, index: usize) -> Option> + where + T: 'static, + { WriteLock::filter_map(self.write(), |v: &mut Vec| v.get_mut(index)) } @@ -524,9 +578,4 @@ impl<'a, T: 'static, R: Writable>> Iterator for WritableValueIte } } -impl WritableVecExt for W -where - T: 'static, - W: Writable>, -{ -} +impl WritableVecExt for W where W: Writable> {} diff --git a/packages/stores-macro/Cargo.toml b/packages/stores-macro/Cargo.toml new file mode 100644 index 0000000000..bd141cb34b --- /dev/null +++ b/packages/stores-macro/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dioxus-stores-macro" +version = { workspace = true } +edition = "2021" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com/docs/0.5/guide/en/getting_started/fullstack.html" +keywords = ["dom", "ui", "gui", "react", "liveview"] +authors = ["Jonathan Kelley", "Evan Almloff"] +license = "MIT OR Apache-2.0" +description = "Server function macros for Dioxus" + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features = ["full"] } +convert_case = { workspace = true } + +[dev-dependencies] +dioxus = { workspace = true } +dioxus-stores = { workspace = true } + +[lib] +proc-macro = true + +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/stores-macro/src/derive.rs b/packages/stores-macro/src/derive.rs new file mode 100644 index 0000000000..c3cf829d18 --- /dev/null +++ b/packages/stores-macro/src/derive.rs @@ -0,0 +1,493 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse_quote, spanned::Spanned, DataEnum, DataStruct, DeriveInput, Field, Fields, Generics, + Ident, Index, LitInt, +}; + +pub(crate) fn derive_store(input: DeriveInput) -> syn::Result { + let item_name = &input.ident; + let extension_trait_name = format_ident!("{}StoreExt", item_name); + let transposed_name = format_ident!("{}StoreTransposed", item_name); + + // Create generics for the extension trait and transposed struct. Both items need the original generics + // and bounds plus an extra __Lens type used in the store generics + let generics = &input.generics; + let mut extension_generics = generics.clone(); + extension_generics.params.insert(0, parse_quote!(__Lens)); + + match &input.data { + syn::Data::Struct(data_struct) => derive_store_struct( + &input, + data_struct, + extension_trait_name, + transposed_name, + extension_generics, + ), + syn::Data::Enum(data_enum) => derive_store_enum( + &input, + data_enum, + extension_trait_name, + transposed_name, + extension_generics, + ), + syn::Data::Union(_) => Err(syn::Error::new( + input.span(), + "Store macro does not support unions", + )), + } +} + +// For structs, we derive two items: +// - An extension trait with methods to access the fields of the struct as stores and a `transpose` method +// - A transposed version of the struct with all fields wrapped in stores +fn derive_store_struct( + input: &DeriveInput, + structure: &DataStruct, + extension_trait_name: Ident, + transposed_name: Ident, + extension_generics: Generics, +) -> syn::Result { + let struct_name = &input.ident; + let fields = &structure.fields; + let visibility = &input.vis; + + // We don't need to do anything if there are no fields + if fields.is_empty() { + return Ok(quote! {}); + } + + let generics = &input.generics; + let (_, ty_generics, _) = generics.split_for_impl(); + let (extension_impl_generics, extension_generics, extension_where_clause) = + extension_generics.split_for_impl(); + + // We collect the definitions and implementations for the extension trait methods along with the types of the fields in the transposed struct + let mut implementations = Vec::new(); + let mut definitions = Vec::new(); + let mut transposed_fields = Vec::new(); + + for (field_index, field) in fields.iter().enumerate() { + generate_field_methods( + field_index, + field, + struct_name, + &ty_generics, + &mut transposed_fields, + &mut definitions, + &mut implementations, + ); + } + + // Add a transpose method to turn the stored struct into a struct with all fields as stores + // We need the copy bound here because the store will be copied into the selector for each field + let definition = quote! { + fn transpose( + self, + ) -> #transposed_name #extension_generics where Self: ::std::marker::Copy; + }; + definitions.push(definition); + let field_names = fields + .iter() + .enumerate() + .map(|(i, field)| function_name_from_field(i, field)) + .collect::>(); + // Construct the transposed struct with the fields as stores from the field variables in scope + let construct = match &structure.fields { + Fields::Named(_) => { + quote! { #transposed_name { #(#field_names),* } } + } + Fields::Unnamed(_) => { + quote! { #transposed_name(#(#field_names),*) } + } + Fields::Unit => { + quote! { #transposed_name } + } + }; + let implementation = quote! { + fn transpose( + self, + ) -> #transposed_name #extension_generics where Self: ::std::marker::Copy { + // Convert each field into the corresponding store + #( + let #field_names = self.#field_names(); + )* + #construct + } + }; + implementations.push(implementation); + + // Generate the transposed struct definition + let transposed_struct = match &structure.fields { + Fields::Named(_) => { + quote! { #visibility struct #transposed_name #extension_generics #extension_where_clause {#(#transposed_fields),*} } + } + Fields::Unnamed(_) => { + quote! { #visibility struct #transposed_name #extension_generics (#(#transposed_fields),*) #extension_where_clause; } + } + Fields::Unit => { + quote! {#visibility struct #transposed_name #extension_generics #extension_where_clause;} + } + }; + + // Expand to the extension trait and its implementation for the store alongside the transposed struct + Ok(quote! { + #visibility trait #extension_trait_name #extension_generics where #extension_where_clause { + #( + #definitions + )* + } + + impl #extension_impl_generics #extension_trait_name #extension_generics for dioxus_stores::Store<#struct_name #ty_generics, __Lens> #extension_where_clause { + #( + #implementations + )* + } + + #transposed_struct + }) +} + +fn generate_field_methods( + field_index: usize, + field: &syn::Field, + struct_name: &Ident, + ty_generics: &syn::TypeGenerics, + transposed_fields: &mut Vec, + definitions: &mut Vec, + implementations: &mut Vec, +) { + let vis = &field.vis; + let field_name = &field.ident; + let colon = field.colon_token.as_ref(); + + // When we map the field, we need to use either the field name for named fields or the index for unnamed fields. + let field_accessor = field_name.as_ref().map_or_else( + || Index::from(field_index).to_token_stream(), + |name| name.to_token_stream(), + ); + let function_name = function_name_from_field(field_index, field); + let field_type = &field.ty; + let store_type = mapped_type(struct_name, ty_generics, field_type); + + transposed_fields.push(quote! { #vis #field_name #colon #store_type }); + + // Each field gets its own reactive scope within the child based on the field's index + let ordinal = LitInt::new(&field_index.to_string(), field.span()); + + let definition = quote! { + fn #function_name( + self, + ) -> #store_type; + }; + definitions.push(definition); + let implementation = quote! { + fn #function_name( + self, + ) -> #store_type { + let __map_field: fn(&#struct_name #ty_generics) -> &#field_type = |value| &value.#field_accessor; + let __map_mut_field: fn(&mut #struct_name #ty_generics) -> &mut #field_type = |value| &mut value.#field_accessor; + // Map the field into a child selector that tracks the field + let scope = self.into_selector().child( + #ordinal, + __map_field, + __map_mut_field, + ); + // Convert the selector into a store + ::std::convert::Into::into(scope) + } + }; + implementations.push(implementation); +} + +// For enums, we derive two items: +// - An extension trait with methods to check if the store is a specific variant and a method +// to access the field of that variant if there is only one field +// - A transposed version of the enum with all fields wrapped in stores +fn derive_store_enum( + input: &DeriveInput, + structure: &DataEnum, + extension_trait_name: Ident, + transposed_name: Ident, + extension_generics: Generics, +) -> syn::Result { + let enum_name = &input.ident; + let variants = &structure.variants; + let visibility = &input.vis; + + let generics = &input.generics; + let (_, ty_generics, _) = generics.split_for_impl(); + let (extension_impl_generics, extension_generics, extension_where_clause) = + extension_generics.split_for_impl(); + + // We collect the definitions and implementations for the extension trait methods along with the types of the fields in the transposed enum + // and the match arms for the transposed enum. + let mut implementations = Vec::new(); + let mut definitions = Vec::new(); + let mut transposed_variants = Vec::new(); + let mut transposed_match_arms = Vec::new(); + + // The generated items that check the variant of the enum need to read the enum which requires these extra bounds + let readable_bounds = quote! { __Lens: dioxus_stores::macro_helpers::dioxus_signals::Readable, #enum_name #ty_generics: 'static }; + + for variant in variants { + let variant_name = &variant.ident; + let snake_case_variant = format_ident!("{}", variant_name.to_string().to_case(Case::Snake)); + let is_fn = format_ident!("is_{}", snake_case_variant); + + generate_is_variant_method( + &is_fn, + variant_name, + enum_name, + readable_bounds.clone(), + &mut definitions, + &mut implementations, + ); + + let mut transposed_fields = Vec::new(); + let mut transposed_field_selectors = Vec::new(); + let fields = &variant.fields; + for (i, field) in fields.iter().enumerate() { + let vis = &field.vis; + let field_name = &field.ident; + let colon = field.colon_token.as_ref(); + let field_type = &field.ty; + let store_type = mapped_type(enum_name, &ty_generics, field_type); + + // Push the field for the transposed enum + transposed_fields.push(quote! { #vis #field_name #colon #store_type }); + + // Generate the code to get Store from the enum + let select_field = select_enum_variant_field( + enum_name, + &ty_generics, + variant_name, + field, + i, + fields.len(), + ); + + // If there is only one field, generate a field() -> Option> method + if fields.len() == 1 { + generate_as_variant_method( + &is_fn, + &snake_case_variant, + &select_field, + &store_type, + &readable_bounds, + &mut definitions, + &mut implementations, + ); + } + + transposed_field_selectors.push(select_field); + } + + // Now that we have the types for the field selectors within the variant, + // we can construct the transposed variant and the logic to turn the normal + // version of that variant into the store version + let field_names = fields + .iter() + .enumerate() + .map(|(i, field)| function_name_from_field(i, field)) + .collect::>(); + // Turn each field into its store + let construct_fields = field_names + .iter() + .zip(transposed_field_selectors.iter()) + .map(|(name, selector)| { + quote! { let #name = { #selector }; } + }); + // Merge the stores into the variant + let construct_variant = match &fields { + Fields::Named(_) => { + quote! { #transposed_name::#variant_name { #(#field_names),* } } + } + Fields::Unnamed(_) => { + quote! { #transposed_name::#variant_name(#(#field_names),*) } + } + Fields::Unit => { + quote! { #transposed_name::#variant_name } + } + }; + let match_arm = quote! { + #enum_name::#variant_name { .. } => { + #(#construct_fields)* + #construct_variant + }, + }; + transposed_match_arms.push(match_arm); + + // Push the type definition of the variant to the transposed enum + let transposed_variant = match &fields { + Fields::Named(_) => { + quote! { #variant_name {#(#transposed_fields),*} } + } + Fields::Unnamed(_) => { + quote! { #variant_name (#(#transposed_fields),*) } + } + Fields::Unit => { + quote! { #variant_name } + } + }; + transposed_variants.push(transposed_variant); + } + + let definition = quote! { + fn transpose( + self, + ) -> #transposed_name #extension_generics where #readable_bounds, Self: ::std::marker::Copy; + }; + definitions.push(definition); + let implementation = quote! { + fn transpose( + self, + ) -> #transposed_name #extension_generics where #readable_bounds, Self: ::std::marker::Copy { + // We only do a shallow read of the store to get the current variant. We only need to rerun + // this match when the variant changes, not when the fields change + self.selector().track_shallow(); + let read = dioxus_stores::macro_helpers::dioxus_signals::ReadableExt::peek(self.selector()); + match &*read { + #(#transposed_match_arms)* + // The enum may be #[non_exhaustive] + #[allow(unreachable)] + _ => unreachable!(), + } + } + }; + implementations.push(implementation); + + let transposed_enum = quote! { #visibility enum #transposed_name #extension_generics #extension_where_clause {#(#transposed_variants),*} }; + + // Expand to the extension trait and its implementation for the store alongside the transposed enum + Ok(quote! { + #visibility trait #extension_trait_name #extension_generics where #extension_where_clause { + #( + #definitions + )* + } + + impl #extension_impl_generics #extension_trait_name #extension_generics for dioxus_stores::Store<#enum_name #ty_generics, __Lens> #extension_where_clause { + #( + #implementations + )* + } + + #transposed_enum + }) +} + +fn generate_is_variant_method( + is_fn: &Ident, + variant_name: &Ident, + enum_name: &Ident, + readable_bounds: TokenStream2, + definitions: &mut Vec, + implementations: &mut Vec, +) { + // Generate a is_variant method that checks if the store is a specific variant + let definition = quote! { + fn #is_fn( + &self, + ) -> bool where #readable_bounds; + }; + definitions.push(definition); + let implementation = quote! { + fn #is_fn( + &self, + ) -> bool where #readable_bounds { + // Reading the current variant only tracks the shallow value of the store. Writing to a specific + // variant will not cause the variant to change, so we don't need to subscribe deeply + self.selector().track_shallow(); + let ref_self = dioxus_stores::macro_helpers::dioxus_signals::ReadableExt::peek(self.selector()); + matches!(&*ref_self, #enum_name::#variant_name { .. }) + } + }; + implementations.push(implementation); +} + +/// Generate a method to turn Store into Option> if the variant only has one field. +fn generate_as_variant_method( + is_fn: &Ident, + snake_case_variant: &Ident, + select_field: &TokenStream2, + store_type: &TokenStream2, + readable_bounds: &TokenStream2, + definitions: &mut Vec, + implementations: &mut Vec, +) { + let definition = quote! { + fn #snake_case_variant( + self, + ) -> Option<#store_type> where #readable_bounds; + }; + definitions.push(definition); + let implementation = quote! { + fn #snake_case_variant( + self, + ) -> Option<#store_type> where #readable_bounds { + self.#is_fn().then(|| { + #select_field + }) + } + }; + implementations.push(implementation); +} + +fn select_enum_variant_field( + enum_name: &Ident, + ty_generics: &syn::TypeGenerics, + variant_name: &Ident, + field: &Field, + field_index: usize, + field_count: usize, +) -> TokenStream2 { + // Generate the match arm for the field + let function_name = function_name_from_field(field_index, field); + let field_type = &field.ty; + let match_field = if field.ident.is_none() { + let ignore_before = (0..field_index).map(|_| quote!(_)); + let ignore_after = (field_index + 1..field_count).map(|_| quote!(_)); + quote!( ( #(#ignore_before,)* #function_name, #(#ignore_after),* ) ) + } else { + quote!( { #function_name, .. }) + }; + let ordinal = LitInt::new(&field_index.to_string(), variant_name.span()); + quote! { + let __map_field: fn(&#enum_name #ty_generics) -> &#field_type = |value| match value { + #enum_name::#variant_name #match_field => #function_name, + _ => panic!("Selector that was created to match {} read after variant changed", stringify!(#variant_name)), + }; + let __map_mut_field: fn(&mut #enum_name #ty_generics) -> &mut #field_type = |value| match value { + #enum_name::#variant_name #match_field => #function_name, + _ => panic!("Selector that was created to match {} written after variant changed", stringify!(#variant_name)), + }; + // Each field within the variant gets its own reactive scope. Writing to one field will not notify the enum or + // other fields + let scope = self.into_selector().child( + #ordinal, + __map_field, + __map_mut_field, + ); + ::std::convert::Into::into(scope) + } +} + +fn function_name_from_field(index: usize, field: &syn::Field) -> Ident { + // Generate a function name from the field's identifier or index + field + .ident + .as_ref() + .map_or_else(|| format_ident!("field_{index}"), |name| name.clone()) +} + +fn mapped_type( + item: &Ident, + ty_generics: &syn::TypeGenerics, + field_type: &syn::Type, +) -> TokenStream2 { + // The zoomed in store type is a MappedMutSignal with function pointers to map the reference to the enum into a reference to the field + let write_type = quote! { dioxus_stores::macro_helpers::dioxus_signals::MappedMutSignal<#field_type, __Lens, fn(&#item #ty_generics) -> &#field_type, fn(&mut #item #ty_generics) -> &mut #field_type> }; + quote! { dioxus_stores::Store<#field_type, #write_type> } +} diff --git a/packages/stores-macro/src/extend.rs b/packages/stores-macro/src/extend.rs new file mode 100644 index 0000000000..622b7c0f6c --- /dev/null +++ b/packages/stores-macro/src/extend.rs @@ -0,0 +1,281 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Parse; +use syn::spanned::Spanned; +use syn::{ + parse_quote, Ident, ImplItem, ImplItemConst, ImplItemType, ItemImpl, PathArguments, Type, + WherePredicate, +}; + +pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result { + // Extract the type name the store is generic over + let store_type = &*input.self_ty; + let store = parse_store_type(store_type)?; + let store_path = &store.store_path; + let item = store.store_generic; + let lens_generic = store.store_lens; + let visibility = args + .visibility + .unwrap_or_else(|| syn::Visibility::Inherited); + if input.trait_.is_some() { + return Err(syn::Error::new_spanned( + input.trait_.unwrap().1, + "The `store` attribute can only be used on `impl Store { ... }` blocks, not trait implementations.", + )); + } + + let extension_name = match args.name { + Some(attr) => attr, + None => { + // Otherwise, generate a name based on the type name + let type_name = stringify_type(&item)?; + Ident::new(&format!("{}StoreImplExt", type_name), item.span()) + } + }; + + // Go through each method in the impl block and add extra bounds to lens as needed + let immutable_bounds: WherePredicate = parse_quote!(#lens_generic: dioxus_stores::macro_helpers::dioxus_signals::Readable + ::std::marker::Copy + 'static); + let mutable_bounds: WherePredicate = parse_quote!(#lens_generic: dioxus_stores::macro_helpers::dioxus_signals::Writable + ::std::marker::Copy + 'static); + for item in &mut input.items { + let ImplItem::Fn(func) = item else { + continue; // Only process function items + }; + // Only add bounds if the function has a self argument + let Some(receiver) = func.sig.inputs.iter().find_map(|arg| { + if let syn::FnArg::Receiver(receiver) = arg { + Some(receiver) + } else { + None + } + }) else { + continue; + }; + let extra_bounds = match (&receiver.reference, &receiver.mutability) { + // The function takes &self + (Some(_), None) => &immutable_bounds, + // The function takes &mut self + (Some(_), Some(_)) => &mutable_bounds, + _ => { + // If the function doesn't take &self or &mut self, we don't need to add any bounds + continue; + } + }; + func.sig + .generics + .make_where_clause() + .predicates + .push(extra_bounds.clone()); + } + + // Push a __Lens generic to the impl if it doesn't already exist + let contains_lens_generic = input.generics.params.iter().any(|param| { + if let syn::GenericParam::Type(ty) = param { + ty.ident == lens_generic + } else { + false + } + }); + if !contains_lens_generic { + input + .generics + .params + .push(parse_quote!(#lens_generic: ::std::marker::Copy + 'static)); + } + + // quote as the trait definition + let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?; + + // Reformat the type to be generic over the lens + input.self_ty = Box::new(parse_quote!(#store_path<#item, #lens_generic>)); + // Change the standalone impl block to a trait impl block + let (_, trait_generics, _) = input.generics.split_for_impl(); + input.trait_ = Some(( + None, + parse_quote!(#extension_name #trait_generics), + parse_quote!(for), + )); + + Ok(quote! { + #trait_definition + + #input + }) +} + +fn stringify_type(ty: &Type) -> syn::Result { + match ty { + Type::Array(type_array) => { + let elem = stringify_type(&type_array.elem)?; + Ok(format!("Array{elem}")) + } + Type::Slice(type_slice) => { + let elem = stringify_type(&type_slice.elem)?; + Ok(format!("Slice{elem}")) + } + Type::Paren(type_paren) => stringify_type(&type_paren.elem), + Type::Path(type_path) => { + let last_segment = type_path.path.segments.last().ok_or_else(|| { + syn::Error::new_spanned(type_path, "Type path must have at least one segment") + })?; + let ident = &last_segment.ident; + Ok(ident.to_string()) + } + _ => Err(syn::Error::new_spanned( + ty, + "Unsupported type in store implementation", + )), + } +} + +fn impl_to_trait_body( + trait_name: &Ident, + item: &ItemImpl, + visibility: &syn::Visibility, +) -> syn::Result { + let ItemImpl { + attrs, + defaultness, + unsafety, + items, + .. + } = item; + + let generics = &item.generics; + + let items = items + .iter() + .map(item_to_trait_definition) + .collect::>>()?; + + Ok(quote! { + #(#attrs)* + #visibility #defaultness #unsafety trait #trait_name #generics { + #(#items)* + } + }) +} + +fn item_to_trait_definition(item: &syn::ImplItem) -> syn::Result { + match item { + syn::ImplItem::Fn(func) => { + let sig = &func.sig; + + Ok(quote! { + #sig; + }) + } + syn::ImplItem::Const(impl_item_const) => { + let ImplItemConst { + attrs, + const_token, + ident, + generics, + colon_token, + ty, + semi_token, + .. + } = impl_item_const; + + Ok(quote! { + #(#attrs)* + #const_token #ident #generics #colon_token #ty #semi_token + }) + } + syn::ImplItem::Type(impl_item_type) => { + let ImplItemType { + attrs, + type_token, + ident, + generics, + eq_token, + ty, + semi_token, + .. + } = impl_item_type; + + Ok(quote! { + #(#attrs)* + #type_token #ident #generics #eq_token #ty #semi_token + }) + } + _ => Err(syn::Error::new_spanned(item, "Unsupported item type")), + } +} + +fn argument_as_type(arg: &syn::GenericArgument) -> Option { + if let syn::GenericArgument::Type(ty) = arg { + Some(ty.clone()) + } else { + None + } +} + +struct StorePath { + store_path: syn::Path, + store_generic: syn::Type, + store_lens: syn::Ident, +} + +fn parse_store_type(store_type: &Type) -> syn::Result { + if let Type::Path(type_path) = store_type { + if let Some(segment) = type_path.path.segments.last() { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(store_generics) = args.args.first().and_then(argument_as_type) { + let store_lens = args + .args + .iter() + .nth(1) + .and_then(argument_as_type) + .unwrap_or_else(|| parse_quote!(__Lens)); + let store_lens = parse_quote!(#store_lens); + let mut path_without_generics = type_path.path.clone(); + for segment in &mut path_without_generics.segments { + segment.arguments = PathArguments::None; + } + return Ok(StorePath { + store_path: path_without_generics, + store_generic: store_generics, + store_lens, + }); + } + } + } + } + Err(syn::Error::new_spanned( + store_type, + "The implementation must be in the form `impl Store {...}`", + )) +} + +/// The args the `#[store]` attribute macro accepts +pub(crate) struct ExtendArgs { + /// The name of the extension trait generated + name: Option, + /// The visibility of the extension trait + visibility: Option, +} + +impl Parse for ExtendArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // Try to parse visibility if it exists + let visibility = if input.peek(syn::Token![pub]) { + let vis: syn::Visibility = input.parse()?; + Some(vis) + } else { + None + }; + // Try to parse name = ident if it exists + let name = if input.peek(Ident) && input.peek2(syn::Token![=]) { + let ident: Ident = input.parse()?; + if ident != "name" { + return Err(syn::Error::new_spanned(ident, "Expected `name` argument")); + } + let _eq_token: syn::Token![=] = input.parse()?; + let ident: Ident = input.parse()?; + Some(ident) + } else { + None + }; + Ok(ExtendArgs { name, visibility }) + } +} diff --git a/packages/stores-macro/src/lib.rs b/packages/stores-macro/src/lib.rs new file mode 100644 index 0000000000..f000bd1539 --- /dev/null +++ b/packages/stores-macro/src/lib.rs @@ -0,0 +1,179 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput, ItemImpl}; + +use crate::extend::ExtendArgs; + +mod derive; +mod extend; + +/// # `derive(Store)` +/// +/// The `Store` macro is used to create an extension trait for stores that makes it possible to access the fields or variants +/// of an item as stores. +/// +/// ## Expansion +/// +/// The macro expands to two different items: +/// - An extension trait which is implemented for `Store` with methods to access fields and variants for your type. +/// - A transposed version of your type which contains the fields or variants as stores. +/// +/// ### Structs +/// +/// For structs, the store macro generates methods for each field that returns a store scoped to that field and a `transpose` method that returns a struct with all fields as stores: +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// #[derive(Store)] +/// struct TodoItem { +/// checked: bool, +/// contents: String, +/// } +/// +/// let store = use_store(|| TodoItem { +/// checked: false, +/// contents: "Learn about stores".to_string(), +/// }); +/// +/// // The store macro creates an extension trait with methods for each field +/// // that returns a store scoped to that field. +/// let checked: Store = store.checked(); +/// let contents: Store = store.contents(); +/// +/// // It also generates a `transpose` method returns a variant of your structure +/// // with stores wrapping each of your data types. This can be very useful when destructuring +/// // or matching your type +/// let TodoItemStoreTransposed { checked, contents } = store.transpose(); +/// let checked: bool = checked(); +/// let contents: String = contents(); +/// ``` +/// +/// +/// ### Enums +/// +/// For enums, the store macro generates methods for each variant that checks if the store is that variant. It also generates a `transpose` method that returns an enum with all fields as stores. +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// #[derive(Store, PartialEq, Clone, Debug)] +/// enum Enum { +/// Foo(String), +/// Bar { foo: i32, bar: String }, +/// } +/// +/// let store = use_store(|| Enum::Foo("Hello".to_string())); +/// // The store macro creates an extension trait with methods for each variant to check +/// // if the store is that variant. +/// let foo: bool = store.is_foo(); +/// let bar: bool = store.is_bar(); +/// +/// // If there is only one field in the variant, it also generates a method to try +/// // to downcast the store to that variant. +/// let foo: Option> = store.foo(); +/// if let Some(foo) = foo { +/// println!("Foo: {foo}"); +/// } +/// +/// // It also generates a `transpose` method that returns a variant of your enum where all +/// // the fields are stores. You can use this to match your enum +/// let transposed = store.transpose(); +/// use EnumStoreTransposed::*; +/// match transposed { +/// EnumStoreTransposed::Foo(foo) => println!("Foo: {foo}"), +/// EnumStoreTransposed::Bar { foo, bar } => { +/// let foo: i32 = foo(); +/// let bar: String = bar(); +/// println!("Bar: foo = {foo}, bar = {bar}"); +/// } +/// } +/// ``` +#[proc_macro_derive(Store)] +pub fn derive_store(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + let expanded = match derive::derive_store(input) { + Ok(tokens) => tokens, + Err(err) => { + // If there was an error, return it as a compile error + return err.to_compile_error().into(); + } + }; + + // Hand the output tokens back to the compiler + TokenStream::from(expanded) +} + +/// # `#[store]` +/// +/// The `store` attribute macro is used to create an extension trait for store implementations. The extension traits lets you add +/// methods to the store even though the type is not defined in your crate. +/// +/// ## Arguments +/// +/// - `pub`: Makes the generated extension trait public. If not provided, the trait will be private. +/// - `name = YourExtensionName`: The name of the extension trait. If not provided, it will be generated based on the type name. +/// +/// ## Bounds +/// +/// The generated extension trait will have bounds on the lens generic parameter to ensure it implements `Readable` or `Writable` as needed. +/// +/// - If a method accepts `&self`, the lens will require `Readable` which lets you read the value of the store. +/// - If a method accepts `&mut self`, the lens will require `Writable` which lets you change the value of the store. +/// +/// ## Example +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// #[derive(Store)] +/// struct TodoItem { +/// checked: bool, +/// contents: String, +/// } +/// +/// // You can use the store attribute macro to add methods to your stores +/// #[store] +/// impl Store { +/// // Since this method takes &mut self, the lens will require Writable automatically. It cannot be used +/// // with ReadStore +/// fn toggle_checked(&mut self) { +/// self.checked().toggle(); +/// } +/// +/// // Since this method takes &self, the lens will require Readable automatically +/// fn checked_contents(&self) -> Option { +/// self.checked().cloned().then(|| self.contents().to_string()) +/// } +/// } +/// +/// let mut store = use_store(|| TodoItem { +/// checked: false, +/// contents: "Learn about stores".to_string(), +/// }); +/// +/// // You can use the methods defined in the extension trait +/// store.toggle_checked(); +/// let contents: Option = store.checked_contents(); +/// ``` +#[proc_macro_attribute] +pub fn store(args: TokenStream, input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let args = parse_macro_input!(args as ExtendArgs); + let input = parse_macro_input!(input as ItemImpl); + + let expanded = match extend::extend_store(args, input) { + Ok(tokens) => tokens, + Err(err) => { + // If there was an error, return it as a compile error + return err.to_compile_error().into(); + } + }; + + // Hand the output tokens back to the compiler + TokenStream::from(expanded) +} diff --git a/packages/stores/Cargo.toml b/packages/stores/Cargo.toml new file mode 100644 index 0000000000..28a40f06be --- /dev/null +++ b/packages/stores/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dioxus-stores" +version = { workspace = true } +edition = "2021" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com/docs/0.5/guide/en/getting_started/fullstack.html" +keywords = ["dom", "ui", "gui", "react", "liveview"] +authors = ["Jonathan Kelley", "Evan Almloff"] +license = "MIT OR Apache-2.0" +description = "Server function macros for Dioxus" + +[dependencies] +dioxus-core = { workspace = true } +dioxus-signals = { workspace = true } +dioxus-stores-macro = { workspace = true, optional = true } + +[dev-dependencies] +dioxus = { workspace = true } + +[features] +default = ["macro"] +macro = ["dep:dioxus-stores-macro"] +large-path = [] + +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/stores/README.md b/packages/stores/README.md new file mode 100644 index 0000000000..71795ac942 --- /dev/null +++ b/packages/stores/README.md @@ -0,0 +1,64 @@ +# Dioxus Stores + +Stores are an extension to the Dioxus signals system for reactive nested data structures. Each store will lazily create signals for each field/member of the data structure as needed. + +By default stores act a lot like [`dioxus_signals::Signal`]s, but they provide more granular +subscriptions without requiring nested signals. You should derive [`Store`](dioxus_stores_macro::Store) on your data +structures to generate selectors that let you scope the store to a specific part of your data structure. + +```rust, no_run +use dioxus::prelude::*; +use dioxus_stores::*; + +fn main() { + dioxus::launch(app); +} + +// Deriving the store trait provides methods to scope the store to specific parts of your data structure. +// The `Store` macro generates a `count` and `children` method for `Store` +#[derive(Store, Default)] +struct CounterTree { + count: i32, + children: Vec, +} + +fn app() -> Element { + let value = use_store(Default::default); + + rsx! { + Tree { + value + } + } +} + +#[component] +fn Tree(value: Store) -> Element { + // Calling the generated `count` method returns a new store that can only + // read and write the count field + let mut count = value.count(); + let mut children = value.children(); + + rsx! { + button { + // Incrementing the count will only rerun parts of the app that have read the count field + onclick: move |_| count += 1, + "Increment" + } + button { + // Stores are aware of data structures like `Vec` and `Hashmap`. When we push an item to the vec + // it will only rerun the parts of the app that depend on the length of the vec + onclick: move |_| children.push(Default::default()), + "Push child" + } + ul { + // Iterating over the children gives us stores scoped to each child. + for value in children.iter() { + li { + Tree { value } + } + } + } + } +} +``` diff --git a/packages/stores/src/impls/btreemap.rs b/packages/stores/src/impls/btreemap.rs new file mode 100644 index 0000000000..63dda61902 --- /dev/null +++ b/packages/stores/src/impls/btreemap.rs @@ -0,0 +1,366 @@ +use std::{borrow::Borrow, collections::BTreeMap, hash::Hash, iter::FusedIterator}; + +use crate::{store::Store, ReadStore}; +use dioxus_signals::{ + AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, ReadableExt, UnsyncStorage, + Writable, WriteLock, WriteSignal, +}; + +impl> + 'static, K: 'static, V: 'static> + Store, Lens> +{ + /// Get the length of the BTreeMap. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// assert_eq!(store.len(), 0); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.len(), 1); + /// ``` + pub fn len(&self) -> usize { + self.selector().track_shallow(); + self.selector().peek().len() + } + + /// Check if the BTreeMap is empty. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// assert!(store.is_empty()); + /// store.insert(0, "value".to_string()); + /// assert!(!store.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_empty() + } + + /// Iterate over the current entries in the BTreeMap, returning a tuple of the key and a store for the value. This method + /// will track the store shallowly and only cause re-runs when items are added or removed from the map, not when existing + /// values are modified. + /// + /// # Example + /// + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// store.insert(0, "value1".to_string()); + /// store.insert(1, "value2".to_string()); + /// for (key, value_store) in store.iter() { + /// println!("{}: {}", key, value_store.read()); + /// } + /// ``` + pub fn iter( + &self, + ) -> impl ExactSizeIterator>)> + + DoubleEndedIterator + + FusedIterator + + '_ + where + K: Hash + Ord + Clone, + Lens: Clone, + { + self.selector().track_shallow(); + let keys: Vec<_> = self.selector().peek_unchecked().keys().cloned().collect(); + keys.into_iter().map(move |key| { + let value = self.clone().get(key.clone()).unwrap(); + (key, value) + }) + } + + /// Get an iterator over the values in the BTreeMap. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// store.insert(0, "value1".to_string()); + /// store.insert(1, "value2".to_string()); + /// for value_store in store.values() { + /// println!("{}", value_store.read()); + /// } + /// ``` + pub fn values( + &self, + ) -> impl ExactSizeIterator>> + + DoubleEndedIterator + + FusedIterator + + '_ + where + K: Hash + Ord + Clone, + Lens: Clone, + { + self.selector().track_shallow(); + let keys = self.selector().peek().keys().cloned().collect::>(); + keys.into_iter() + .map(move |key| self.clone().get(key).unwrap()) + } + + /// Insert a new key-value pair into the BTreeMap. This method will mark the store as shallowly dirty, causing + /// re-runs of any reactive scopes that depend on the shape of the map. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// assert!(store.get(0).is_none()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// ``` + pub fn insert(&mut self, key: K, value: V) + where + K: Ord, + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().insert(key, value); + } + + /// Remove a key-value pair from the BTreeMap. This method will mark the store as shallowly dirty, causing + /// re-runs of any reactive scopes that depend on the shape of the map or the value of the removed key. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// let removed_value = store.remove(&0); + /// assert_eq!(removed_value, Some("value".to_string())); + /// assert!(store.get(0).is_none()); + /// ``` + pub fn remove(&mut self, key: &Q) -> Option + where + Q: ?Sized + Ord + 'static, + K: Borrow + Ord, + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().remove(key) + } + + /// Clear the BTreeMap, removing all key-value pairs. This method will mark the store as shallowly dirty, + /// causing re-runs of any reactive scopes that depend on the shape of the map. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// store.insert(1, "value1".to_string()); + /// store.insert(2, "value2".to_string()); + /// assert_eq!(store.len(), 2); + /// store.clear(); + /// assert!(store.is_empty()); + /// ``` + pub fn clear(&mut self) + where + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().clear(); + } + + /// Retain only the key-value pairs that satisfy the given predicate. This method will mark the store as shallowly dirty, + /// causing re-runs of any reactive scopes that depend on the shape of the map or the values retained. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// store.insert(1, "value1".to_string()); + /// store.insert(2, "value2".to_string()); + /// store.retain(|key, value| *key == 1); + /// assert_eq!(store.len(), 1); + /// assert!(store.get(1).is_some()); + /// assert!(store.get(2).is_none()); + /// ``` + pub fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool) + where + Lens: Writable, + K: Ord, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().retain(|k, v| f(k, v)); + } + + /// Check if the BTreeMap contains a key. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// assert!(!store.contains_key(&0)); + /// store.insert(0, "value".to_string()); + /// assert!(store.contains_key(&0)); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + Q: ?Sized + Ord + 'static, + K: Borrow + Ord, + { + self.selector().track_shallow(); + self.selector().peek().contains_key(key) + } + + /// Get a store for the value associated with the given key. This method creates a new store scope + /// that tracks just changes to the value associated with the key. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::BTreeMap; + /// let mut store = use_store(|| BTreeMap::new()); + /// assert!(store.get(0).is_none()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// ``` + pub fn get(self, key: Q) -> Option>> + where + Q: Hash + Ord + 'static, + K: Borrow + Ord, + { + self.contains_key(&key).then(|| { + self.into_selector() + .hash_child_unmapped(key.borrow()) + .map_writer(move |writer| GetWrite { + index: key, + write: writer, + }) + .into() + }) + } +} + +/// A specific index in a `Readable` / `Writable` BTreeMap +#[derive(Clone, Copy)] +pub struct GetWrite { + index: Index, + write: Write, +} + +impl Readable for GetWrite +where + Write: Readable>, + Index: Ord + 'static, + K: Borrow + Ord + 'static, +{ + type Target = V; + + type Storage = Write::Storage; + + fn try_read_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_read_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value + .get(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } + + fn try_peek_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_peek_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value + .get(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } + + fn subscribers(&self) -> dioxus_core::Subscribers + where + Self::Target: 'static, + { + self.write.subscribers() + } +} + +impl Writable for GetWrite +where + Write: Writable>, + Index: Ord + 'static, + K: Borrow + Ord + 'static, +{ + type WriteMetadata = Write::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, BorrowMutError> + where + Self::Target: 'static, + { + self.write.try_write_unchecked().map(|value| { + WriteLock::map(value, |value: &mut Write::Target| { + value + .get_mut(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } +} + +impl ::std::convert::From>> + for Store> +where + Write::WriteMetadata: 'static, + Write: Writable, Storage = UnsyncStorage> + 'static, + Index: Ord + 'static, + K: Borrow + Ord + 'static, + V: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| WriteSignal::new(writer)) + .into() + } +} + +impl ::std::convert::From>> for ReadStore +where + Write: Readable, Storage = UnsyncStorage> + 'static, + Index: Ord + 'static, + K: Borrow + Ord + 'static, + V: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| ReadSignal::new(writer)) + .into() + } +} diff --git a/packages/stores/src/impls/deref.rs b/packages/stores/src/impls/deref.rs new file mode 100644 index 0000000000..96ba981c3c --- /dev/null +++ b/packages/stores/src/impls/deref.rs @@ -0,0 +1,28 @@ +use std::ops::DerefMut; + +use crate::{store::Store, MappedStore}; +use dioxus_signals::Readable; + +impl Store +where + Lens: Readable + 'static, + T: DerefMut + 'static, +{ + /// Returns a store that dereferences the original value. The dereferenced store shares the same + /// subscriptions and tracking as the original store, but allows you to access the methods of the underlying type. + /// + /// # Example + /// + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Box::new(vec![1, 2, 3])); + /// let deref_store = store.deref(); + /// // The dereferenced store can access the store methods of the underlying type. + /// assert_eq!(deref_store.len(), 3); + /// ``` + pub fn deref(self) -> MappedStore { + let map: fn(&T) -> &T::Target = |value| value.deref(); + let map_mut: fn(&mut T) -> &mut T::Target = |value| value.deref_mut(); + self.into_selector().map(map, map_mut).into() + } +} diff --git a/packages/stores/src/impls/hashmap.rs b/packages/stores/src/impls/hashmap.rs new file mode 100644 index 0000000000..8dfe07835f --- /dev/null +++ b/packages/stores/src/impls/hashmap.rs @@ -0,0 +1,380 @@ +use std::{ + borrow::Borrow, + collections::HashMap, + hash::{BuildHasher, Hash}, + iter::FusedIterator, +}; + +use crate::{store::Store, ReadStore}; +use dioxus_signals::{ + AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, ReadableExt, UnsyncStorage, + Writable, WriteLock, WriteSignal, +}; + +impl> + 'static, K: 'static, V: 'static, St: 'static> + Store, Lens> +{ + /// Get the length of the HashMap. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// assert_eq!(store.len(), 0); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.len(), 1); + /// ``` + pub fn len(&self) -> usize { + self.selector().track_shallow(); + self.selector().peek().len() + } + + /// Check if the HashMap is empty. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// assert!(store.is_empty()); + /// store.insert(0, "value".to_string()); + /// assert!(!store.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_empty() + } + + /// Iterate over the current entries in the HashMap, returning a tuple of the key and a store for the value. This method + /// will track the store shallowly and only cause re-runs when items are added or removed from the map, not when existing + /// values are modified. + /// + /// # Example + /// + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// store.insert(0, "value1".to_string()); + /// store.insert(1, "value2".to_string()); + /// for (key, value_store) in store.iter() { + /// println!("{}: {}", key, value_store.read()); + /// } + /// ``` + pub fn iter( + &self, + ) -> impl ExactSizeIterator>)> + + DoubleEndedIterator + + FusedIterator + + '_ + where + K: Eq + Hash + Clone, + St: BuildHasher, + Lens: Clone, + { + self.selector().track_shallow(); + let keys: Vec<_> = self.selector().peek_unchecked().keys().cloned().collect(); + keys.into_iter().map(move |key| { + let value = self.clone().get(key.clone()).unwrap(); + (key, value) + }) + } + + /// Get an iterator over the values in the HashMap. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// store.insert(0, "value1".to_string()); + /// store.insert(1, "value2".to_string()); + /// for value_store in store.values() { + /// println!("{}", value_store.read()); + /// } + /// ``` + pub fn values( + &self, + ) -> impl ExactSizeIterator>> + + DoubleEndedIterator + + FusedIterator + + '_ + where + K: Eq + Hash + Clone, + St: BuildHasher, + Lens: Clone, + { + self.selector().track_shallow(); + let keys = self.selector().peek().keys().cloned().collect::>(); + keys.into_iter() + .map(move |key| self.clone().get(key).unwrap()) + } + + /// Insert a new key-value pair into the HashMap. This method will mark the store as shallowly dirty, causing + /// re-runs of any reactive scopes that depend on the shape of the map. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// assert!(store.get(0).is_none()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// ``` + pub fn insert(&mut self, key: K, value: V) + where + K: Eq + Hash, + St: BuildHasher, + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().insert(key, value); + } + + /// Remove a key-value pair from the HashMap. This method will mark the store as shallowly dirty, causing + /// re-runs of any reactive scopes that depend on the shape of the map or the value of the removed key. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// let removed_value = store.remove(&0); + /// assert_eq!(removed_value, Some("value".to_string())); + /// assert!(store.get(0).is_none()); + /// ``` + pub fn remove(&mut self, key: &Q) -> Option + where + Q: ?Sized + Hash + Eq + 'static, + K: Borrow + Eq + Hash, + St: BuildHasher, + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().remove(key) + } + + /// Clear the HashMap, removing all key-value pairs. This method will mark the store as shallowly dirty, + /// causing re-runs of any reactive scopes that depend on the shape of the map. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// store.insert(1, "value1".to_string()); + /// store.insert(2, "value2".to_string()); + /// assert_eq!(store.len(), 2); + /// store.clear(); + /// assert!(store.is_empty()); + /// ``` + pub fn clear(&mut self) + where + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().clear(); + } + + /// Retain only the key-value pairs that satisfy the given predicate. This method will mark the store as shallowly dirty, + /// causing re-runs of any reactive scopes that depend on the shape of the map or the values retained. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// store.insert(1, "value1".to_string()); + /// store.insert(2, "value2".to_string()); + /// store.retain(|key, value| *key == 1); + /// assert_eq!(store.len(), 1); + /// assert!(store.get(1).is_some()); + /// assert!(store.get(2).is_none()); + /// ``` + pub fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool) + where + Lens: Writable, + { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().retain(|k, v| f(k, v)); + } + + /// Check if the HashMap contains a key. This method will track the store shallowly and only cause + /// re-runs when items are added or removed from the map, not when existing values are modified. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// assert!(!store.contains_key(&0)); + /// store.insert(0, "value".to_string()); + /// assert!(store.contains_key(&0)); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + Q: ?Sized + Hash + Eq + 'static, + K: Borrow + Eq + Hash, + St: BuildHasher, + { + self.selector().track_shallow(); + self.selector().peek().contains_key(key) + } + + /// Get a store for the value associated with the given key. This method creates a new store scope + /// that tracks just changes to the value associated with the key. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// use dioxus::prelude::*; + /// use std::collections::HashMap; + /// let mut store = use_store(|| HashMap::new()); + /// assert!(store.get(0).is_none()); + /// store.insert(0, "value".to_string()); + /// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string()); + /// ``` + pub fn get(self, key: Q) -> Option>> + where + Q: Hash + Eq + 'static, + K: Borrow + Eq + Hash, + St: BuildHasher, + { + self.contains_key(&key).then(|| { + self.into_selector() + .hash_child_unmapped(key.borrow()) + .map_writer(move |writer| GetWrite { + index: key, + write: writer, + }) + .into() + }) + } +} + +/// A specific index in a `Readable` / `Writable` hashmap +#[derive(Clone, Copy)] +pub struct GetWrite { + index: Index, + write: Write, +} + +impl Readable for GetWrite +where + Write: Readable>, + Index: Hash + Eq + 'static, + K: Borrow + Eq + Hash + 'static, + St: BuildHasher + 'static, +{ + type Target = V; + + type Storage = Write::Storage; + + fn try_read_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_read_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value + .get(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } + + fn try_peek_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_peek_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value + .get(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } + + fn subscribers(&self) -> dioxus_core::Subscribers + where + Self::Target: 'static, + { + self.write.subscribers() + } +} + +impl Writable for GetWrite +where + Write: Writable>, + Index: Hash + Eq + 'static, + K: Borrow + Eq + Hash + 'static, + St: BuildHasher + 'static, +{ + type WriteMetadata = Write::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, BorrowMutError> + where + Self::Target: 'static, + { + self.write.try_write_unchecked().map(|value| { + WriteLock::map(value, |value: &mut Write::Target| { + value + .get_mut(&self.index) + .expect("Tried to access a key that does not exist") + }) + }) + } +} + +impl ::std::convert::From>> + for Store> +where + Write::WriteMetadata: 'static, + Write: Writable, Storage = UnsyncStorage> + 'static, + Index: Hash + Eq + 'static, + K: Borrow + Eq + Hash + 'static, + St: BuildHasher + 'static, + V: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| WriteSignal::new(writer)) + .into() + } +} + +impl ::std::convert::From>> for ReadStore +where + Write: Readable, Storage = UnsyncStorage> + 'static, + Index: Hash + Eq + 'static, + K: Borrow + Eq + Hash + 'static, + St: BuildHasher + 'static, + V: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| ReadSignal::new(writer)) + .into() + } +} diff --git a/packages/stores/src/impls/index.rs b/packages/stores/src/impls/index.rs new file mode 100644 index 0000000000..dca09106f0 --- /dev/null +++ b/packages/stores/src/impls/index.rs @@ -0,0 +1,136 @@ +use std::{ + hash::Hash, + ops::{self, Index, IndexMut}, +}; + +use crate::{store::Store, ReadStore}; +use dioxus_signals::{ + AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, UnsyncStorage, Writable, + WriteLock, WriteSignal, +}; + +impl Store { + /// Index into the store, returning a store that allows access to the item at the given index. The + /// new store will only update when the item at the index changes. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| vec![1, 2, 3]); + /// let indexed_store = store.index(1); + /// // The indexed store can access the store methods of the indexed store. + /// assert_eq!(indexed_store(), 2); + /// ``` + pub fn index(self, index: Idx) -> Store> + where + T: IndexMut + 'static, + Idx: Hash + 'static, + Lens: Readable + 'static, + { + self.into_selector() + .hash_child_unmapped(&index) + .map_writer(move |write| IndexWrite { index, write }) + .into() + } +} + +/// A specific index in a `Readable` / `Writable` type +#[derive(Clone, Copy)] +pub struct IndexWrite { + index: Index, + write: Write, +} + +impl Readable for IndexWrite +where + Write: Readable, + Write::Target: ops::Index + 'static, + Index: Clone, +{ + type Target = >::Output; + + type Storage = Write::Storage; + + fn try_read_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_read_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value.index(self.index.clone()) + }) + }) + } + + fn try_peek_unchecked(&self) -> Result, BorrowError> + where + Self::Target: 'static, + { + self.write.try_peek_unchecked().map(|value| { + Self::Storage::map(value, |value: &Write::Target| { + value.index(self.index.clone()) + }) + }) + } + + fn subscribers(&self) -> dioxus_core::Subscribers + where + Self::Target: 'static, + { + self.write.subscribers() + } +} + +impl Writable for IndexWrite +where + Write: Writable, + Write::Target: ops::IndexMut + 'static, + Index: Clone, +{ + type WriteMetadata = Write::WriteMetadata; + + fn try_write_unchecked( + &self, + ) -> Result, BorrowMutError> + where + Self::Target: 'static, + { + self.write.try_write_unchecked().map(|value| { + WriteLock::map(value, |value: &mut Write::Target| { + value.index_mut(self.index.clone()) + }) + }) + } +} + +impl ::std::convert::From>> + for Store> +where + Write: Writable + 'static, + Write::WriteMetadata: 'static, + Write::Target: ops::IndexMut + 'static, + Idx: Clone + 'static, + T: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| WriteSignal::new(writer)) + .into() + } +} + +impl ::std::convert::From>> for ReadStore +where + Write: Readable + 'static, + Write::Target: ops::Index + 'static, + Idx: Clone + 'static, + T: 'static, +{ + fn from(value: Store>) -> Self { + value + .into_selector() + .map_writer(|writer| ReadSignal::new(writer)) + .into() + } +} diff --git a/packages/stores/src/impls/mod.rs b/packages/stores/src/impls/mod.rs new file mode 100644 index 0000000000..16139dd8e1 --- /dev/null +++ b/packages/stores/src/impls/mod.rs @@ -0,0 +1,8 @@ +mod btreemap; +mod deref; +mod hashmap; +mod index; +mod option; +mod result; +mod slice; +mod vec; diff --git a/packages/stores/src/impls/option.rs b/packages/stores/src/impls/option.rs new file mode 100644 index 0000000000..1b6fe4355d --- /dev/null +++ b/packages/stores/src/impls/option.rs @@ -0,0 +1,230 @@ +use std::ops::DerefMut; + +use crate::{store::Store, MappedStore}; +use dioxus_signals::{Readable, ReadableExt}; + +impl> + 'static, T: 'static> Store, Lens> { + /// Checks if the `Option` is `Some`. This will only track the shallow state of the `Option`. It will + /// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// assert!(store.is_some()); + /// ``` + pub fn is_some(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_some() + } + + /// Returns true if the option is Some and the closure returns true. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Some. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// assert!(store.is_some_and(|v| *v == 42)); + /// ``` + pub fn is_some_and(&self, f: impl FnOnce(&T) -> bool) -> bool { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Some(v) = &*value { + self.selector().track(); + f(v) + } else { + false + } + } + + /// Checks if the `Option` is `None`. This will only track the shallow state of the `Option`. It will + /// only cause a re-run if the `Option` could change from `Some` to `None` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| None::); + /// assert!(store.is_none()); + /// ``` + pub fn is_none(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_none() + } + + /// Returns true if the option is None or the closure returns true. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Some. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// assert!(store.is_none_or(|v| *v == 42)); + /// ``` + pub fn is_none_or(&self, f: impl FnOnce(&T) -> bool) -> bool { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Some(v) = &*value { + self.selector().track(); + f(v) + } else { + true + } + } + + /// Transpose the `Store>` into a `Option>`. This will only track the shallow state of the `Option`. It will + /// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// let transposed = store.transpose(); + /// match transposed { + /// Some(inner_store) => assert_eq!(inner_store(), 42), + /// None => panic!("Expected Some"), + /// } + /// ``` + pub fn transpose(self) -> Option> { + self.is_some().then(move || { + let map: fn(&Option) -> &T = |value| { + value.as_ref().unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + }; + let map_mut: fn(&mut Option) -> &mut T = |value| { + value.as_mut().unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + }; + self.into_selector().child(0, map, map_mut).into() + }) + } + + /// Unwraps the `Option` and returns a `Store`. This will only track the shallow state of the `Option`. It will + /// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// let unwrapped = store.unwrap(); + /// assert_eq!(unwrapped(), 42); + /// ``` + pub fn unwrap(self) -> MappedStore { + self.transpose().unwrap() + } + + /// Expects the `Option` to be `Some` and returns a `Store`. If the value is `None`, this will panic with `msg`. This will + /// only track the shallow state of the `Option`. It will only cause a re-run if the `Option` could change from `None` + /// to `Some` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// let unwrapped = store.expect("the answer to life the universe and everything"); + /// assert_eq!(unwrapped(), 42); + /// ``` + pub fn expect(self, msg: &str) -> MappedStore { + self.transpose().expect(msg) + } + + /// Returns a slice of the contained value, or an empty slice. This will not subscribe to any part of the store. + /// + /// # Example + /// ```rust, no_run + /// use dioxus::prelude::*; + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// let slice = store.as_slice(); + /// assert_eq!(&*slice.read(), [42]); + /// ``` + pub fn as_slice(self) -> MappedStore<[T], Lens> { + let map: fn(&Option) -> &[T] = |value| value.as_slice(); + let map_mut: fn(&mut Option) -> &mut [T] = |value| value.as_mut_slice(); + self.into_selector().map(map, map_mut).into() + } + + /// Transpose the store then coerce the contents of the Option with deref. This will only track the shallow state of the `Option`. It will + /// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(Box::new(42))); + /// let derefed = store.as_deref().unwrap(); + /// assert_eq!(derefed(), 42); + /// ``` + pub fn as_deref(self) -> Option> + where + T: DerefMut, + { + self.is_some().then(move || { + let map: fn(&Option) -> &T::Target = |value| { + value + .as_ref() + .unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + .deref() + }; + let map_mut: fn(&mut Option) -> &mut T::Target = |value| { + value + .as_mut() + .unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + .deref_mut() + }; + self.into_selector().child(0, map, map_mut).into() + }) + } + + /// Transpose the store then filter the contents of the Option with a closure. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Some. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)); + /// let option = store.filter(|&v| v > 40); + /// let value = option.unwrap(); + /// assert_eq!(value(), 42); + /// ``` + pub fn filter(self, f: impl FnOnce(&T) -> bool) -> Option> { + self.is_some_and(f).then(move || { + let map: fn(&Option) -> &T = |value| { + value.as_ref().unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + }; + let map_mut: fn(&mut Option) -> &mut T = |value| { + value.as_mut().unwrap_or_else(|| { + panic!("Tried to access `Some` on an Option value"); + }) + }; + self.into_selector().child(0, map, map_mut).into() + }) + } + + /// Call the function with a reference to the inner value if it is Some. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Some. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Some(42)).inspect(|v| println!("{v}")); + /// ``` + pub fn inspect(self, f: impl FnOnce(&T)) -> Self { + { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Some(v) = &*value { + self.selector().track(); + f(v); + } + } + self + } +} diff --git a/packages/stores/src/impls/result.rs b/packages/stores/src/impls/result.rs new file mode 100644 index 0000000000..312a590e65 --- /dev/null +++ b/packages/stores/src/impls/result.rs @@ -0,0 +1,336 @@ +use std::fmt::Debug; + +use crate::{store::Store, MappedStore}; +use dioxus_signals::{Readable, ReadableExt, Writable}; + +impl Store, Lens> +where + Lens: Readable> + 'static, + T: 'static, + E: 'static, +{ + /// Checks if the `Result` is `Ok`. This will only track the shallow state of the `Result`. It will + /// only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// assert!(store.is_ok()); + /// ``` + pub fn is_ok(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_ok() + } + + /// Returns true if the result is Ok and the closure returns true. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Ok. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// assert!(store.is_ok_and(|v| *v == 42)); + /// ``` + pub fn is_ok_and(&self, f: impl FnOnce(&T) -> bool) -> bool { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Ok(v) = &*value { + self.selector().track(); + f(v) + } else { + false + } + } + + /// Checks if the `Result` is `Err`. This will only track the shallow state of the `Result`. It will + /// only cause a re-run if the `Result` could change from `Ok` to `Err` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)); + /// assert!(store.is_err()); + /// ``` + pub fn is_err(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_err() + } + + /// Returns true if the result is Err and the closure returns true. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Err. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)); + /// assert!(store.is_err_and(|v| *v == 42)); + /// ``` + pub fn is_err_and(&self, f: impl FnOnce(&E) -> bool) -> bool { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Err(e) = &*value { + self.selector().track(); + f(e) + } else { + false + } + } + + /// Converts `Store>` into `Option>`, discarding the error if present. This will + /// only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could + /// change from `Err` to `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// match store.ok() { + /// Some(ok_store) => assert_eq!(ok_store(), 42), + /// None => panic!("Expected Ok"), + /// } + /// ``` + pub fn ok(self) -> Option> { + let map: fn(&Result) -> &T = |value| { + value.as_ref().unwrap_or_else(|_| { + panic!("Tried to access `ok` on an Err value"); + }) + }; + let map_mut: fn(&mut Result) -> &mut T = |value| { + value.as_mut().unwrap_or_else(|_| { + panic!("Tried to access `ok` on an Err value"); + }) + }; + self.is_ok() + .then(|| self.into_selector().child(0, map, map_mut).into()) + } + + /// Converts `Store>` into `Option>`, discarding the success if present. This will + /// only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could + /// change from `Ok` to `Err` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)); + /// match store.err() { + /// Some(err_store) => assert_eq!(err_store(), 42), + /// None => panic!("Expected Err"), + /// } + /// ``` + pub fn err(self) -> Option> + where + Lens: Writable> + 'static, + { + self.is_err().then(|| { + let map: fn(&Result) -> &E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + let map_mut: fn(&mut Result) -> &mut E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + self.into_selector().child(1, map, map_mut).into() + }) + } + + /// Transposes the `Store>` into a `Result, Store>`. This will only track the + /// shallow state of the `Result`. It will only cause a re-run if the `Result` could change from `Err` to + /// `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// match store.transpose() { + /// Ok(ok_store) => assert_eq!(ok_store(), 42), + /// Err(err_store) => assert_eq!(err_store(), ()), + /// } + /// ``` + #[allow(clippy::result_large_err)] + pub fn transpose(self) -> Result, MappedStore> + where + Lens: Writable> + 'static, + { + if self.is_ok() { + let map: fn(&Result) -> &T = |value| match value { + Ok(t) => t, + Err(_) => panic!("Tried to access `ok` on an Err value"), + }; + let map_mut: fn(&mut Result) -> &mut T = |value| match value { + Ok(t) => t, + Err(_) => panic!("Tried to access `ok` on an Err value"), + }; + Ok(self.into_selector().child(0, map, map_mut).into()) + } else { + let map: fn(&Result) -> &E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + let map_mut: fn(&mut Result) -> &mut E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + Err(self.into_selector().child(1, map, map_mut).into()) + } + } + + /// Unwraps the `Result` and returns a `Store`. This will only track the shallow state of the `Result`. + /// It will only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// let unwrapped = store.unwrap(); + /// assert_eq!(unwrapped(), 42); + /// ``` + pub fn unwrap(self) -> MappedStore + where + Lens: Writable> + 'static, + E: Debug, + { + self.transpose().unwrap() + } + + /// Expects the `Result` to be `Ok` and returns a `Store`. If the value is `Err`, this will panic with `msg`. + /// This will only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could + /// change from `Err` to `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)); + /// let unwrapped = store.expect("Expected Ok"); + /// assert_eq!(unwrapped(), 42); + /// ``` + pub fn expect(self, msg: &str) -> MappedStore + where + Lens: Writable> + 'static, + E: Debug, + { + self.transpose().expect(msg) + } + + /// Unwraps the error variant of the `Result`. This will only track the shallow state of the `Result`. + /// It will only cause a re-run if the `Result` could change from `Ok` to `Err` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)); + /// let unwrapped_err = store.unwrap_err(); + /// assert_eq!(unwrapped_err(), 42); + /// ``` + pub fn unwrap_err(self) -> MappedStore + where + Lens: Writable> + 'static, + T: Debug, + { + self.transpose().unwrap_err() + } + + /// Expects the `Result` to be `Err` and returns a `Store`. If the value is `Ok`, this will panic with `msg`. + /// This will only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could + /// change from `Ok` to `Err` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)); + /// let unwrapped_err = store.expect_err("Expected Err"); + /// assert_eq!(unwrapped_err(), 42); + /// ``` + pub fn expect_err(self, msg: &str) -> MappedStore + where + Lens: Writable> + 'static, + T: Debug, + { + self.transpose().expect_err(msg) + } + + /// Call the function with a reference to the inner value if it is Ok. This will always track the shallow + /// state of the and will track the inner state of the enum if the enum is Ok. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::(42)).inspect(|v| println!("{v}")); + /// ``` + pub fn inspect(self, f: impl FnOnce(&T)) -> Self + where + Lens: Writable> + 'static, + { + { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Ok(value) = &*value { + self.selector().track(); + f(value); + } + } + self + } + + /// Call the function with a mutable reference to the inner value if it is Err. This will always track the shallow + /// state of the `Result` and will track the inner state of the enum if the enum is Err. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Err::<(), u32>(42)).inspect_err(|v| println!("{v}")); + /// ``` + pub fn inspect_err(self, f: impl FnOnce(&E)) -> Self + where + Lens: Writable> + 'static, + { + { + self.selector().track_shallow(); + let value = self.selector().peek(); + if let Err(value) = &*value { + self.selector().track(); + f(value); + } + } + self + } + + /// Transpose the store then coerce the contents of the Result with deref. This will only track the shallow state of the `Result`. It will + /// only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| Ok::, ()>(Box::new(42))); + /// let derefed = store.as_deref().unwrap(); + /// assert_eq!(derefed(), 42); + /// ``` + pub fn as_deref(self) -> Result, MappedStore> + where + Lens: Writable> + 'static, + T: std::ops::DerefMut, + { + if self.is_ok() { + let map: fn(&Result) -> &T::Target = |value| match value { + Ok(t) => t.deref(), + Err(_) => panic!("Tried to access `ok` on an Err value"), + }; + let map_mut: fn(&mut Result) -> &mut T::Target = |value| match value { + Ok(t) => t.deref_mut(), + Err(_) => panic!("Tried to access `ok` on an Err value"), + }; + Ok(self.into_selector().child(0, map, map_mut).into()) + } else { + let map: fn(&Result) -> &E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + let map_mut: fn(&mut Result) -> &mut E = |value| match value { + Ok(_) => panic!("Tried to access `err` on an Ok value"), + Err(e) => e, + }; + Err(self.into_selector().child(1, map, map_mut).into()) + } + } +} diff --git a/packages/stores/src/impls/slice.rs b/packages/stores/src/impls/slice.rs new file mode 100644 index 0000000000..9dce2580d7 --- /dev/null +++ b/packages/stores/src/impls/slice.rs @@ -0,0 +1,60 @@ +use std::iter::FusedIterator; + +use crate::{impls::index::IndexWrite, store::Store}; +use dioxus_signals::{Readable, ReadableExt}; + +impl Store, Lens> +where + Lens: Readable> + 'static, + I: 'static, +{ + /// Returns the length of the slice. This will only track the shallow state of the slice. + /// It will only cause a re-run if the length of the slice could change. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| vec![1, 2, 3]); + /// assert_eq!(store.len(), 3); + /// ``` + pub fn len(&self) -> usize { + self.selector().track_shallow(); + self.selector().peek().len() + } + + /// Checks if the slice is empty. This will only track the shallow state of the slice. + /// It will only cause a re-run if the length of the slice could change. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| vec![1, 2, 3]); + /// assert!(!store.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.selector().track_shallow(); + self.selector().peek().is_empty() + } + + /// Returns an iterator over the items in the slice. This will only track the shallow state of the slice. + /// It will only cause a re-run if the length of the slice could change. + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let store = use_store(|| vec![1, 2, 3]); + /// for item in store.iter() { + /// println!("{}", item); + /// } + /// ``` + pub fn iter( + &self, + ) -> impl ExactSizeIterator>> + + DoubleEndedIterator + + FusedIterator + + '_ + where + Lens: Clone, + { + (0..self.len()).map(move |i| self.clone().index(i)) + } +} diff --git a/packages/stores/src/impls/vec.rs b/packages/stores/src/impls/vec.rs new file mode 100644 index 0000000000..ad3049e3eb --- /dev/null +++ b/packages/stores/src/impls/vec.rs @@ -0,0 +1,88 @@ +use crate::store::Store; +use dioxus_signals::Writable; + +impl> + 'static, T: 'static> Store, Lens> { + /// Pushes an item to the end of the vector. This will only mark the length of the vector as dirty. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let mut store = use_store(|| vec![1, 2, 3]); + /// store.push(4); + /// ``` + pub fn push(&mut self, value: T) { + self.selector().mark_dirty_shallow(); + self.selector().write_untracked().push(value); + } + + /// Removes an item from the vector at the specified index and returns it. This will mark items after + /// the index and the length of the vector as dirty. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let mut store = use_store(|| vec![1, 2, 3]); + /// let removed = store.remove(1); + /// assert_eq!(removed, 2); + /// ``` + pub fn remove(&mut self, index: usize) -> T { + self.selector().mark_dirty_shallow(); + self.selector().mark_dirty_at_and_after_index(index); + self.selector().write_untracked().remove(index) + } + + /// Inserts an item at the specified index in the vector. This will mark items at and after the index + /// and the length of the vector as dirty. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let mut store = use_store(|| vec![1, 2, 3]); + /// store.insert(1, 4); + /// ``` + pub fn insert(&mut self, index: usize, value: T) { + self.selector().mark_dirty_shallow(); + self.selector().mark_dirty_at_and_after_index(index); + self.selector().write_untracked().insert(index, value); + } + + /// Clears the vector, marking it as dirty. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let mut store = use_store(|| vec![1, 2, 3]); + /// store.clear(); + /// ``` + pub fn clear(&mut self) { + self.selector().mark_dirty(); + self.selector().write_untracked().clear(); + } + + /// Retains only the elements specified by the predicate. This will only mark the length of the vector + /// and items after the first removed item as dirty. + /// + /// # Example + /// ```rust, no_run + /// use dioxus_stores::*; + /// let mut store = use_store(|| vec![1, 2, 3, 4, 5]); + /// store.retain(|&x| x % 2 == 0); + /// assert_eq!(store.len(), 2); + /// ``` + pub fn retain(&mut self, mut f: impl FnMut(&T) -> bool) { + let mut index = 0; + let mut first_removed_index = None; + self.selector().write_untracked().retain(|item| { + let keep = f(item); + if !keep { + first_removed_index = first_removed_index.or(Some(index)); + } + index += 1; + keep + }); + if let Some(index) = first_removed_index { + self.selector().mark_dirty_shallow(); + self.selector().mark_dirty_at_and_after_index(index); + } + } +} diff --git a/packages/stores/src/lib.rs b/packages/stores/src/lib.rs new file mode 100644 index 0000000000..3d2b8dbf48 --- /dev/null +++ b/packages/stores/src/lib.rs @@ -0,0 +1,21 @@ +#![doc = include_str!("../README.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] +#![warn(missing_docs)] +#![allow(clippy::type_complexity)] + +mod impls; +mod store; +mod subscriptions; +pub use store::*; +pub mod scope; + +#[cfg(feature = "macro")] +pub use dioxus_stores_macro::{store, Store}; + +/// Re-exports for the store derive macro +#[doc(hidden)] +pub mod macro_helpers { + pub use dioxus_core; + pub use dioxus_signals; +} diff --git a/packages/stores/src/scope.rs b/packages/stores/src/scope.rs new file mode 100644 index 0000000000..383ebba94c --- /dev/null +++ b/packages/stores/src/scope.rs @@ -0,0 +1,215 @@ +//! This module contains the `SelectorScope` type with raw access to the underlying store system. Most applications should +//! use the [`Store`](dioxus_stores_macro::Store) macro to derive stores for their data structures, which provides a more ergonomic API. + +use std::{fmt::Debug, hash::Hash}; + +use crate::subscriptions::{PathKey, StoreSubscriptions, TinyVec}; +use dioxus_core::Subscribers; +use dioxus_signals::{ + BorrowError, BorrowMutError, MappedMutSignal, Readable, ReadableRef, Writable, WritableExt, + WritableRef, +}; + +/// SelectorScope is the primitive that backs the store system. +/// +/// Under the hood stores consist of two different parts: +/// - The underlying lock that contains the data in the store. +/// - A tree of subscriptions used to make the store reactive. +/// +/// The `SelectorScope` contains a view into the lock (`Lens`) and a path into the subscription tree. When +/// the selector is read to, it will track the current path in the subscription tree. When it it written to +/// it marks itself and all its children as dirty. +/// +/// When you derive the [`Store`](dioxus_stores_macro::Store) macro on your data structure, +/// it generates methods that map the lock to a new type and scope the path to a specific part of the subscription structure. +/// For example, a `Counter` store might look like this: +/// +/// ```rust, ignore +/// #[derive(Store)] +/// struct Counter { +/// count: i32, +/// } +/// +/// impl CounterStoreExt for Store { +/// fn count( +/// self, +/// ) -> dioxus_stores::Store< +/// i32, +/// dioxus_stores::macro_helpers::dioxus_signals::MappedMutSignal, +/// > { +/// let __map_field: fn(&CounterTree) -> &i32 = |value| &value.count; +/// let __map_mut_field: fn(&mut CounterTree) -> &mut i32 = |value| &mut value.count; +/// let scope = self.selector().scope(0u32, __map_field, __map_mut_field); +/// dioxus_stores::Store::new(scope) +/// } +/// } +/// ``` +/// +/// The `count` method maps the lock to the `i32` type and creates a child `0` path in the subscription tree. Only writes +/// to that `0` path or its parents will trigger a re-render of the components that read the `count` field. +#[derive(PartialEq)] +pub struct SelectorScope { + path: TinyVec, + store: StoreSubscriptions, + write: Lens, +} + +impl Debug for SelectorScope { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("SelectorScope") + .field("path", &self.path) + .finish() + } +} + +impl Clone for SelectorScope +where + Lens: Clone, +{ + fn clone(&self) -> Self { + Self { + path: self.path, + store: self.store, + write: self.write.clone(), + } + } +} + +impl Copy for SelectorScope where Lens: Copy {} + +impl SelectorScope { + pub(crate) fn new(path: TinyVec, store: StoreSubscriptions, write: Lens) -> Self { + Self { path, store, write } + } + + /// Create a child selector scope for a hash key. The scope will only be marked as dirty when a + /// write occurs to that key or its parents. + /// + /// Note the hash is lossy, so there may rarely be collisions. If a collision does occur, it may + /// cause reruns in a part of the app that has not changed. As long as derived data is pure, + /// this should not cause issues. + pub fn hash_child( + self, + index: &impl Hash, + map: F, + map_mut: FMut, + ) -> SelectorScope> + where + F: Fn(&T) -> &U, + FMut: Fn(&mut T) -> &mut U, + { + let hash = self.store.hash(index); + self.child(hash, map, map_mut) + } + + /// Create a child selector scope for a specific index. The scope will only be marked as dirty when a + /// write occurs to that index or its parents. + pub fn child( + self, + index: PathKey, + map: F, + map_mut: FMut, + ) -> SelectorScope> + where + F: Fn(&T) -> &U, + FMut: Fn(&mut T) -> &mut U, + { + self.child_unmapped(index).map(map, map_mut) + } + + /// Create a hashed child selector scope for a specific index without mapping the writer. The scope will only + /// be marked as dirty when a write occurs to that index or its parents. + pub fn hash_child_unmapped(self, index: &impl Hash) -> SelectorScope { + let hash = self.store.hash(index); + self.child_unmapped(hash) + } + + /// Create a child selector scope for a specific index without mapping the writer. The scope will only + /// be marked as dirty when a write occurs to that index or its parents. + pub fn child_unmapped(mut self, index: PathKey) -> SelectorScope { + self.path.push(index); + self + } + + /// Map the view into the writable data without creating a child selector scope + pub fn map( + self, + map: F, + map_mut: FMut, + ) -> SelectorScope> + where + F: Fn(&T) -> &U, + FMut: Fn(&mut T) -> &mut U, + { + self.map_writer(move |write| MappedMutSignal::new(write, map, map_mut)) + } + + /// Track this scope shallowly. + pub fn track_shallow(&self) { + self.store.track(&self.path); + } + + /// Track this scope recursively. + pub fn track(&self) { + self.store.track_recursive(&self.path); + } + + /// Mark this scope as dirty recursively. + pub fn mark_dirty(&self) { + self.store.mark_dirty(&self.path); + } + + /// Mark this scope as dirty shallowly. + pub fn mark_dirty_shallow(&self) { + self.store.mark_dirty_shallow(&self.path); + } + + /// Mark this scope as dirty at and after the given index. + pub fn mark_dirty_at_and_after_index(&self, index: usize) { + self.store.mark_dirty_at_and_after_index(&self.path, index); + } + + /// Map the writer to a new type. + pub fn map_writer(self, map: impl FnOnce(Lens) -> W2) -> SelectorScope { + SelectorScope { + path: self.path, + store: self.store, + write: map(self.write), + } + } + + /// Write without notifying subscribers. + pub fn write_untracked(&self) -> WritableRef<'static, Lens> + where + Lens: Writable, + { + self.write.write_unchecked() + } +} + +impl Readable for SelectorScope { + type Target = Lens::Target; + type Storage = Lens::Storage; + + fn try_read_unchecked(&self) -> Result, BorrowError> { + self.track(); + self.write.try_read_unchecked() + } + + fn try_peek_unchecked(&self) -> Result, BorrowError> { + self.write.try_peek_unchecked() + } + + fn subscribers(&self) -> Subscribers { + self.store.subscribers(&self.path) + } +} + +impl Writable for SelectorScope { + type WriteMetadata = Lens::WriteMetadata; + + fn try_write_unchecked(&self) -> Result, BorrowMutError> { + self.mark_dirty(); + self.write.try_write_unchecked() + } +} diff --git a/packages/stores/src/store.rs b/packages/stores/src/store.rs new file mode 100644 index 0000000000..e731d8a4b3 --- /dev/null +++ b/packages/stores/src/store.rs @@ -0,0 +1,408 @@ +use crate::{ + scope::SelectorScope, + subscriptions::{StoreSubscriptions, TinyVec}, +}; +use dioxus_core::{ + use_hook, AttributeValue, DynamicNode, IntoAttributeValue, IntoDynNode, Subscribers, SuperInto, +}; +use dioxus_signals::{ + read_impls, write_impls, BorrowError, BorrowMutError, CopyValue, Global, + InitializeFromFunction, MappedMutSignal, ReadSignal, Readable, ReadableExt, ReadableRef, + Storage, UnsyncStorage, Writable, WritableExt, WritableRef, WriteSignal, +}; +use std::marker::PhantomData; + +/// A type alias for a store that has been mapped with a function +pub(crate) type MappedStore< + T, + Lens, + F = fn(&::Target) -> &T, + FMut = fn(&mut ::Target) -> &mut T, +> = Store>; + +/// A type alias for a boxed read-only store. +pub type ReadStore = Store>; + +/// A type alias for a boxed writable-only store. +pub type WriteStore = Store>; + +/// Stores are a reactive type built for nested data structures. Each store will lazily create signals +/// for each field/member of the data structure as needed. +/// +/// By default stores act a lot like [`dioxus_signals::Signal`]s, but they provide more granular +/// subscriptions without requiring nested signals. You should derive [`Store`](dioxus_stores_macro::Store) on your data +/// structures to generate selectors that let you scope the store to a specific part of your data. +/// +/// You can also use the [`#[store]`](dioxus_stores_macro::store) macro on an impl block to add any additional methods to your store +/// with an extension trait. This lets you add methods to the store even though the type is not defined in your crate. +/// +/// # Example +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// fn main() { +/// dioxus::launch(app); +/// } +/// +/// // Deriving the store trait provides methods to scope the store to specific parts of your data structure. +/// // The `Store` macro generates a `count` and `children` method for the `CounterTree` struct. +/// #[derive(Store, Default)] +/// struct CounterTree { +/// count: i32, +/// children: Vec, +/// } +/// +/// // The store macro generates an extension trait with additional methods for the store based on the impl block. +/// #[store] +/// impl Store { +/// // Methods that take &self automatically require the lens to implement `Readable` which lets you read the store. +/// fn sum(&self) -> i32 { +/// self.count().cloned() + self.children().iter().map(|c| c.sum()).sum::() +/// } +/// } +/// +/// fn app() -> Element { +/// let value = use_store(Default::default); +/// +/// rsx! { +/// Tree { +/// value +/// } +/// } +/// } +/// +/// #[component] +/// fn Tree(value: Store) -> Element { +/// // Calling the generated `count` method returns a new store that can only +/// // read and write the count field +/// let mut count = value.count(); +/// let mut children = value.children(); +/// rsx! { +/// button { +/// // Incrementing the count will only rerun parts of the app that have read the count field +/// onclick: move |_| count += 1, +/// "Increment" +/// } +/// button { +/// // Stores are aware of data structures like `Vec` and `Hashmap`. When we push an item to the vec +/// // it will only rerun the parts of the app that depend on the length of the vec +/// onclick: move |_| children.push(Default::default()), +/// "Push child" +/// } +/// "sum: {value.sum()}" +/// ul { +/// // Iterating over the children gives us stores scoped to each child. +/// for value in children.iter() { +/// li { +/// Tree { value } +/// } +/// } +/// } +/// } +/// } +/// ``` +pub struct Store> { + selector: SelectorScope, + _phantom: PhantomData>, +} + +impl> Store> { + /// Creates a new `Store` that might be sync. This allocates memory in the current scope, so this should only be called + /// inside of an initialization closure like the closure passed to [`use_hook`]. + #[track_caller] + pub fn new_maybe_sync(value: T) -> Self { + let store = StoreSubscriptions::new(); + let value = CopyValue::new_maybe_sync(value); + + let path = TinyVec::new(); + let selector = SelectorScope::new(path, store, value); + selector.into() + } +} + +impl Store { + /// Creates a new `Store`. This allocates memory in the current scope, so this should only be called + /// inside of an initialization closure like the closure passed to [`use_hook`]. + #[track_caller] + pub fn new(value: T) -> Self { + let store = StoreSubscriptions::new(); + let value = CopyValue::new_maybe_sync(value); + let value = value.into(); + + let path = TinyVec::new(); + let selector = SelectorScope::new(path, store, value); + selector.into() + } +} + +impl Store { + /// Get the underlying selector for this store. The selector provides low level access to the lazy tracking system + /// of the store. This can be useful to create selectors for custom data structures in libraries. For most applications + /// the selectors generated by the [`Store`](dioxus_stores_macro::Store) macro provide all the functionality you need. + pub fn selector(&self) -> &SelectorScope { + &self.selector + } + + /// Convert the store into the underlying selector + pub fn into_selector(self) -> SelectorScope { + self.selector + } +} + +impl From> for Store { + fn from(selector: SelectorScope) -> Self { + Self { + selector, + _phantom: PhantomData, + } + } +} + +impl PartialEq for Store +where + Lens: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.selector == other.selector + } +} +impl Clone for Store +where + Lens: Clone, +{ + fn clone(&self) -> Self { + Self { + selector: self.selector.clone(), + _phantom: ::std::marker::PhantomData, + } + } +} +impl Copy for Store where Lens: Copy {} + +impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From> + for Store> +where + Lens: Writable + 'static, + __F: Fn(&Lens::Target) -> &T + 'static, + __FMut: Fn(&mut Lens::Target) -> &mut T + 'static, + T: 'static, +{ + fn from(value: MappedStore) -> Self { + Store { + selector: value.selector.map_writer(::std::convert::Into::into), + _phantom: ::std::marker::PhantomData, + } + } +} +impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From> + for Store> +where + Lens: Writable + 'static, + __F: Fn(&Lens::Target) -> &T + 'static, + __FMut: Fn(&mut Lens::Target) -> &mut T + 'static, + T: 'static, +{ + fn from(value: MappedStore) -> Self { + Store { + selector: value.selector.map_writer(::std::convert::Into::into), + _phantom: ::std::marker::PhantomData, + } + } +} +impl ::std::convert::From> for ReadStore +where + T: ?Sized + 'static, +{ + fn from(value: Store) -> Self { + Self { + selector: value.selector.map_writer(::std::convert::Into::into), + _phantom: ::std::marker::PhantomData, + } + } +} + +#[doc(hidden)] +pub struct SuperIntoReadSignalMarker; +impl SuperInto, SuperIntoReadSignalMarker> for Store +where + T: ?Sized + 'static, + Lens: Readable + 'static, +{ + fn super_into(self) -> ReadSignal { + ReadSignal::new(self) + } +} + +#[doc(hidden)] +pub struct SuperIntoWriteSignalMarker; +impl SuperInto, SuperIntoWriteSignalMarker> for Store +where + T: ?Sized + 'static, + Lens: Writable + 'static, +{ + fn super_into(self) -> WriteSignal { + WriteSignal::new(self) + } +} + +impl Readable for Store +where + Lens: Readable, + T: 'static, +{ + type Storage = Lens::Storage; + type Target = T; + fn try_read_unchecked(&self) -> Result, BorrowError> { + self.selector.try_read_unchecked() + } + fn try_peek_unchecked(&self) -> Result, BorrowError> { + self.selector.try_peek_unchecked() + } + fn subscribers(&self) -> Subscribers { + self.selector.subscribers() + } +} +impl Writable for Store +where + Lens: Writable, + T: 'static, +{ + type WriteMetadata = Lens::WriteMetadata; + fn try_write_unchecked(&self) -> Result, BorrowMutError> { + self.selector.try_write_unchecked() + } +} +impl IntoAttributeValue for Store +where + Self: Readable, + T: ::std::clone::Clone + IntoAttributeValue + 'static, +{ + fn into_value(self) -> AttributeValue { + ReadableExt::cloned(&self).into_value() + } +} +impl IntoDynNode for Store +where + Self: Readable, + T: ::std::clone::Clone + IntoDynNode + 'static, +{ + fn into_dyn_node(self) -> DynamicNode { + ReadableExt::cloned(&self).into_dyn_node() + } +} +impl ::std::ops::Deref for Store +where + Self: Readable + 'static, + T: ::std::clone::Clone + 'static, +{ + type Target = dyn Fn() -> T; + fn deref(&self) -> &Self::Target { + unsafe { ReadableExt::deref_impl(self) } + } +} + +read_impls!(Store where Lens: Readable); +write_impls!(Store where Lens: Writable); + +/// Create a new [`Store`]. Stores are a reactive type built for nested data structures. +/// +/// +/// By default stores act a lot like [`dioxus_signals::Signal`]s, but they provide more granular +/// subscriptions without requiring nested signals. You should derive [`Store`](dioxus_stores_macro::Store) on your data +/// structures to generate selectors that let you scope the store to a specific part of your data structure. +/// +/// # Example +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// fn main() { +/// dioxus::launch(app); +/// } +/// +/// // Deriving the store trait provides methods to scope the store to specific parts of your data structure. +/// // The `Store` macro generates a `count` and `children` method for `Store`. +/// #[derive(Store, Default)] +/// struct CounterTree { +/// count: i32, +/// children: Vec, +/// } +/// +/// fn app() -> Element { +/// let value = use_store(Default::default); +/// +/// rsx! { +/// Tree { +/// value +/// } +/// } +/// } +/// +/// #[component] +/// fn Tree(value: Store) -> Element { +/// // Calling the generated `count` method returns a new store that can only +/// // read and write the count field +/// let mut count = value.count(); +/// let mut children = value.children(); +/// rsx! { +/// button { +/// // Incrementing the count will only rerun parts of the app that have read the count field +/// onclick: move |_| count += 1, +/// "Increment" +/// } +/// button { +/// // Stores are aware of data structures like `Vec` and `Hashmap`. When we push an item to the vec +/// // it will only rerun the parts of the app that depend on the length of the vec +/// onclick: move |_| children.push(Default::default()), +/// "Push child" +/// } +/// ul { +/// // Iterating over the children gives us stores scoped to each child. +/// for value in children.iter() { +/// li { +/// Tree { value } +/// } +/// } +/// } +/// } +/// } +/// ``` +pub fn use_store(init: impl FnOnce() -> T) -> Store { + use_hook(move || Store::new(init())) +} + +/// A type alias for global stores +/// +/// # Example +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_stores::*; +/// +/// #[derive(Store)] +/// struct Counter { +/// count: i32, +/// } +/// +/// static COUNTER: GlobalStore = Global::new(|| Counter { count: 0 }); +/// +/// fn app() -> Element { +/// let mut count = COUNTER.resolve().count(); +/// +/// rsx! { +/// button { +/// onclick: move |_| count += 1, +/// "{count}" +/// } +/// } +/// } +/// ``` +pub type GlobalStore = Global, T>; + +impl InitializeFromFunction for Store { + fn initialize_from_function(f: fn() -> T) -> Self { + Store::new(f()) + } +} diff --git a/packages/stores/src/subscriptions.rs b/packages/stores/src/subscriptions.rs new file mode 100644 index 0000000000..9dac464e4b --- /dev/null +++ b/packages/stores/src/subscriptions.rs @@ -0,0 +1,323 @@ +use dioxus_core::{ReactiveContext, SubscriberList, Subscribers}; +use dioxus_signals::{CopyValue, ReadableExt, SyncStorage, Writable, WritableExt}; +use std::fmt::Debug; +use std::hash::BuildHasher; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + ops::Deref, + sync::Arc, +}; + +/// A single node in the [`StoreSubscriptions`] tree. Each path is a specific view into the store +/// and can be subscribed to and marked dirty separately. If the whole store is read or written to, all +/// nodes in the subtree are subscribed to or marked as dirty. +#[derive(Clone, Default)] +pub(crate) struct SelectorNode { + subscribers: HashSet, + root: HashMap, +} + +impl SelectorNode { + /// Get an existing selector node by its path. + fn get(&self, path: &[PathKey]) -> Option<&SelectorNode> { + let [first, rest @ ..] = path else { + return Some(self); + }; + self.root.get(first).and_then(|child| child.get(rest)) + } + + /// Get an existing selector node by its path mutably. + fn get_mut(&mut self, path: &[PathKey]) -> Option<&mut SelectorNode> { + let [first, rest @ ..] = path else { + return Some(self); + }; + self.root + .get_mut(first) + .and_then(|child| child.get_mut(rest)) + } + + /// Get a selector mutably or create one if it doesn't exist. This is used when subscribing to + /// a path that may not exist yet. + fn get_mut_or_default(&mut self, path: &[PathKey]) -> &mut SelectorNode { + let [first, rest @ ..] = path else { + return self; + }; + self.root + .entry(*first) + .or_default() + .get_mut_or_default(rest) + } + + /// Visit this node and all of its children in depth-first order, calling the provided function on each node. + /// + /// This is used to mark nodes dirty recursively when a Store is written to. + fn visit_depth_first_mut(&mut self, f: &mut dyn FnMut(&mut SelectorNode)) { + f(self); + for child in self.root.values_mut() { + child.visit_depth_first_mut(f); + } + } + + /// Mark this selector and all children as dirty. This should be called any time a raw mutable reference to a store + /// is exposed to the user. They could write to any level of the store, so we need to mark all nodes as dirty. + fn mark_children_dirty(&mut self, path: &[PathKey]) { + let Some(node) = self.get_mut(path) else { + return; + }; + + // Mark the node and all its children as dirty + node.visit_depth_first_mut(&mut |node| { + node.mark_dirty(); + }); + } + + /// Mark only children after a certain index as dirty. This is used when inserting a new item into a list. + /// Items after the index that is inserted need to be marked dirty because the value that index points to may have changed. + fn mark_dirty_at_and_after_index(&mut self, path: &[PathKey], index: usize) { + let Some(node) = self.get_mut(path) else { + return; + }; + + // Mark the nodes before the index as dirty + for (i, child) in node.root.iter_mut() { + if *i as usize >= index { + child.visit_depth_first_mut(&mut |node| { + node.mark_dirty(); + }); + } + } + } + + /// Mark a specific node as dirty without marking its children. This is used for data structures like HashMaps + /// when inserting or removing items. Inserting an item into a HashMap only changes the length of the map and the + /// specific value that was inserted or removed. + fn mark_dirty_shallow(&mut self, path: &[PathKey]) { + let Some(node) = self.get_mut(path) else { + return; + }; + + node.mark_dirty(); + } + + /// Mark this node as dirty, which will notify all subscribers that the value has changed. + fn mark_dirty(&mut self) { + // We cannot hold the subscribers lock while calling mark_dirty, because mark_dirty can run user code which may cause a new subscriber to be added. If we hold the lock, we will deadlock. + #[allow(clippy::mutable_key_type)] + let mut subscribers = std::mem::take(&mut self.subscribers); + subscribers.retain(|reactive_context| reactive_context.mark_dirty()); + // Extend the subscribers list instead of overwriting it in case a subscriber is added while reactive contexts are marked dirty + self.subscribers.extend(subscribers); + } + + /// Remove a path from the subscription tree + fn remove(&mut self, path: &[PathKey]) { + let [first, rest @ ..] = path else { + return; + }; + if let Some(node) = self.root.get_mut(first) { + if rest.is_empty() { + self.root.remove(first); + } else { + node.remove(rest); + } + } + } +} + +pub(crate) type PathKey = u16; +#[cfg(feature = "large-path")] +const PATH_LENGTH: usize = 32; +#[cfg(not(feature = "large-path"))] +const PATH_LENGTH: usize = 16; + +#[derive(Copy, Clone, PartialEq)] +pub(crate) struct TinyVec { + length: usize, + path: [PathKey; PATH_LENGTH], +} + +impl Default for TinyVec { + fn default() -> Self { + Self::new() + } +} + +impl Debug for TinyVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TinyVec") + .field("path", &&self.path[..self.length]) + .finish() + } +} + +impl TinyVec { + pub(crate) const fn new() -> Self { + Self { + length: 0, + path: [0; PATH_LENGTH], + } + } + + pub(crate) const fn push(&mut self, index: u16) { + if self.length < self.path.len() { + self.path[self.length] = index; + self.length += 1; + } else { + panic!("SelectorPath is full"); + } + } +} + +impl Deref for TinyVec { + type Target = [u16]; + + fn deref(&self) -> &Self::Target { + &self.path[..self.length] + } +} + +#[derive(Default)] +pub(crate) struct StoreSubscriptionsInner { + root: SelectorNode, + hasher: std::collections::hash_map::RandomState, +} + +#[derive(Default)] +pub(crate) struct StoreSubscriptions { + inner: CopyValue, +} + +impl Clone for StoreSubscriptions { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StoreSubscriptions {} + +impl PartialEq for StoreSubscriptions { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl StoreSubscriptions { + /// Create a new instance of StoreSubscriptions. + pub(crate) fn new() -> Self { + Self { + inner: CopyValue::new_maybe_sync(StoreSubscriptionsInner { + root: SelectorNode::default(), + hasher: std::collections::hash_map::RandomState::new(), + }), + } + } + + /// Hash an index into a PathKey using the hasher. The hash should be consistent + /// across calls + pub(crate) fn hash(&self, index: &impl Hash) -> PathKey { + (self.inner.write_unchecked().hasher.hash_one(index) % PathKey::MAX as u64) as PathKey + } + + /// Subscribe to a specific path in the store. + pub(crate) fn track(&self, key: &[PathKey]) { + if let Some(rc) = ReactiveContext::current() { + let subscribers = self.subscribers(key); + rc.subscribe(subscribers); + } + } + + /// Subscribe to a path and all of its children recursively. This should be called any time we give out + /// a raw reference to a store, because the user could read any level of the store. + pub(crate) fn track_recursive(&self, key: &[PathKey]) { + if let Some(rc) = ReactiveContext::current() { + let mut paths = Vec::new(); + { + let mut write = self.inner.write_unchecked(); + + let root = write.root.get_mut_or_default(key); + let mut nodes = vec![(key.to_vec(), &*root)]; + while let Some((path, node)) = nodes.pop() { + for (child_key, child_node) in &node.root { + let mut new_path = path.clone(); + new_path.push(*child_key); + nodes.push((new_path, child_node)); + } + paths.push(path); + } + } + for path in paths { + let subscribers = self.subscribers(&path); + rc.subscribe(subscribers); + } + } + } + + pub(crate) fn mark_dirty(&self, key: &[PathKey]) { + self.inner.write_unchecked().root.mark_children_dirty(key); + } + + pub(crate) fn mark_dirty_shallow(&self, key: &[PathKey]) { + self.inner.write_unchecked().root.mark_dirty_shallow(key); + } + + pub(crate) fn mark_dirty_at_and_after_index(&self, key: &[PathKey], index: usize) { + self.inner + .write_unchecked() + .root + .mark_dirty_at_and_after_index(key, index); + } + + /// Get a subscriber list for a specific path in the store. This is used to subscribe to changes + /// to a specific path in the store and remove the node from the subscription tree when it is no longer needed. + pub(crate) fn subscribers(&self, key: &[PathKey]) -> Subscribers { + Arc::new(StoreSubscribers { + subscriptions: *self, + path: key.to_vec().into_boxed_slice(), + }) + .into() + } +} + +/// A subscriber list implementation that handles garbage collection of the subscription tree. +struct StoreSubscribers { + subscriptions: StoreSubscriptions, + path: Box<[PathKey]>, +} + +impl SubscriberList for StoreSubscribers { + /// Add a subscriber to the subscription list for this path in the store, creating the node if it doesn't exist. + fn add(&self, subscriber: ReactiveContext) { + let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else { + return; + }; + let node = write.root.get_mut_or_default(&self.path); + node.subscribers.insert(subscriber); + } + + /// Remove a subscriber from the subscription list for this path in the store. If the node has no subscribers left + /// remove that node from the subscription tree. + fn remove(&self, subscriber: &ReactiveContext) { + let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else { + return; + }; + let Some(node) = write.root.get_mut(&self.path) else { + return; + }; + node.subscribers.remove(subscriber); + if node.subscribers.is_empty() && node.root.is_empty() { + write.root.remove(&self.path); + } + } + + /// Visit all subscribers for this path in the store, calling the provided function on each subscriber. + fn visit(&self, f: &mut dyn FnMut(&ReactiveContext)) { + let Ok(read) = self.subscriptions.inner.try_read() else { + return; + }; + let Some(node) = read.root.get(&self.path) else { + return; + }; + node.subscribers.iter().for_each(f); + } +} diff --git a/packages/stores/tests/coercions.rs b/packages/stores/tests/coercions.rs new file mode 100644 index 0000000000..2c197ec0cf --- /dev/null +++ b/packages/stores/tests/coercions.rs @@ -0,0 +1,68 @@ +#![allow(unused)] + +use dioxus::prelude::*; +use dioxus_stores::*; + +#[derive(Store)] +struct TodoItem { + checked: bool, + contents: String, +} + +fn app() -> Element { + let item = use_store(|| TodoItem { + checked: false, + contents: "Learn about stores".to_string(), + }); + + rsx! { + TakesReadSignal { + item, + } + TakesReadStore { + item, + } + TakesStr { + item: item.contents().deref(), + } + TakesStrStore { + item: item.contents().deref(), + } + } +} + +#[component] +fn TakesStr(item: ReadSignal) -> Element { + rsx! { + TakesStr { + item, + } + } +} + +#[component] +fn TakesStrStore(item: ReadStore) -> Element { + rsx! { + TakesStrStore { + item, + } + } +} + +#[component] +fn TakesReadSignal(item: ReadSignal) -> Element { + rsx! { + TakesReadSignal { + item, + } + } +} + +#[component] +fn TakesReadStore(item: ReadStore) -> Element { + rsx! { + TakesReadStore { + item, + } + } +} diff --git a/packages/stores/tests/marco.rs b/packages/stores/tests/marco.rs new file mode 100644 index 0000000000..4f50e3bfb3 --- /dev/null +++ b/packages/stores/tests/marco.rs @@ -0,0 +1,235 @@ +#[allow(unused)] +#[allow(clippy::disallowed_names)] +mod macro_tests { + use dioxus_signals::*; + use dioxus_stores::*; + use std::collections::HashMap; + + fn derive_unit() { + #[derive(Store)] + struct TodoItem; + } + + fn derive_struct() { + #[derive(Store)] + struct TodoItem { + checked: bool, + contents: String, + } + + #[store] + impl Store { + fn is_checked(&self) -> bool { + self.checked().cloned() + } + + fn check(&mut self) { + self.checked().set(true); + } + } + + let mut store = use_store(|| TodoItem { + checked: false, + contents: "Learn about stores".to_string(), + }); + + // The store macro creates an extension trait with methods for each field + // that returns a store scoped to that field. + let checked: Store = store.checked(); + let contents: Store = store.contents(); + let checked: bool = checked(); + let contents: String = contents(); + + // It also generates a `transpose` method returns a variant of your structure + // with stores wrapping each of your data types. This can be very useful when destructuring + // or matching your type + let TodoItemStoreTransposed { checked, contents } = store.transpose(); + let checked: bool = checked(); + let contents: String = contents(); + + let is_checked = store.is_checked(); + store.check(); + } + + fn derive_generic_struct() { + #[derive(Store)] + struct Item { + checked: bool, + contents: T, + } + + #[store] + impl Store> { + fn is_checked(&self) -> bool + where + T: 'static, + { + self.checked().cloned() + } + + fn check(&mut self) + where + T: 'static, + { + self.checked().set(true); + } + } + + let mut store = use_store(|| Item { + checked: false, + contents: "Learn about stores".to_string(), + }); + + let checked: Store = store.checked(); + let contents: Store = store.contents(); + let checked: bool = checked(); + let contents: String = contents(); + + let ItemStoreTransposed { checked, contents } = store.transpose(); + let checked: bool = checked(); + let contents: String = contents(); + + let is_checked = store.is_checked(); + store.check(); + } + + fn derive_tuple() { + #[derive(Store, PartialEq, Clone, Debug)] + struct Item(bool, String); + + let store = use_store(|| Item(true, "Hello".to_string())); + + let first = store.field_0(); + let first: bool = first(); + + let transposed = store.transpose(); + let first = transposed.0; + let second = transposed.1; + let first: bool = first(); + let second: String = second(); + } + + fn derive_enum() { + #[derive(Store, PartialEq, Clone, Debug)] + #[non_exhaustive] + enum Enum { + Foo, + Bar(String), + Baz { foo: i32, bar: String }, + FooBar(u32, String), + BarFoo { foo: String }, + } + + #[store] + impl Store { + fn is_foo_or_bar(&self) -> bool { + matches!(self.cloned(), Enum::Foo | Enum::Bar(_)) + } + + fn make_foo(&mut self) { + self.set(Enum::Foo); + } + } + + let mut store = use_store(|| Enum::Bar("Hello".to_string())); + + let foo = store.is_foo(); + let bar = store.is_bar(); + let baz = store.is_baz(); + let foobar = store.is_foo_bar(); + let barfoo = store.is_bar_foo(); + + let foo = store.bar().unwrap(); + let foo: String = foo(); + let bar = store.bar_foo().unwrap(); + let bar: String = bar(); + + let transposed = store.transpose(); + use EnumStoreTransposed::*; + match transposed { + EnumStoreTransposed::Foo => {} + Bar(bar) => { + let bar: String = bar(); + } + Baz { foo, bar } => { + let foo: i32 = foo(); + let bar: String = bar(); + } + FooBar(foo, bar) => { + let foo: u32 = foo(); + let bar: String = bar(); + } + BarFoo { foo } => { + let foo: String = foo(); + } + } + + let is_foo_or_bar = store.is_foo_or_bar(); + store.make_foo(); + } + + fn derive_generic_enum() { + #[derive(Store, PartialEq, Clone, Debug)] + #[non_exhaustive] + enum Enum { + Foo, + Bar(T), + Baz { foo: i32, bar: T }, + FooBar(u32, T), + BarFoo { foo: T }, + } + + #[store] + impl Store> { + fn is_foo_or_bar(&self) -> bool + where + T: Clone + 'static, + { + matches!(self.cloned(), Enum::Foo | Enum::Bar(_)) + } + + fn make_foo(&mut self) + where + T: 'static, + { + self.set(Enum::Foo); + } + } + + let mut store = use_store(|| Enum::Bar("Hello".to_string())); + + let foo = store.is_foo(); + let bar = store.is_bar(); + let baz = store.is_baz(); + let foobar = store.is_foo_bar(); + let barfoo = store.is_bar_foo(); + + let foo = store.bar().unwrap(); + let foo: String = foo(); + let bar = store.bar_foo().unwrap(); + let bar: String = bar(); + + let transposed = store.transpose(); + use EnumStoreTransposed::*; + match transposed { + EnumStoreTransposed::Foo => {} + Bar(bar) => { + let bar: String = bar(); + } + Baz { foo, bar } => { + let foo: i32 = foo(); + let bar: String = bar(); + } + FooBar(foo, bar) => { + let foo: u32 = foo(); + let bar: String = bar(); + } + BarFoo { foo } => { + let foo: String = foo(); + } + } + + let is_foo_or_bar = store.is_foo_or_bar(); + store.make_foo(); + } +}