Stores: Externally tracked state and reactive collections (#4483)

* map signals mutably

* boxed signals

* remove mutable borrow type from boxed writable

* improve map mut example

* restore boxed_mut helper

* make boxed readable/writable types copy

* Move away from the ReadOnlySignal alias in examples

* automatically convert Write

* Fix some tests

* ReadSignal and WriteSignal

* move read only signal alias

* make maps copy

* store prototype

* implement derive macro

* Handle generics in the derive macro

* Make Selector boxable

* derive boxed conversion

* Get rid of store type

* Remove hardcoded type from derive macro

* selector -> store

* Refactor store implementation

* more vec methods

* todo mvc store example

* Implement Storable for HashMap

* fix write

* restore sorting logic in todomvc

* derive readable and writable

* improve todomvc store example

* Result store

* Option store

* collect dead subscribers

* fix clippy and formatting

* recognize some common foreign types automatically

* fix foreign stores in props

* slice and array selectors

* clean up store example

* writable is child owned

* box selector

* read and write to builtins

* re-use the proc macro in the derive macro

* Switch to a extension trait based approach

* switch to extension traits in the derive macro

* transposed method

* derive enums

* Expose Store::new

* remove stores from the prelude

* More consistent naming scheme for SelectorScope methods

* document hashmap methods

* document index extension

* document option ext

* document result ext

* document slice extension

* document the vec extension

* make more methods borrow the store mutably

* Move to impls to have higher priority over the builtin write extension traits

* fix deadlock

* finish stores documentation

* Document the store macro

* cleanup docs and organization

* finish derive docs

* fix typo

* fix clippy

* fix doc tests

* fix derive macro with generics

* fix clippy

* test and fix empty structs

* clean up todomvc store example

* default to a boxed store writer to make it easier to type when providing to the context api

* fix cargo doc

* fix clippy in store example

* relax generational box lifetime bounds

* relax bounds

* relax signal bounds

* fix compilation

* read store

* relax bounds on default store impls

* fix enum derive

* Convert to readsignal and writesignal automatically

* simplify type matching logic

* add write store

* unsized stores

* shrink the path vec type

* use more concrete types

* remove opaque types

* remove copy bounds

* use lens as generic name everywhere

* more consistent borrow semantics

* use a concrete type for the signal metadata and mention references in the map docs

* improve todomvc store docs

* document store derive macro internals

* refactor macro

* improve selector node documentation

* fix clippy

* add btreemap

* fix doc tests

* more option methods

* fix doc

* fix clippy

* add a bunch more result methods

* add a bunch of bounds to the iterators

* fix clippy

* remove option from subscribers

* add store macro to generate extension traits

* fix store example

* fix deadlock

* fix docs

* fix doc tests

* fix clippy

* add stores to public api

* add store macro, and then update todomvc_store example

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-08-05 11:30:21 -07:00
1 parent 5591070e92
commit 58c41f8561
57 files changed
+4956 -434

No files matched your search

Generated
+24
View File
@@ -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"
+5
View File
@@ -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 }
+313
View File
@@ -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<TodoState> with method to zoom into the fields of the struct.
///
/// For this struct, the macro derives the following methods for Store<TodoState>:
/// - `todos(self) -> Store<HashMap<u32, TodoItem>, _>`
/// - `filter(self) -> Store<FilterState, _>`
#[derive(Store, PartialEq, Clone, Debug)]
struct TodoState {
todos: HashMap<u32, TodoItem>,
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<TodoState>.
// Methods that take &self will automatically get a bound that Lens: Readable<Target = TodoState>
// Methods that take &mut self will automatically get a bound that Lens: Writable<Target = TodoState>
#[store]
impl<Lens> Store<TodoState, Lens> {
fn active_items(&self) -> Vec<u32> {
let filter = self.filter().cloned();
let mut active_ids: Vec<u32> = 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<Lens> Store<TodoItem, Lens> {
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<TodoState>) -> 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<TodoState>, 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<TodoState>, active_todo_count: ReadSignal<usize>) -> 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"
}
}
}
}
}
+24 -34
View File
@@ -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<TokenStream, Error> {
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]
+15
View File
@@ -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 {
+2 -1
View File
@@ -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"]
+7
View File
@@ -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)]
+34 -26
View File
@@ -117,11 +117,6 @@ impl<T, S: Storage<T>> GenerationalBox<T, S> {
*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<T, S: Storage<T>> GenerationalBox<T, S> {
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<GenerationalBox<T, S>> {
@@ -150,6 +140,21 @@ impl<T, S: Storage<T>> GenerationalBox<T, S> {
}
}
impl<T, S> GenerationalBox<T, S> {
/// 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<T, S> Copy for GenerationalBox<T, S> {}
impl<T, S> Clone for GenerationalBox<T, S> {
@@ -159,7 +164,7 @@ impl<T, S> Clone for GenerationalBox<T, S> {
}
/// A trait for a storage backing type. (RefCell, RwLock, etc.)
pub trait Storage<Data = ()>: AnyStorage + 'static {
pub trait Storage<Data = ()>: AnyStorage {
/// Try to read the value. Returns None if the value is no longer valid.
fn try_read(pointer: GenerationalPointer<Self>) -> BorrowResult<Self::Ref<'static, Data>>;
@@ -193,34 +198,34 @@ pub trait Storage<Data = ()>: 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<Target = T>;
type Ref<'a, T: ?Sized + 'a>: Deref<Target = T>;
/// The mutable reference this storage type returns.
type Mut<'a, T: ?Sized + 'static>: DerefMut<Target = T>;
type Mut<'a, T: ?Sized + 'a>: DerefMut<Target = T>;
/// 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<T: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<T: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, T>,
f: impl FnOnce(&mut T) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>>;
/// Map the mutable ref.
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
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<T: ?Sized, U: ?Sized + 'static>(
fn try_map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> Option<&U>,
) -> Option<Self::Ref<'_, U>>;
/// Map the ref.
fn map<T: ?Sized, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
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<Self>);
/// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates.
fn owner() -> Owner<Self> {
fn owner() -> Owner<Self>
where
Self: 'static,
{
Owner(Arc::new(Mutex::new(OwnerInner {
owned: Default::default(),
})))
@@ -374,7 +382,7 @@ impl<S: AnyStorage> Clone for Owner<S> {
impl<S: AnyStorage> Owner<S> {
/// Insert a value into the store. The value will be dropped when the owner is dropped.
#[track_caller]
pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T, S>
pub fn insert<T>(&self, value: T) -> GenerationalBox<T, S>
where
S: Storage<T>,
{
@@ -383,7 +391,7 @@ impl<S: AnyStorage> Owner<S> {
/// Create a new reference counted box. The box will be dropped when all references are dropped.
#[track_caller]
pub fn insert_rc<T: 'static>(&self, value: T) -> GenerationalBox<T, S>
pub fn insert_rc<T>(&self, value: T) -> GenerationalBox<T, S>
where
S: Storage<T>,
{
@@ -391,7 +399,7 @@ impl<S: AnyStorage> Owner<S> {
}
/// 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<T: 'static>(
pub fn insert_rc_with_caller<T>(
&self,
value: T,
caller: &'static std::panic::Location<'static>,
@@ -408,7 +416,7 @@ impl<S: AnyStorage> Owner<S> {
}
/// 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<T: 'static>(
pub fn insert_with_caller<T>(
&self,
value: T,
caller: &'static std::panic::Location<'static>,
@@ -428,7 +436,7 @@ impl<S: AnyStorage> Owner<S> {
///
/// 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<T: 'static>(
pub fn insert_reference<T>(
&self,
other: GenerationalBox<T, S>,
) -> BorrowResult<GenerationalBox<T, S>>
+3 -3
View File
@@ -84,7 +84,7 @@ pub struct GenerationalRef<R> {
guard: GenerationalRefBorrowGuard,
}
impl<T: ?Sized + 'static, R: Deref<Target = T>> GenerationalRef<R> {
impl<T: ?Sized, R: Deref<Target = T>> GenerationalRef<R> {
pub(crate) fn new(inner: R, guard: GenerationalRefBorrowGuard) -> Self {
Self { inner, guard }
}
@@ -118,7 +118,7 @@ impl<T: ?Sized + Display, R: Deref<Target = T>> Display for GenerationalRef<R> {
}
}
impl<T: ?Sized + 'static, R: Deref<Target = T>> Deref for GenerationalRef<R> {
impl<T: ?Sized, R: Deref<Target = T>> Deref for GenerationalRef<R> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -218,7 +218,7 @@ pub struct GenerationalRefMut<W> {
pub(crate) borrow: GenerationalRefBorrowMutGuard,
}
impl<T: ?Sized + 'static, R: DerefMut<Target = T>> GenerationalRefMut<R> {
impl<T: ?Sized, R: DerefMut<Target = T>> GenerationalRefMut<R> {
pub(crate) fn new(inner: R, borrow: GenerationalRefBorrowMutGuard) -> Self {
Self { inner, borrow }
}
+8 -8
View File
@@ -178,43 +178,43 @@ fn sync_runtime() -> &'static Arc<Mutex<Vec<&'static SyncStorage>>> {
}
impl AnyStorage for SyncStorage {
type Ref<'a, R: ?Sized + 'static> = GenerationalRef<MappedRwLockReadGuard<'a, R>>;
type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut<MappedRwLockWriteGuard<'a, W>>;
type Ref<'a, R: ?Sized + 'a> = GenerationalRef<MappedRwLockReadGuard<'a, R>>;
type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut<MappedRwLockWriteGuard<'a, W>>;
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<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> &U,
) -> Self::Ref<'_, U> {
ref_.map(|inner| MappedRwLockReadGuard::map(inner, f))
}
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
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<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map<I: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, I>,
f: impl FnOnce(&I) -> Option<&U>,
) -> Option<Self::Ref<'_, U>> {
ref_.try_map(|inner| MappedRwLockReadGuard::try_map(inner, f).ok())
}
fn try_map_mut<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<I: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, I>,
f: impl FnOnce(&mut I) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>> {
+8 -8
View File
@@ -174,43 +174,43 @@ impl UnsyncStorage {
}
impl AnyStorage for UnsyncStorage {
type Ref<'a, R: ?Sized + 'static> = GenerationalRef<Ref<'a, R>>;
type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut<RefMut<'a, W>>;
type Ref<'a, R: ?Sized + 'a> = GenerationalRef<Ref<'a, R>>;
type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut<RefMut<'a, W>>;
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<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> &U,
) -> Self::Ref<'_, U> {
ref_.map(|inner| Ref::map(inner, f))
}
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
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<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map<I: ?Sized, U: ?Sized>(
_self: Self::Ref<'_, I>,
f: impl FnOnce(&I) -> Option<&U>,
) -> Option<Self::Ref<'_, U>> {
_self.try_map(|inner| Ref::filter_map(inner, f).ok())
}
fn try_map_mut<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<I: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, I>,
f: impl FnOnce(&mut I) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>> {
+6 -6
View File
@@ -14,7 +14,7 @@ fn compile_fail() {}
#[test]
fn leaking_is_ok() {
fn leaking_is_ok_test<S: Storage<String>>() {
fn leaking_is_ok_test<S: Storage<String> + 'static>() {
let data = String::from("hello world");
let key;
{
@@ -37,7 +37,7 @@ fn leaking_is_ok() {
#[test]
fn drops() {
fn drops_test<S: Storage<String>>() {
fn drops_test<S: Storage<String> + 'static>() {
let data = String::from("hello world");
let key;
{
@@ -56,7 +56,7 @@ fn drops() {
#[test]
fn works() {
fn works_test<S: Storage<i32>>() {
fn works_test<S: Storage<i32> + '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<S: Storage<String> + Storage<&'static i32>>() {
fn insert_while_reading_test<S: Storage<String> + 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<S: Storage<i32>>() {
fn panics_test<S: Storage<i32> + 'static>() {
let owner = S::owner();
let key = owner.insert(1);
@@ -103,7 +103,7 @@ fn panics() {
#[test]
fn fuzz() {
fn maybe_owner_scope<S: Storage<String>>(
fn maybe_owner_scope<S: Storage<String> + 'static>(
valid_keys: &mut Vec<GenerationalBox<String, S>>,
invalid_keys: &mut Vec<GenerationalBox<String, S>>,
path: &mut Vec<u8>,
+4 -4
View File
@@ -35,7 +35,7 @@ fn create_at_location<S: Storage<i32>>(
#[test]
fn read_while_writing_error() {
fn read_while_writing_error_test<S: Storage<i32>>() {
fn read_while_writing_error_test<S: Storage<i32> + '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<S: Storage<i32>>() {
fn read_after_dropped_error_test<S: Storage<i32> + '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<S: Storage<i32>>() {
fn write_while_writing_error_test<S: Storage<i32> + '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<S: Storage<i32>>() {
fn write_while_reading_error_test<S: Storage<i32> + 'static>() {
let owner = S::owner();
let value = owner.insert(1);
@@ -2,7 +2,7 @@ use generational_box::{Storage, SyncStorage, UnsyncStorage};
#[test]
fn reference_counting() {
fn reference_counting<S: Storage<String>>() {
fn reference_counting<S: Storage<String> + '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<S: Storage<String>>() {
fn move_reference_in_place<S: Storage<String> + 'static>() {
let data1 = String::from("hello world");
let data2 = String::from("hello world 2");
+1 -1
View File
@@ -7,7 +7,7 @@ use generational_box::{Storage, SyncStorage, UnsyncStorage};
#[test]
fn reused() {
fn reused_test<S: Storage<i32>>() {
fn reused_test<S: Storage<i32> + 'static>() {
let first_ptr;
{
let owner = S::owner();
+1 -1
View File
@@ -181,7 +181,7 @@ impl Readable for UseFuture {
self.state.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers {
self.state.subscribers()
}
}
+1 -1
View File
@@ -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<R: PartialEq>(mut f: impl FnMut() -> R + 'static) -> Memo<R> {
pub fn use_memo<R: PartialEq + 'static>(mut f: impl FnMut() -> R + 'static) -> Memo<R> {
let callback = use_callback(move |_| f());
let caller = std::panic::Location::caller();
#[allow(clippy::redundant_closure)]
+1 -1
View File
@@ -454,7 +454,7 @@ impl<T> Readable for Resource<T> {
self.value.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers {
self.value.subscribers()
}
}
+2 -2
View File
@@ -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<R: Eq + Hash>(f: impl FnMut() -> R + 'static) -> SetCompare<R> {
pub fn use_set_compare<R: Eq + Hash + 'static>(f: impl FnMut() -> R + 'static) -> SetCompare<R> {
use_hook(move || SetCompare::new(f))
}
@@ -48,7 +48,7 @@ pub fn use_set_compare<R: Eq + Hash>(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<R: Eq + Hash>(
pub fn use_set_compare_equal<R: Eq + Hash + 'static>(
value: R,
mut compare: SetCompare<R>,
) -> ReadSignal<bool> {
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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::*;
+95 -78
View File
@@ -12,7 +12,7 @@ use crate::{
pub type ReadOnlySignal<T> = ReadSignal<T>;
/// A boxed version of [Readable] that can be used to store any readable type.
pub struct ReadSignal<T: ?Sized + 'static> {
pub struct ReadSignal<T: ?Sized> {
value: CopyValue<Box<dyn Readable<Target = T, Storage = UnsyncStorage>>>,
}
@@ -25,45 +25,42 @@ impl<T: ?Sized + 'static> ReadSignal<T> {
}
/// 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| {
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| {
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<T: ?Sized + 'static> Clone for ReadSignal<T> {
impl<T: ?Sized> Clone for ReadSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized + 'static> Copy for ReadSignal<T> {}
impl<T: ?Sized> Copy for ReadSignal<T> {}
impl<T: ?Sized + 'static> PartialEq for ReadSignal<T> {
impl<T: ?Sized> PartialEq for ReadSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
@@ -103,14 +100,17 @@ impl<T: Clone + 'static> Deref for ReadSignal<T> {
}
}
impl<T: 'static> Readable for ReadSignal<T> {
impl<T: ?Sized> Readable for ReadSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -118,39 +118,46 @@ impl<T: 'static> Readable for ReadSignal<T> {
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.value.try_peek_unchecked().unwrap().subscribers()
}
}
// We can't implement From<impl Readable<Target = T, Storage = S> + 'static> for ReadSignal<T, S>
// We can't implement From<impl Readable<Target = T, Storage = S> > for ReadSignal<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for ReadSignal<T> {
impl<T: 'static> From<Signal<T>> for ReadSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T: PartialEq> From<Memo<T>> for ReadSignal<T> {
impl<T: PartialEq + 'static> From<Memo<T>> for ReadSignal<T> {
fn from(value: Memo<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for ReadSignal<T> {
impl<T: 'static> From<CopyValue<T>> for ReadSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for ReadSignal<R>
impl<T, R> From<Global<T, R>> for ReadSignal<R>
where
T: Readable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
T: Readable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R> + Clone + 'static,
R: 'static,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
@@ -158,7 +165,7 @@ where
}
impl<V, O, F> From<MappedSignal<O, V, F>> for ReadSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
{
@@ -168,7 +175,7 @@ where
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for ReadSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: 'static,
@@ -177,18 +184,23 @@ where
Self::new(value)
}
}
impl<T: ?Sized + 'static> From<WriteSignal<T>> for ReadSignal<T> {
fn from(value: WriteSignal<T>) -> Self {
Self::new(value)
}
}
/// A boxed version of [Writable] that can be used to store any writable type.
pub struct WriteSignal<T: ?Sized + 'static> {
pub struct WriteSignal<T: ?Sized> {
value: CopyValue<
Box<dyn Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = Box<dyn Any>>>,
>,
}
impl<T: ?Sized> WriteSignal<T> {
impl<T: ?Sized + 'static> WriteSignal<T> {
/// Create a new boxed writable value.
pub fn new<M>(
value: impl Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = M> + 'static,
pub fn new(
value: impl Writable<Target = T, Storage = UnsyncStorage, WriteMetadata: 'static> + 'static,
) -> Self {
Self {
value: CopyValue::new(Box::new(BoxWriteMetadata::new(value))),
@@ -213,51 +225,47 @@ impl<W: Readable> Readable for BoxWriteMetadata<W> {
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
W::Target: 'static,
{
self.value.try_read_unchecked()
}
fn try_peek_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
W::Target: 'static,
{
self.value.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
W::Target: 'static,
{
self.value.subscribers()
}
}
impl<W: Writable> Writable for BoxWriteMetadata<W> {
impl<W> Writable for BoxWriteMetadata<W>
where
W: Writable,
W::WriteMetadata: 'static,
{
type WriteMetadata = Box<dyn Any>;
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError>
where
W::Target: 'static,
{
self.value
.try_write_unchecked()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write(&mut self) -> crate::WritableRef<'_, Self> {
self.value
.write()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
fn try_write(
&mut self,
) -> Result<crate::WritableRef<'_, Self>, generational_box::BorrowMutError> {
self.value
.try_write()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value
.write_unchecked()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
}
impl<T: ?Sized> Clone for WriteSignal<T> {
@@ -303,14 +311,17 @@ impl<T: Clone + 'static> Deref for WriteSignal<T> {
}
}
impl<T: 'static> Readable for WriteSignal<T> {
impl<T: ?Sized> Readable for WriteSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -318,28 +329,33 @@ impl<T: 'static> Readable for WriteSignal<T> {
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.value.try_peek_unchecked().unwrap().subscribers()
}
}
impl<T: 'static> Writable for WriteSignal<T> {
impl<T: ?Sized> Writable for WriteSignal<T> {
type WriteMetadata = Box<dyn Any>;
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value.try_peek_unchecked().unwrap().write_unchecked()
}
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -347,22 +363,23 @@ impl<T: 'static> Writable for WriteSignal<T> {
}
}
// We can't implement From<impl Writable<Target = T, Storage = S> + 'static> for Write<T, S>
// We can't implement From<impl Writable<Target = T, Storage = S>> for Write<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for WriteSignal<T> {
impl<T: 'static> From<Signal<T>> for WriteSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for WriteSignal<T> {
impl<T: 'static> From<CopyValue<T>> for WriteSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for WriteSignal<R>
impl<T, R> From<Global<T, R>> for WriteSignal<R>
where
T: Writable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
T: Writable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R> + Clone + 'static,
R: 'static,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
@@ -370,7 +387,7 @@ where
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for WriteSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Writable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: Fn(&mut V::Target) -> &mut O + 'static,
+30 -19
View File
@@ -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<T: 'static, S: Storage<T> = UnsyncStorage> {
pub struct CopyValue<T, S: 'static = UnsyncStorage> {
pub(crate) value: GenerationalBox<T, S>,
pub(crate) origin_scope: ScopeId,
}
#[cfg(feature = "serialize")]
impl<T: 'static, Store: Storage<T>> serde::Serialize for CopyValue<T, Store>
impl<T, Store: Storage<T>> serde::Serialize for CopyValue<T, Store>
where
T: serde::Serialize,
T: serde::Serialize + 'static,
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.read().serialize(serializer)
@@ -34,9 +36,9 @@ where
}
#[cfg(feature = "serialize")]
impl<'de, T: 'static, Store: Storage<T>> serde::Deserialize<'de> for CopyValue<T, Store>
impl<'de, T, Store: Storage<T>> serde::Deserialize<'de> for CopyValue<T, Store>
where
T: serde::Deserialize<'de>,
T: serde::Deserialize<'de> + 'static,
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = T::deserialize(deserializer)?;
@@ -61,17 +63,23 @@ impl<T: 'static> CopyValue<T> {
}
}
impl<T: 'static, S: Storage<T>> CopyValue<T, S> {
impl<T, S: Storage<T>> CopyValue<T, S> {
/// 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<T: 'static, S: Storage<T>> CopyValue<T, S> {
}
/// 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<T: 'static, S: Storage<T>> CopyValue<T, S> {
}
}
impl<T: 'static, S: Storage<T>> Readable for CopyValue<T, S> {
impl<T, S: Storage<T>> Readable for CopyValue<T, S> {
type Target = T;
type Storage = S;
@@ -155,12 +166,12 @@ impl<T: 'static, S: Storage<T>> Readable for CopyValue<T, S> {
self.value.try_read()
}
fn subscribers(&self) -> Option<Subscribers> {
None
fn subscribers(&self) -> Subscribers {
Subscribers::new_noop()
}
}
impl<T: 'static, S: Storage<T>> Writable for CopyValue<T, S> {
impl<T, S: Storage<T>> Writable for CopyValue<T, S> {
type WriteMetadata = ();
#[track_caller]
@@ -172,14 +183,14 @@ impl<T: 'static, S: Storage<T>> Writable for CopyValue<T, S> {
}
}
impl<T: 'static, S: Storage<T>> PartialEq for CopyValue<T, S> {
impl<T, S: AnyStorage> PartialEq for CopyValue<T, S> {
fn eq(&self, other: &Self) -> bool {
self.value.ptr_eq(&other.value)
}
}
impl<T: 'static, S: Storage<T>> Eq for CopyValue<T, S> {}
impl<T, S: AnyStorage> Eq for CopyValue<T, S> {}
impl<T: Copy, S: Storage<T>> Deref for CopyValue<T, S> {
impl<T: Copy + 'static, S: Storage<T>> Deref for CopyValue<T, S> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
@@ -187,13 +198,13 @@ impl<T: Copy, S: Storage<T>> Deref for CopyValue<T, S> {
}
}
impl<T, S: Storage<T>> Clone for CopyValue<T, S> {
impl<T, S> Clone for CopyValue<T, S> {
fn clone(&self) -> Self {
*self
}
}
impl<T, S: Storage<T>> Copy for CopyValue<T, S> {}
impl<T, S> Copy for CopyValue<T, S> {}
read_impls!(CopyValue<T, S: Storage<T>>);
default_impl!(CopyValue<T, S: Storage<T>>);
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::read::ReadableExt;
use crate::read_impls;
use crate::Memo;
impl<T: PartialEq> InitializeFromFunction<T> for Memo<T> {
impl<T: PartialEq + 'static> InitializeFromFunction<T> for Memo<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Memo::new(f)
}
+31 -15
View File
@@ -32,9 +32,10 @@ pub struct Global<T, R = T> {
/// Allow calling a signal with signal() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: Clone + 'static, R: Clone + 'static> Deref for Global<T, R>
impl<T: Clone, R: Clone> Deref for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
T: Readable<Target = R> + InitializeFromFunction<R> + 'static,
T::Target: 'static,
{
type Target = dyn Fn() -> R;
@@ -43,9 +44,9 @@ where
}
}
impl<T: Clone + 'static, R: 'static> Readable for Global<T, R>
impl<T, R> Readable for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
T: Readable<Target = R> + InitializeFromFunction<R> + Clone + 'static,
{
type Target = R;
type Storage = T::Storage;
@@ -53,23 +54,32 @@ where
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
R: 'static,
{
self.resolve().try_read_unchecked()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
R: 'static,
{
self.resolve().try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
R: 'static,
{
self.resolve().subscribers()
}
}
impl<T: Clone + 'static, R: 'static> Writable for Global<T, R>
impl<T: Clone, R> Writable for Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
T: Writable<Target = R> + InitializeFromFunction<R> + 'static,
{
type WriteMetadata = T::WriteMetadata;
@@ -81,9 +91,9 @@ where
}
}
impl<T: Clone + 'static, R: 'static> Global<T, R>
impl<T: Clone, R> Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
T: Writable<Target = R> + InitializeFromFunction<R> + '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<O>(&self, f: impl FnOnce(&mut R) -> O) -> O {
pub fn with_mut<O>(&self, f: impl FnOnce(&mut R) -> O) -> O
where
T::Target: 'static,
{
self.resolve().with_mut(f)
}
}
impl<T: Clone + 'static, R> Global<T, R>
impl<T: Clone, R> Global<T, R>
where
T: InitializeFromFunction<R>,
{
@@ -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<T>(&self, key: GlobalKey) -> Option<Signal<T>> {
pub fn get_signal_with_key<T: 'static>(&self, key: GlobalKey) -> Option<Signal<T>> {
self.map.borrow().get(&key).map(|f| {
*f.downcast_ref::<Signal<T>>().unwrap_or_else(|| {
panic!(
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::read::ReadableExt;
use crate::read_impls;
use crate::Signal;
impl<T> InitializeFromFunction<T> for Signal<T> {
impl<T: 'static> InitializeFromFunction<T> for Signal<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Signal::new(f())
}
+27 -27
View File
@@ -6,33 +6,33 @@
/// use dioxus::prelude::*;
/// use dioxus_core::Subscribers;
///
/// struct MyCopyValue<T: 'static, S: Storage<T>> {
/// struct MyCopyValue<T, S: 'static> {
/// value: CopyValue<T, S>,
/// }
///
/// impl<T: 'static, S: Storage<T>> MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> MyCopyValue<T, S> {
/// fn new_maybe_sync(value: T) -> Self {
/// Self { value: CopyValue::new_maybe_sync(value) }
/// }
/// }
///
/// impl<T: 'static, S: Storage<T>> Readable for MyCopyValue<T, S> {
/// impl<T, S: Storage<T> + 'static> Readable for MyCopyValue<T, S> {
/// type Target = T;
/// type Storage = S;
///
/// fn try_read_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn try_peek_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// 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<T: 'static, S: Storage<T>> {
/// struct MyCopyValue<T, S: 'static> {
/// value: CopyValue<T, S>,
/// }
///
/// impl<T: 'static, S: Storage<T>> Readable for MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> Readable for MyCopyValue<T, S> where T: 'static {
/// type Target = T;
/// type Storage = S;
///
/// fn try_read_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn try_peek_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// 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<T: 'static, S: Storage<T>> {
/// struct MyCopyValue<T, S: 'static> {
/// value: CopyValue<T, S>,
/// }
///
/// impl<T: 'static, S: Storage<T>> Readable for MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> Readable for MyCopyValue<T, S> {
/// type Target = T;
/// type Storage = S;
///
/// fn try_read_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn try_peek_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// 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<T: 'static, S: Storage<T>> {
/// struct MyCopyValue<T, S: 'static> {
/// value: CopyValue<T, S>,
/// }
///
/// impl<T: 'static, S: Storage<T>> Readable for MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> Readable for MyCopyValue<T, S> {
/// type Target = T;
/// type Storage = S;
///
/// fn try_read_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn try_peek_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> where T: 'static {
/// self.value.try_read_unchecked()
/// }
///
/// fn subscribers(&self) -> Option<Subscribers> {
/// 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<T: 'static, S: Storage<T>> {
/// struct MyCopyValue<T, S: 'static> {
/// value: CopyValue<T, S>,
/// }
///
/// impl<T: 'static, S: Storage<T>> Readable for MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> Readable for MyCopyValue<T, S> {
/// type Target = T;
/// type Storage = S;
///
/// fn try_read_unchecked(
/// &self,
/// ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
/// ) -> Result<ReadableRef<'static, Self>, 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<T: 'static, S: Storage<T>> Writable for MyCopyValue<T, S> {
/// impl<T: 'static, S: Storage<T> + 'static> Writable for MyCopyValue<T, S> {
/// fn try_write_unchecked(
/// &self,
/// ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
/// ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> where T: 'static {
/// self.value.try_write_unchecked()
/// }
///
+8 -8
View File
@@ -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<O: ?Sized + 'static, V: Readable, F = fn(&<V as Readable>::Target) -> &O> {
pub struct MappedSignal<O: ?Sized, V, F = fn(&<V as Readable>::Target) -> &O> {
value: V,
map_fn: F,
_marker: std::marker::PhantomData<O>,
@@ -34,11 +34,9 @@ where
impl<V, O, F> MappedSignal<O, V, F>
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<V, O, F> Readable for MappedSignal<O, V, F>
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<Subscribers> {
fn subscribers(&self) -> Subscribers {
self.value.subscribers()
}
}
impl<V, O, F> IntoAttributeValue for MappedSignal<O, V, F>
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<V, O, F> Deref for MappedSignal<O, V, F>
where
O: Clone,
O: Clone + 'static,
V: Readable + 'static,
F: Fn(&V::Target) -> &O + 'static,
{
@@ -111,4 +111,4 @@ where
}
}
read_impls!(MappedSignal<T, V, F> where V: Readable, F: Fn(&V::Target) -> &T);
read_impls!(MappedSignal<T, V, F> where V: Readable<Target: 'static>, F: Fn(&V::Target) -> &T);
+25 -14
View File
@@ -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(&<V as Readable>::Target) -> &O,
FMut = fn(&mut <V as Readable>::Target) -> &mut O,
> {
@@ -22,7 +22,7 @@ pub struct MappedMutSignal<
impl<V, O, F, FMut> Clone for MappedMutSignal<O, V, F, FMut>
where
V: Readable + Clone,
V: Clone,
F: Clone,
FMut: Clone,
{
@@ -38,7 +38,7 @@ where
impl<V, O, F, FMut> Copy for MappedMutSignal<O, V, F, FMut>
where
V: Readable + Copy,
V: Copy,
F: Copy,
FMut: Copy,
{
@@ -47,11 +47,9 @@ where
impl<V, O, F, FMut> MappedMutSignal<O, V, F, FMut>
where
O: ?Sized,
V: Readable,
F: Fn(&V::Target) -> &O,
{
/// Create a new mapped signal.
pub(crate) fn new(value: V, map_fn: F, map_fn_mut: FMut) -> Self {
pub fn new(value: V, map_fn: F, map_fn_mut: FMut) -> Self {
MappedMutSignal {
value,
map_fn,
@@ -65,6 +63,7 @@ impl<V, O, F, FMut> Readable for MappedMutSignal<O, V, F, FMut>
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<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, 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<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
O: 'static,
{
let value = self.value.try_peek_unchecked()?;
Ok(V::Storage::map(value, |v| (self.map_fn)(v)))
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
O: 'static,
{
self.value.subscribers()
}
}
@@ -91,6 +99,7 @@ impl<V, O, F, FMut> Writable for MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> IntoAttributeValue for MappedMutSignal<O, V, F, FMut>
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<V, O, F, FMut> Deref for MappedMutSignal<O, V, F, FMut>
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<T, V, F, FMut> where V: Readable, F: Fn(&V::Target) -> &T);
write_impls!(MappedMutSignal<T, V, F, FMut> where V: Writable, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T);
read_impls!(MappedMutSignal<T, V, F, FMut> where V: Readable<Target: 'static>, F: Fn(&V::Target) -> &T);
write_impls!(MappedMutSignal<T, V, F, FMut> where V: Writable<Target: 'static>, F: Fn(&V::Target) -> &T, FMut: Fn(&mut V::Target) -> &mut T);
+31 -17
View File
@@ -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<T> {
#[doc(alias = "Selector")]
#[doc(alias = "UseMemo")]
#[doc(alias = "Memorize")]
pub struct Memo<T: 'static> {
pub struct Memo<T> {
inner: Signal<T>,
update: CopyValue<UpdateInformation<T>>,
}
impl<T: 'static> Memo<T> {
impl<T> Memo<T> {
/// 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<T: 'static> Memo<T> {
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<T: 'static> Memo<T> {
#[track_caller]
pub const fn global(constructor: fn() -> T) -> GlobalMemo<T>
where
T: PartialEq,
T: PartialEq + 'static,
{
GlobalMemo::new(constructor)
}
@@ -123,7 +122,7 @@ impl<T: 'static> Memo<T> {
#[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<T: 'static> Memo<T> {
}
/// 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<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, 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<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.inner.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.inner.subscribers()
}
}
impl<T> IntoAttributeValue for Memo<T>
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<T> IntoDynNode for Memo<T>
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<T: 'static> PartialEq for Memo<T> {
impl<T: Clone> Deref for Memo<T>
where
T: PartialEq,
T: PartialEq + 'static,
{
type Target = dyn Fn() -> T;
@@ -238,10 +252,10 @@ where
read_impls!(Memo<T> where T: PartialEq);
impl<T: 'static> Clone for Memo<T> {
impl<T> Clone for Memo<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static> Copy for Memo<T> {}
impl<T> Copy for Memo<T> {}
+1
View File
@@ -7,6 +7,7 @@ pub struct ReadFromMarker<M>(std::marker::PhantomData<M>);
impl<T, O, M> SuperFrom<T, ReadFromMarker<M>> for ReadSignal<O>
where
O: SuperFrom<T, M> + 'static,
T: 'static,
{
fn super_from(input: T) -> Self {
ReadSignal::new(Signal::new(O::super_from(input)))
+91 -50
View File
@@ -37,7 +37,7 @@ pub type ReadableRef<'a, T: Readable, O = <T as Readable>::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<ReadableRef<'static, Self>, generational_box::BorrowError>;
) -> Result<ReadableRef<'static, Self>, 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<ReadableRef<'static, Self>, generational_box::BorrowError>;
) -> Result<ReadableRef<'static, Self>, 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<Subscribers>;
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<Self> {
fn read(&self) -> ReadableRef<Self>
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<ReadableRef<Self>, generational_box::BorrowError> {
fn try_read(&self) -> Result<ReadableRef<Self>, 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<Self> {
fn peek(&self) -> ReadableRef<Self>
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<ReadableRef<Self>, generational_box::BorrowError> {
fn try_peek(&self) -> Result<ReadableRef<Self>, 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<F, O>(self, f: F) -> MappedSignal<O, Self, F>
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<O>(&self, f: impl FnOnce(&Self::Target) -> O) -> O {
fn with<O>(&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<O>(&self, f: impl FnOnce(&Self::Target) -> O) -> O {
fn with_peek<O>(&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<I>(&self, index: I) -> ReadableRef<Self, <Self::Target as std::ops::Index<I>>::Output>
where
Self::Target: std::ops::Index<I>,
Self::Target: std::ops::Index<I> + 'static,
{
<Self::Storage as AnyStorage>::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<Storage = UnsyncStorage> {
ReadSignal::new(self)
}
}
impl<R: Readable<Storage = UnsyncStorage> + ?Sized> ReadableBoxExt for R {}
/// An extension trait for `Readable<Vec<T>>` that provides some convenience methods.
pub trait ReadableVecExt<T: 'static>: Readable<Target = Vec<T>> {
pub trait ReadableVecExt<T>: Readable<Target = Vec<T>> {
/// 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<ReadableRef<Self, T>> {
fn first(&self) -> Option<ReadableRef<Self, T>>
where
T: 'static,
{
<Self::Storage as AnyStorage>::try_map(self.read(), |v| v.first())
}
/// Get the last element of the inner vector.
#[track_caller]
fn last(&self) -> Option<ReadableRef<Self, T>> {
fn last(&self) -> Option<ReadableRef<Self, T>>
where
T: 'static,
{
<Self::Storage as AnyStorage>::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<ReadableRef<Self, T>> {
fn get(&self, index: usize) -> Option<ReadableRef<Self, T>>
where
T: 'static,
{
<Self::Storage as AnyStorage>::try_map(self.read(), |v| v.get(index))
}
@@ -318,45 +367,39 @@ impl<'a, T: 'static, R: Readable<Target = Vec<T>>> Iterator for ReadableValueIte
}
}
impl<T, R> ReadableVecExt<T> for R
where
T: 'static,
R: Readable<Target = Vec<T>>,
{
}
impl<T, R> ReadableVecExt<T> for R where R: Readable<Target = Vec<T>> {}
/// An extension trait for `Readable<Option<T>>` that provides some convenience methods.
pub trait ReadableOptionExt<T: 'static>: Readable<Target = Option<T>> {
pub trait ReadableOptionExt<T>: Readable<Target = Option<T>> {
/// 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<ReadableRef<Self, T>> {
fn as_ref(&self) -> Option<ReadableRef<Self, T>>
where
T: 'static,
{
<Self::Storage as AnyStorage>::try_map(self.read(), |v| v.as_ref())
}
}
impl<T, R> ReadableOptionExt<T> for R
where
T: 'static,
R: Readable<Target = Option<T>>,
{
}
impl<T, R> ReadableOptionExt<T> for R where R: Readable<Target = Option<T>> {}
/// An extension trait for `Readable<Option<T>>` that provides some convenience methods.
pub trait ReadableResultExt<T: 'static, E: 'static>: Readable<Target = Result<T, E>> {
pub trait ReadableResultExt<T, E>: Readable<Target = Result<T, E>> {
/// 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<T: 'static, E: 'static>: Readable<Target = Result<T,
/// Attempts to read the inner value of the Option.
#[track_caller]
fn as_ref(&self) -> Result<ReadableRef<Self, T>, ReadableRef<Self, E>> {
fn as_ref(&self) -> Result<ReadableRef<Self, T>, ReadableRef<Self, E>>
where
T: 'static,
E: 'static,
{
<Self::Storage as AnyStorage>::try_map(self.read(), |v| v.as_ref().ok()).ok_or(
<Self::Storage as AnyStorage>::map(self.read(), |v| v.as_ref().err().unwrap()),
)
}
}
impl<T, E, R> ReadableResultExt<T, E> for R
where
T: 'static,
E: 'static,
R: Readable<Target = Result<T, E>>,
{
}
impl<T, E, R> ReadableResultExt<T, E> for R where R: Readable<Target = Result<T, E>> {}
+8 -9
View File
@@ -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<R: 'static, S: Storage<SignalData<bool>> = UnsyncStorage> {
pub struct SetCompare<R, S: 'static = UnsyncStorage> {
subscribers: CopyValue<FxHashMap<R, Signal<bool, S>>>,
}
impl<R: Eq + Hash> SetCompare<R> {
impl<R: Eq + Hash + 'static> SetCompare<R> {
/// 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<R: Eq + Hash> SetCompare<R> {
}
}
impl<R: Eq + Hash, S: Storage<SignalData<bool>>> SetCompare<R, S> {
impl<R: Eq + Hash + 'static, S: Storage<SignalData<bool>> + 'static> SetCompare<R, S> {
/// 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<R> {
let subscribers: CopyValue<FxHashMap<R, Signal<bool>>> =
pub fn new_maybe_sync(mut f: impl FnMut() -> R + 'static) -> SetCompare<R, S> {
let subscribers: CopyValue<FxHashMap<R, Signal<bool, S>>> =
CopyValue::new(FxHashMap::default());
let mut previous = CopyValue::new(None);
@@ -67,7 +66,7 @@ impl<R: Eq + Hash, S: Storage<SignalData<bool>>> SetCompare<R, S> {
}
}
impl<R: Eq + Hash> SetCompare<R> {
impl<R: Eq + Hash + 'static> SetCompare<R> {
/// 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<bool> {
let subscribers = self.subscribers.write();
@@ -85,7 +84,7 @@ impl<R: Eq + Hash> SetCompare<R> {
}
}
impl<R: 'static, S: Storage<SignalData<bool>>> PartialEq for SetCompare<R, S> {
impl<R, S: Storage<SignalData<bool>>> PartialEq for SetCompare<R, S> {
fn eq(&self, other: &Self) -> bool {
self.subscribers == other.subscribers
}
+57 -28
View File
@@ -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<T: 'static, S: Storage<SignalData<T>> = UnsyncStorage> {
pub struct Signal<T, S: 'static = UnsyncStorage> {
pub(crate) inner: CopyValue<SignalData<T>, S>,
}
@@ -139,11 +139,14 @@ impl<T: PartialEq + 'static> Signal<T> {
}
}
impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
impl<T, S: Storage<SignalData<T>>> Signal<T, S> {
/// 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::<SignalData<T>, S>::new_maybe_sync(SignalData {
subscribers: Default::default(),
@@ -165,7 +168,10 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
/// 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
}
/// 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
}
/// 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
}
/// 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
/// *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<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
type Storage = S;
#[track_caller]
fn try_read_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_read_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
let inner = self.inner.try_read_unchecked()?;
if let Some(reactive_context) = ReactiveContext::current() {
@@ -399,19 +420,25 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
///
/// If the signal has been dropped, this will panic.
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.inner
.try_read_unchecked()
.map(|inner| S::map(inner, |v| &v.value))
}
fn subscribers(&self) -> Option<Subscribers> {
Some(self.inner.read().subscribers.clone().into())
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.inner.read().subscribers.clone().into()
}
}
impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
type WriteMetadata = Box<dyn Any>;
type WriteMetadata = SignalSubscriberDrop<T, S>;
#[track_caller]
fn try_write_unchecked(
@@ -423,11 +450,11 @@ impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
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<dyn Any>,
},
)
})
}
@@ -435,7 +462,7 @@ impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
impl<T> IntoAttributeValue for Signal<T>
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<T> IntoDynNode for Signal<T>
where
T: Clone + IntoDynNode,
T: Clone + IntoDynNode + 'static,
{
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
self().into_dyn_node()
}
}
impl<T: 'static, S: Storage<SignalData<T>>> PartialEq for Signal<T, S> {
impl<T, S: Storage<SignalData<T>>> PartialEq for Signal<T, S> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T: 'static, S: Storage<SignalData<T>>> Eq for Signal<T, S> {}
impl<T, S: Storage<SignalData<T>>> Eq for Signal<T, S> {}
/// Allow calling a signal with signal() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: Clone, S: Storage<SignalData<T>> + 'static> Deref for Signal<T, S> {
impl<T: Clone + 'static, S: Storage<SignalData<T>> + 'static> Deref for Signal<T, S> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
@@ -471,7 +498,7 @@ impl<T: Clone, S: Storage<SignalData<T>> + 'static> Deref for Signal<T, S> {
}
#[cfg(feature = "serialize")]
impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>>> serde::Serialize
impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>> + 'static> serde::Serialize
for Signal<T, Store>
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
@@ -480,7 +507,7 @@ impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>>> serde::Serial
}
#[cfg(feature = "serialize")]
impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>>>
impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>> + 'static>
serde::Deserialize<'de> for Signal<T, Store>
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
@@ -488,14 +515,16 @@ impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>>>
}
}
struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
#[doc(hidden)]
/// A drop guard that will update the subscribers of the signal when it is dropped.
pub struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>> + 'static> {
signal: Signal<T, S>,
#[cfg(debug_assertions)]
origin: &'static std::panic::Location<'static>,
}
#[allow(clippy::no_effect)]
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
impl<T: 'static, S: Storage<SignalData<T>> + 'static> Drop for SignalSubscriberDrop<T, S> {
fn drop(&mut self) {
#[cfg(debug_assertions)]
{
@@ -512,10 +541,10 @@ fmt_impls!(Signal<T, S: Storage<SignalData<T>>>);
default_impl!(Signal<T, S: Storage<SignalData<T>>>);
write_impls!(Signal<T, S: Storage<SignalData<T>>>);
impl<T: 'static, S: Storage<SignalData<T>>> Clone for Signal<T, S> {
impl<T, S> Clone for Signal<T, S> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static, S: Storage<SignalData<T>>> Copy for Signal<T, S> {}
impl<T, S> Copy for Signal<T, S> {}
+1 -1
View File
@@ -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<T: 'static, S: generational_box::Storage<T>>(
pub fn copy_value_hoisted<T: 'static, S: generational_box::Storage<T> + 'static>(
value: &crate::CopyValue<T, S>,
caller: &'static std::panic::Location<'static>,
) {
+114 -65
View File
@@ -36,34 +36,16 @@ pub type WritableRef<'a, T: Writable, O = <T as Readable>::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<WritableRef<'_, Self>, 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<WritableRef<'static, Self>, 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<WritableRef<'static, Self>, 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<T, S, D> Deref for WriteLock<'_, T, S, D>
where
S: AnyStorage,
T: ?Sized + 'static,
T: ?Sized,
{
type Target = T;
@@ -268,7 +250,7 @@ where
impl<T, S, D> 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<WritableRef<'_, Self>, 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<O, F, FMut>(self, f: F, f_mut: FMut) -> MappedMutSignal<O, Self, F, FMut>
where
Self: Clone + Sized + 'static,
O: ?Sized + 'static,
F: Fn(&Self::Target) -> &O + 'static,
FMut: Fn(&mut Self::Target) -> &mut O + 'static,
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<O>(&mut self, f: impl FnOnce(&mut Self::Target) -> O) -> O {
fn with_mut<O>(&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<Output = Self::Target> + Clone,
Self::Target: std::ops::Not<Output = Self::Target> + Clone + 'static,
{
let inverted = !(*self.peek()).clone();
self.set(inverted);
@@ -346,7 +363,7 @@ pub trait WritableExt: Writable {
index: I,
) -> WritableRef<'_, Self, <Self::Target as std::ops::Index<I>>::Output>
where
Self::Target: std::ops::IndexMut<I>,
Self::Target: std::ops::IndexMut<I> + '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<T: Writable<Storage = UnsyncStorage> + 'static> WritableBoxedExt for T {
}
/// An extension trait for [`Writable<Option<T>>`]` that provides some convenience methods.
pub trait WritableOptionExt<T: 'static>: Writable<Target = Option<T>> {
pub trait WritableOptionExt<T>: Writable<Target = Option<T>> {
/// 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<T: 'static>: Writable<Target = Option<T>> {
/// Attempts to write the inner value of the Option.
#[track_caller]
fn as_mut(&mut self) -> Option<WritableRef<'_, Self, T>> {
fn as_mut(&mut self) -> Option<WritableRef<'_, Self, T>>
where
T: 'static,
{
WriteLock::filter_map(self.write(), |v: &mut Option<T>| v.as_mut())
}
}
impl<T, W> WritableOptionExt<T> for W
where
T: 'static,
W: Writable<Target = Option<T>>,
{
}
impl<T, W> WritableOptionExt<T> for W where W: Writable<Target = Option<T>> {}
/// An extension trait for [`Writable<Vec<T>>`] that provides some convenience methods.
pub trait WritableVecExt<T: 'static>: Writable<Target = Vec<T>> {
pub trait WritableVecExt<T>: Writable<Target = Vec<T>> {
/// 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<T> {
fn pop(&mut self) -> Option<T>
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<Item = T>) {
fn extend(&mut self, iter: impl IntoIterator<Item = T>)
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<T> {
fn split_off(&mut self, at: usize) -> Vec<T>
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<WritableRef<'_, Self, T>> {
fn get_mut(&mut self, index: usize) -> Option<WritableRef<'_, Self, T>>
where
T: 'static,
{
WriteLock::filter_map(self.write(), |v: &mut Vec<T>| v.get_mut(index))
}
@@ -524,9 +578,4 @@ impl<'a, T: 'static, R: Writable<Target = Vec<T>>> Iterator for WritableValueIte
}
}
impl<T, W> WritableVecExt<T> for W
where
T: 'static,
W: Writable<Target = Vec<T>>,
{
}
impl<T, W> WritableVecExt<T> for W where W: Writable<Target = Vec<T>> {}
+26
View File
@@ -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"]
+493
View File
@@ -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<TokenStream2> {
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<TokenStream2> {
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::<Vec<_>>();
// 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<TokenStream2>,
definitions: &mut Vec<TokenStream2>,
implementations: &mut Vec<TokenStream2>,
) {
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<TokenStream2> {
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<Target = #enum_name #ty_generics>, #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<Field, W> 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<Store<O, W>> 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::<Vec<_>>();
// 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<TokenStream2>,
implementations: &mut Vec<TokenStream2>,
) {
// 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<Enum, W> into Option<Store<VariantField, W>> 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<TokenStream2>,
implementations: &mut Vec<TokenStream2>,
) {
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> }
}
+281
View File
@@ -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<TokenStream> {
// 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<T> { ... }` 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<Target = #item> + ::std::marker::Copy + 'static);
let mutable_bounds: WherePredicate = parse_quote!(#lens_generic: dioxus_stores::macro_helpers::dioxus_signals::Writable<Target = #item> + ::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<String> {
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<TokenStream> {
let ItemImpl {
attrs,
defaultness,
unsafety,
items,
..
} = item;
let generics = &item.generics;
let items = items
.iter()
.map(item_to_trait_definition)
.collect::<syn::Result<Vec<_>>>()?;
Ok(quote! {
#(#attrs)*
#visibility #defaultness #unsafety trait #trait_name #generics {
#(#items)*
}
})
}
fn item_to_trait_definition(item: &syn::ImplItem) -> syn::Result<proc_macro2::TokenStream> {
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<Type> {
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<StorePath> {
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<T> {...}`",
))
}
/// The args the `#[store]` attribute macro accepts
pub(crate) struct ExtendArgs {
/// The name of the extension trait generated
name: Option<Ident>,
/// The visibility of the extension trait
visibility: Option<syn::Visibility>,
}
impl Parse for ExtendArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
// 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 })
}
}
+179
View File
@@ -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<YourType, W>` 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<bool, _> = store.checked();
/// let contents: Store<String, _> = 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<String, _>> = 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<Lens> Store<TodoItem, Lens> {
/// // Since this method takes &mut self, the lens will require Writable automatically. It cannot be used
/// // with ReadStore<TodoItem>
/// fn toggle_checked(&mut self) {
/// self.checked().toggle();
/// }
///
/// // Since this method takes &self, the lens will require Readable automatically
/// fn checked_contents(&self) -> Option<String> {
/// 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<String> = 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)
}
+26
View File
@@ -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"]
+64
View File
@@ -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<CounterTree>`
#[derive(Store, Default)]
struct CounterTree {
count: i32,
children: Vec<CounterTree>,
}
fn app() -> Element {
let value = use_store(Default::default);
rsx! {
Tree {
value
}
}
}
#[component]
fn Tree(value: Store<CounterTree>) -> 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 }
}
}
}
}
}
```
+366
View File
@@ -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<Lens: Readable<Target = BTreeMap<K, V>> + 'static, K: 'static, V: 'static>
Store<BTreeMap<K, V>, 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<Item = (K, Store<V, GetWrite<K, Lens>>)>
+ 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<Item = Store<V, GetWrite<K, Lens>>>
+ DoubleEndedIterator
+ FusedIterator
+ '_
where
K: Hash + Ord + Clone,
Lens: Clone,
{
self.selector().track_shallow();
let keys = self.selector().peek().keys().cloned().collect::<Vec<_>>();
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<Q>(&mut self, key: &Q) -> Option<V>
where
Q: ?Sized + Ord + 'static,
K: Borrow<Q> + 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<Q>(&self, key: &Q) -> bool
where
Q: ?Sized + Ord + 'static,
K: Borrow<Q> + 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<Q>(self, key: Q) -> Option<Store<V, GetWrite<Q, Lens>>>
where
Q: Hash + Ord + 'static,
K: Borrow<Q> + 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, Write> {
index: Index,
write: Write,
}
impl<Index, Write, K, V> Readable for GetWrite<Index, Write>
where
Write: Readable<Target = BTreeMap<K, V>>,
Index: Ord + 'static,
K: Borrow<Index> + Ord + 'static,
{
type Target = V;
type Storage = Write::Storage;
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, 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<dioxus_signals::ReadableRef<'static, Self>, 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<Index, Write, K, V> Writable for GetWrite<Index, Write>
where
Write: Writable<Target = BTreeMap<K, V>>,
Index: Ord + 'static,
K: Borrow<Index> + Ord + 'static,
{
type WriteMetadata = Write::WriteMetadata;
fn try_write_unchecked(
&self,
) -> Result<dioxus_signals::WritableRef<'static, Self>, 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<Index, Write, K, V> ::std::convert::From<Store<V, GetWrite<Index, Write>>>
for Store<V, WriteSignal<V>>
where
Write::WriteMetadata: 'static,
Write: Writable<Target = BTreeMap<K, V>, Storage = UnsyncStorage> + 'static,
Index: Ord + 'static,
K: Borrow<Index> + Ord + 'static,
V: 'static,
{
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| WriteSignal::new(writer))
.into()
}
}
impl<Index, Write, K, V> ::std::convert::From<Store<V, GetWrite<Index, Write>>> for ReadStore<V>
where
Write: Readable<Target = BTreeMap<K, V>, Storage = UnsyncStorage> + 'static,
Index: Ord + 'static,
K: Borrow<Index> + Ord + 'static,
V: 'static,
{
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| ReadSignal::new(writer))
.into()
}
}
+28
View File
@@ -0,0 +1,28 @@
use std::ops::DerefMut;
use crate::{store::Store, MappedStore};
use dioxus_signals::Readable;
impl<Lens, T> Store<T, Lens>
where
Lens: Readable<Target = T> + '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<T::Target, Lens> {
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()
}
}
+380
View File
@@ -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<Lens: Readable<Target = HashMap<K, V, St>> + 'static, K: 'static, V: 'static, St: 'static>
Store<HashMap<K, V, St>, 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<Item = (K, Store<V, GetWrite<K, Lens>>)>
+ 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<Item = Store<V, GetWrite<K, Lens>>>
+ DoubleEndedIterator
+ FusedIterator
+ '_
where
K: Eq + Hash + Clone,
St: BuildHasher,
Lens: Clone,
{
self.selector().track_shallow();
let keys = self.selector().peek().keys().cloned().collect::<Vec<_>>();
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<Q>(&mut self, key: &Q) -> Option<V>
where
Q: ?Sized + Hash + Eq + 'static,
K: Borrow<Q> + 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<Q>(&self, key: &Q) -> bool
where
Q: ?Sized + Hash + Eq + 'static,
K: Borrow<Q> + 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<Q>(self, key: Q) -> Option<Store<V, GetWrite<Q, Lens>>>
where
Q: Hash + Eq + 'static,
K: Borrow<Q> + 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, Write> {
index: Index,
write: Write,
}
impl<Index, Write, K, V, St> Readable for GetWrite<Index, Write>
where
Write: Readable<Target = HashMap<K, V, St>>,
Index: Hash + Eq + 'static,
K: Borrow<Index> + Eq + Hash + 'static,
St: BuildHasher + 'static,
{
type Target = V;
type Storage = Write::Storage;
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, 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<dioxus_signals::ReadableRef<'static, Self>, 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<Index, Write, K, V, St> Writable for GetWrite<Index, Write>
where
Write: Writable<Target = HashMap<K, V, St>>,
Index: Hash + Eq + 'static,
K: Borrow<Index> + Eq + Hash + 'static,
St: BuildHasher + 'static,
{
type WriteMetadata = Write::WriteMetadata;
fn try_write_unchecked(
&self,
) -> Result<dioxus_signals::WritableRef<'static, Self>, 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<Index, Write, K, V, St> ::std::convert::From<Store<V, GetWrite<Index, Write>>>
for Store<V, WriteSignal<V>>
where
Write::WriteMetadata: 'static,
Write: Writable<Target = HashMap<K, V, St>, Storage = UnsyncStorage> + 'static,
Index: Hash + Eq + 'static,
K: Borrow<Index> + Eq + Hash + 'static,
St: BuildHasher + 'static,
V: 'static,
{
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| WriteSignal::new(writer))
.into()
}
}
impl<Index, Write, K, V, St> ::std::convert::From<Store<V, GetWrite<Index, Write>>> for ReadStore<V>
where
Write: Readable<Target = HashMap<K, V, St>, Storage = UnsyncStorage> + 'static,
Index: Hash + Eq + 'static,
K: Borrow<Index> + Eq + Hash + 'static,
St: BuildHasher + 'static,
V: 'static,
{
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| ReadSignal::new(writer))
.into()
}
}
+136
View File
@@ -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<Lens, T> Store<T, Lens> {
/// 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<Idx>(self, index: Idx) -> Store<T::Output, IndexWrite<Idx, Lens>>
where
T: IndexMut<Idx> + 'static,
Idx: Hash + 'static,
Lens: Readable<Target = T> + '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, Write> {
index: Index,
write: Write,
}
impl<Index, Write> Readable for IndexWrite<Index, Write>
where
Write: Readable,
Write::Target: ops::Index<Index> + 'static,
Index: Clone,
{
type Target = <Write::Target as ops::Index<Index>>::Output;
type Storage = Write::Storage;
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, 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<dioxus_signals::ReadableRef<'static, Self>, 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<Index, Write> Writable for IndexWrite<Index, Write>
where
Write: Writable,
Write::Target: ops::IndexMut<Index> + 'static,
Index: Clone,
{
type WriteMetadata = Write::WriteMetadata;
fn try_write_unchecked(
&self,
) -> Result<dioxus_signals::WritableRef<'static, Self>, 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<Idx, T, Write> ::std::convert::From<Store<T, IndexWrite<Idx, Write>>>
for Store<T, WriteSignal<T>>
where
Write: Writable<Storage = UnsyncStorage> + 'static,
Write::WriteMetadata: 'static,
Write::Target: ops::IndexMut<Idx, Output = T> + 'static,
Idx: Clone + 'static,
T: 'static,
{
fn from(value: Store<T, IndexWrite<Idx, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| WriteSignal::new(writer))
.into()
}
}
impl<Idx, T, Write> ::std::convert::From<Store<T, IndexWrite<Idx, Write>>> for ReadStore<T>
where
Write: Readable<Storage = UnsyncStorage> + 'static,
Write::Target: ops::Index<Idx, Output = T> + 'static,
Idx: Clone + 'static,
T: 'static,
{
fn from(value: Store<T, IndexWrite<Idx, Write>>) -> Self {
value
.into_selector()
.map_writer(|writer| ReadSignal::new(writer))
.into()
}
}
+8
View File
@@ -0,0 +1,8 @@
mod btreemap;
mod deref;
mod hashmap;
mod index;
mod option;
mod result;
mod slice;
mod vec;
+230
View File
@@ -0,0 +1,230 @@
use std::ops::DerefMut;
use crate::{store::Store, MappedStore};
use dioxus_signals::{Readable, ReadableExt};
impl<Lens: Readable<Target = Option<T>> + 'static, T: 'static> Store<Option<T>, 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::<i32>);
/// 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<Option<T>>` into a `Option<Store<T>>`. 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<MappedStore<T, Lens>> {
self.is_some().then(move || {
let map: fn(&Option<T>) -> &T = |value| {
value.as_ref().unwrap_or_else(|| {
panic!("Tried to access `Some` on an Option value");
})
};
let map_mut: fn(&mut Option<T>) -> &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<T>`. 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<T, Lens> {
self.transpose().unwrap()
}
/// Expects the `Option` to be `Some` and returns a `Store<T>`. 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<T, Lens> {
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>) -> &[T] = |value| value.as_slice();
let map_mut: fn(&mut Option<T>) -> &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<MappedStore<T::Target, Lens>>
where
T: DerefMut,
{
self.is_some().then(move || {
let map: fn(&Option<T>) -> &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<T>) -> &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<MappedStore<T, Lens>> {
self.is_some_and(f).then(move || {
let map: fn(&Option<T>) -> &T = |value| {
value.as_ref().unwrap_or_else(|| {
panic!("Tried to access `Some` on an Option value");
})
};
let map_mut: fn(&mut Option<T>) -> &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
}
}
+336
View File
@@ -0,0 +1,336 @@
use std::fmt::Debug;
use crate::{store::Store, MappedStore};
use dioxus_signals::{Readable, ReadableExt, Writable};
impl<Lens, T, E> Store<Result<T, E>, Lens>
where
Lens: Readable<Target = Result<T, E>> + '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::<u32, ()>(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::<u32, ()>(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<Result<T, E>>` into `Option<Store<T>>`, 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::<u32, ()>(42));
/// match store.ok() {
/// Some(ok_store) => assert_eq!(ok_store(), 42),
/// None => panic!("Expected Ok"),
/// }
/// ```
pub fn ok(self) -> Option<MappedStore<T, Lens>> {
let map: fn(&Result<T, E>) -> &T = |value| {
value.as_ref().unwrap_or_else(|_| {
panic!("Tried to access `ok` on an Err value");
})
};
let map_mut: fn(&mut Result<T, E>) -> &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<Result<T, E>>` into `Option<Store<E>>`, 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<MappedStore<E, Lens>>
where
Lens: Writable<Target = Result<T, E>> + 'static,
{
self.is_err().then(|| {
let map: fn(&Result<T, E>) -> &E = |value| match value {
Ok(_) => panic!("Tried to access `err` on an Ok value"),
Err(e) => e,
};
let map_mut: fn(&mut Result<T, E>) -> &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<Result<T, E>>` into a `Result<Store<T>, Store<E>>`. 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::<u32, ()>(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<T, Lens>, MappedStore<E, Lens>>
where
Lens: Writable<Target = Result<T, E>> + 'static,
{
if self.is_ok() {
let map: fn(&Result<T, E>) -> &T = |value| match value {
Ok(t) => t,
Err(_) => panic!("Tried to access `ok` on an Err value"),
};
let map_mut: fn(&mut Result<T, E>) -> &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<T, E>) -> &E = |value| match value {
Ok(_) => panic!("Tried to access `err` on an Ok value"),
Err(e) => e,
};
let map_mut: fn(&mut Result<T, E>) -> &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<T>`. 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::<u32, ()>(42));
/// let unwrapped = store.unwrap();
/// assert_eq!(unwrapped(), 42);
/// ```
pub fn unwrap(self) -> MappedStore<T, Lens>
where
Lens: Writable<Target = Result<T, E>> + 'static,
E: Debug,
{
self.transpose().unwrap()
}
/// Expects the `Result` to be `Ok` and returns a `Store<T>`. 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::<u32, ()>(42));
/// let unwrapped = store.expect("Expected Ok");
/// assert_eq!(unwrapped(), 42);
/// ```
pub fn expect(self, msg: &str) -> MappedStore<T, Lens>
where
Lens: Writable<Target = Result<T, E>> + '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<E, Lens>
where
Lens: Writable<Target = Result<T, E>> + 'static,
T: Debug,
{
self.transpose().unwrap_err()
}
/// Expects the `Result` to be `Err` and returns a `Store<E>`. 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<E, Lens>
where
Lens: Writable<Target = Result<T, E>> + '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::<u32, ()>(42)).inspect(|v| println!("{v}"));
/// ```
pub fn inspect(self, f: impl FnOnce(&T)) -> Self
where
Lens: Writable<Target = Result<T, E>> + '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<Target = Result<T, E>> + '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<u32>, ()>(Box::new(42)));
/// let derefed = store.as_deref().unwrap();
/// assert_eq!(derefed(), 42);
/// ```
pub fn as_deref(self) -> Result<MappedStore<T::Target, Lens>, MappedStore<E, Lens>>
where
Lens: Writable<Target = Result<T, E>> + 'static,
T: std::ops::DerefMut,
{
if self.is_ok() {
let map: fn(&Result<T, E>) -> &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<T, E>) -> &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<T, E>) -> &E = |value| match value {
Ok(_) => panic!("Tried to access `err` on an Ok value"),
Err(e) => e,
};
let map_mut: fn(&mut Result<T, E>) -> &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())
}
}
}
+60
View File
@@ -0,0 +1,60 @@
use std::iter::FusedIterator;
use crate::{impls::index::IndexWrite, store::Store};
use dioxus_signals::{Readable, ReadableExt};
impl<Lens, I> Store<Vec<I>, Lens>
where
Lens: Readable<Target = Vec<I>> + '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<Item = Store<I, IndexWrite<usize, Lens>>>
+ DoubleEndedIterator
+ FusedIterator
+ '_
where
Lens: Clone,
{
(0..self.len()).map(move |i| self.clone().index(i))
}
}
+88
View File
@@ -0,0 +1,88 @@
use crate::store::Store;
use dioxus_signals::Writable;
impl<Lens: Writable<Target = Vec<T>> + 'static, T: 'static> Store<Vec<T>, 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);
}
}
}
+21
View File
@@ -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;
}
+215
View File
@@ -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<Counter> {
/// fn count(
/// self,
/// ) -> dioxus_stores::Store<
/// i32,
/// dioxus_stores::macro_helpers::dioxus_signals::MappedMutSignal<i32, __W>,
/// > {
/// 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<Lens> {
path: TinyVec,
store: StoreSubscriptions,
write: Lens,
}
impl<Lens> Debug for SelectorScope<Lens> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("SelectorScope")
.field("path", &self.path)
.finish()
}
}
impl<Lens> Clone for SelectorScope<Lens>
where
Lens: Clone,
{
fn clone(&self) -> Self {
Self {
path: self.path,
store: self.store,
write: self.write.clone(),
}
}
}
impl<Lens> Copy for SelectorScope<Lens> where Lens: Copy {}
impl<Lens> SelectorScope<Lens> {
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<U: ?Sized, T, F, FMut>(
self,
index: &impl Hash,
map: F,
map_mut: FMut,
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
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<U: ?Sized, T, F, FMut>(
self,
index: PathKey,
map: F,
map_mut: FMut,
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
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<Lens> {
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<Lens> {
self.path.push(index);
self
}
/// Map the view into the writable data without creating a child selector scope
pub fn map<U: ?Sized, T, F, FMut>(
self,
map: F,
map_mut: FMut,
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
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<W2>(self, map: impl FnOnce(Lens) -> W2) -> SelectorScope<W2> {
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<Lens: Readable> Readable for SelectorScope<Lens> {
type Target = Lens::Target;
type Storage = Lens::Storage;
fn try_read_unchecked(&self) -> Result<ReadableRef<'static, Lens>, BorrowError> {
self.track();
self.write.try_read_unchecked()
}
fn try_peek_unchecked(&self) -> Result<ReadableRef<'static, Lens>, BorrowError> {
self.write.try_peek_unchecked()
}
fn subscribers(&self) -> Subscribers {
self.store.subscribers(&self.path)
}
}
impl<Lens: Writable> Writable for SelectorScope<Lens> {
type WriteMetadata = Lens::WriteMetadata;
fn try_write_unchecked(&self) -> Result<WritableRef<'static, Lens>, BorrowMutError> {
self.mark_dirty();
self.write.try_write_unchecked()
}
}
+408
View File
@@ -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(&<Lens as Readable>::Target) -> &T,
FMut = fn(&mut <Lens as Readable>::Target) -> &mut T,
> = Store<T, MappedMutSignal<T, Lens, F, FMut>>;
/// A type alias for a boxed read-only store.
pub type ReadStore<T> = Store<T, ReadSignal<T>>;
/// A type alias for a boxed writable-only store.
pub type WriteStore<T> = Store<T, WriteSignal<T>>;
/// 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<CounterTree>,
/// }
///
/// // The store macro generates an extension trait with additional methods for the store based on the impl block.
/// #[store]
/// impl<Lens> Store<CounterTree, Lens> {
/// // 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::<i32>()
/// }
/// }
///
/// fn app() -> Element {
/// let value = use_store(Default::default);
///
/// rsx! {
/// Tree {
/// value
/// }
/// }
/// }
///
/// #[component]
/// fn Tree(value: Store<CounterTree>) -> 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<T: ?Sized, Lens = WriteSignal<T>> {
selector: SelectorScope<Lens>,
_phantom: PhantomData<Box<T>>,
}
impl<T: 'static, S: Storage<T>> Store<T, CopyValue<T, S>> {
/// 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<T: 'static> Store<T> {
/// 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<T: ?Sized, Lens> Store<T, Lens> {
/// 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<Lens> {
&self.selector
}
/// Convert the store into the underlying selector
pub fn into_selector(self) -> SelectorScope<Lens> {
self.selector
}
}
impl<T: ?Sized, Lens> From<SelectorScope<Lens>> for Store<T, Lens> {
fn from(selector: SelectorScope<Lens>) -> Self {
Self {
selector,
_phantom: PhantomData,
}
}
}
impl<T: ?Sized, Lens> PartialEq for Store<T, Lens>
where
Lens: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.selector == other.selector
}
}
impl<T: ?Sized, Lens> Clone for Store<T, Lens>
where
Lens: Clone,
{
fn clone(&self) -> Self {
Self {
selector: self.selector.clone(),
_phantom: ::std::marker::PhantomData,
}
}
}
impl<T: ?Sized, Lens> Copy for Store<T, Lens> where Lens: Copy {}
impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From<MappedStore<T, Lens, __F, __FMut>>
for Store<T, WriteSignal<T>>
where
Lens: Writable<Storage = UnsyncStorage> + 'static,
__F: Fn(&Lens::Target) -> &T + 'static,
__FMut: Fn(&mut Lens::Target) -> &mut T + 'static,
T: 'static,
{
fn from(value: MappedStore<T, Lens, __F, __FMut>) -> Self {
Store {
selector: value.selector.map_writer(::std::convert::Into::into),
_phantom: ::std::marker::PhantomData,
}
}
}
impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From<MappedStore<T, Lens, __F, __FMut>>
for Store<T, ReadSignal<T>>
where
Lens: Writable<Storage = UnsyncStorage> + 'static,
__F: Fn(&Lens::Target) -> &T + 'static,
__FMut: Fn(&mut Lens::Target) -> &mut T + 'static,
T: 'static,
{
fn from(value: MappedStore<T, Lens, __F, __FMut>) -> Self {
Store {
selector: value.selector.map_writer(::std::convert::Into::into),
_phantom: ::std::marker::PhantomData,
}
}
}
impl<T> ::std::convert::From<Store<T>> for ReadStore<T>
where
T: ?Sized + 'static,
{
fn from(value: Store<T>) -> Self {
Self {
selector: value.selector.map_writer(::std::convert::Into::into),
_phantom: ::std::marker::PhantomData,
}
}
}
#[doc(hidden)]
pub struct SuperIntoReadSignalMarker;
impl<T, Lens> SuperInto<ReadSignal<T>, SuperIntoReadSignalMarker> for Store<T, Lens>
where
T: ?Sized + 'static,
Lens: Readable<Target = T, Storage = UnsyncStorage> + 'static,
{
fn super_into(self) -> ReadSignal<T> {
ReadSignal::new(self)
}
}
#[doc(hidden)]
pub struct SuperIntoWriteSignalMarker;
impl<T, Lens> SuperInto<WriteSignal<T>, SuperIntoWriteSignalMarker> for Store<T, Lens>
where
T: ?Sized + 'static,
Lens: Writable<Target = T, Storage = UnsyncStorage> + 'static,
{
fn super_into(self) -> WriteSignal<T> {
WriteSignal::new(self)
}
}
impl<T: ?Sized, Lens> Readable for Store<T, Lens>
where
Lens: Readable<Target = T>,
T: 'static,
{
type Storage = Lens::Storage;
type Target = T;
fn try_read_unchecked(&self) -> Result<ReadableRef<'static, Self>, BorrowError> {
self.selector.try_read_unchecked()
}
fn try_peek_unchecked(&self) -> Result<ReadableRef<'static, Self>, BorrowError> {
self.selector.try_peek_unchecked()
}
fn subscribers(&self) -> Subscribers {
self.selector.subscribers()
}
}
impl<T: ?Sized, Lens> Writable for Store<T, Lens>
where
Lens: Writable<Target = T>,
T: 'static,
{
type WriteMetadata = Lens::WriteMetadata;
fn try_write_unchecked(&self) -> Result<WritableRef<'static, Self>, BorrowMutError> {
self.selector.try_write_unchecked()
}
}
impl<T, Lens> IntoAttributeValue for Store<T, Lens>
where
Self: Readable<Target = T>,
T: ::std::clone::Clone + IntoAttributeValue + 'static,
{
fn into_value(self) -> AttributeValue {
ReadableExt::cloned(&self).into_value()
}
}
impl<T, Lens> IntoDynNode for Store<T, Lens>
where
Self: Readable<Target = T>,
T: ::std::clone::Clone + IntoDynNode + 'static,
{
fn into_dyn_node(self) -> DynamicNode {
ReadableExt::cloned(&self).into_dyn_node()
}
}
impl<T, Lens> ::std::ops::Deref for Store<T, Lens>
where
Self: Readable<Target = T> + 'static,
T: ::std::clone::Clone + 'static,
{
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
unsafe { ReadableExt::deref_impl(self) }
}
}
read_impls!(Store<T, Lens> where Lens: Readable<Target = T>);
write_impls!(Store<T, Lens> where Lens: Writable<Target = T>);
/// 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<CounterTree>`.
/// #[derive(Store, Default)]
/// struct CounterTree {
/// count: i32,
/// children: Vec<CounterTree>,
/// }
///
/// fn app() -> Element {
/// let value = use_store(Default::default);
///
/// rsx! {
/// Tree {
/// value
/// }
/// }
/// }
///
/// #[component]
/// fn Tree(value: Store<CounterTree>) -> 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<T: 'static>(init: impl FnOnce() -> T) -> Store<T> {
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<Counter> = Global::new(|| Counter { count: 0 });
///
/// fn app() -> Element {
/// let mut count = COUNTER.resolve().count();
///
/// rsx! {
/// button {
/// onclick: move |_| count += 1,
/// "{count}"
/// }
/// }
/// }
/// ```
pub type GlobalStore<T> = Global<Store<T>, T>;
impl<T: 'static> InitializeFromFunction<T> for Store<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Store::new(f())
}
}
+323
View File
@@ -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<ReactiveContext>,
root: HashMap<PathKey, SelectorNode>,
}
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<StoreSubscriptionsInner, SyncStorage>,
}
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);
}
}
+68
View File
@@ -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<str>) -> Element {
rsx! {
TakesStr {
item,
}
}
}
#[component]
fn TakesStrStore(item: ReadStore<str>) -> Element {
rsx! {
TakesStrStore {
item,
}
}
}
#[component]
fn TakesReadSignal(item: ReadSignal<TodoItem>) -> Element {
rsx! {
TakesReadSignal {
item,
}
}
}
#[component]
fn TakesReadStore(item: ReadStore<TodoItem>) -> Element {
rsx! {
TakesReadStore {
item,
}
}
}
+235
View File
@@ -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<TodoItem> {
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<bool, _> = store.checked();
let contents: Store<String, _> = 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<T> {
checked: bool,
contents: T,
}
#[store]
impl<T> Store<Item<T>> {
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<bool, _> = store.checked();
let contents: Store<String, _> = 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<Enum> {
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<T> {
Foo,
Bar(T),
Baz { foo: i32, bar: T },
FooBar(u32, T),
BarFoo { foo: T },
}
#[store]
impl<T> Store<Enum<T>> {
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();
}
}