diff --git a/examples/global.rs b/examples/global.rs index 401668d74..0a08485b0 100644 --- a/examples/global.rs +++ b/examples/global.rs @@ -10,7 +10,7 @@ use dioxus::prelude::*; const STYLE: &str = asset!("./examples/assets/counter.css"); static COUNT: GlobalSignal = Signal::global(|| 0); -static DOUBLED_COUNT: GlobalMemo = Signal::global_memo(|| COUNT() * 2); +static DOUBLED_COUNT: GlobalMemo = Memo::global(|| COUNT() * 2); fn main() { launch(app); @@ -52,7 +52,7 @@ fn Display() -> Element { fn Reset() -> Element { // Not all write methods are available on global signals since `write` requires a mutable reference. In these cases, // We can simply pull out the actual signal using the signal() method. - let mut as_signal = use_hook(|| COUNT.signal()); + let mut as_signal = use_hook(|| COUNT.resolve()); rsx! { button { onclick: move |_| as_signal.set(0), "Reset" } diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index ef0d2f868..248495f90 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -189,6 +189,6 @@ impl Deref for UseFuture { type Target = dyn Fn() -> UseFutureState; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index c3f031640..8c3c02de8 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -473,6 +473,6 @@ impl Deref for Resource { type Target = dyn Fn() -> Option; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/rsx/src/template_body.rs b/packages/rsx/src/template_body.rs index 4c647f88e..baa72b3d1 100644 --- a/packages/rsx/src/template_body.rs +++ b/packages/rsx/src/template_body.rs @@ -167,24 +167,23 @@ impl ToTokens for TemplateBody { } ); - __template.maybe_with_rt(|__template_read| { - // If the template has not been hot reloaded, we always use the original template - // Templates nested within macros may be merged because they have the same file-line-column-index - // They cannot be hot reloaded, so this prevents incorrect rendering - let __template_read = match __template_read.as_ref() { - Some(__template_read) => __template_read, - None => __original_template(), - }; - let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new( - vec![ #( #dynamic_text.to_string() ),* ], - ); - let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new( - vec![ #( #dynamic_nodes ),* ], - vec![ #( #dyn_attr_printer ),* ], - __dynamic_literal_pool - ); - __dynamic_value_pool.render_with(__template_read) - }) + // If the template has not been hot reloaded, we always use the original template + // Templates nested within macros may be merged because they have the same file-line-column-index + // They cannot be hot reloaded, so this prevents incorrect rendering + let __template_read = dioxus_core::Runtime::current().ok().map(|_| __template.read()); + let __template_read = match __template_read.as_ref().map(|__template_read| __template_read.as_ref()) { + Some(Some(__template_read)) => &__template_read, + _ => __original_template(), + }; + let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new( + vec![ #( #dynamic_text.to_string() ),* ], + ); + let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new( + vec![ #( #dynamic_nodes ),* ], + vec![ #( #dyn_attr_printer ),* ], + __dynamic_literal_pool + ); + __dynamic_value_pool.render_with(__template_read) } #[cfg(not(debug_assertions))] { diff --git a/packages/signals/src/copy_value.rs b/packages/signals/src/copy_value.rs index d59d4e057..6aa3f44bf 100644 --- a/packages/signals/src/copy_value.rs +++ b/packages/signals/src/copy_value.rs @@ -193,7 +193,7 @@ impl> Deref for CopyValue { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/signals/src/global/memo.rs b/packages/signals/src/global/memo.rs index 4bf875308..cdc775928 100644 --- a/packages/signals/src/global/memo.rs +++ b/packages/signals/src/global/memo.rs @@ -1,93 +1,26 @@ -use crate::{read::Readable, Memo, ReadableRef}; -use crate::{read_impls, GlobalKey}; -use dioxus_core::prelude::ScopeId; -use generational_box::{BorrowResult, UnsyncStorage}; -use std::ops::Deref; +use super::{Global, InitializeFromFunction}; +use crate::read::Readable; +use crate::read_impls; +use crate::Memo; -use crate::Signal; - -use super::get_global_context; - -/// A signal that can be accessed from anywhere in the application and created in a static -pub struct GlobalMemo { - selector: fn() -> T, - key: GlobalKey<'static>, +impl InitializeFromFunction for Memo { + fn initialize_from_function(f: fn() -> T) -> Self { + Memo::new(f) + } } +/// A memo that can be accessed from anywhere in the application and created in a static +pub type GlobalMemo = Global, T>; + impl GlobalMemo { - #[track_caller] - /// Create a new global signal - pub const fn new(selector: fn() -> T) -> GlobalMemo - where - T: PartialEq, - { - let key = std::panic::Location::caller(); - GlobalMemo { - selector, - key: GlobalKey::new(key), - } - } - - /// Get the key for this global - pub fn key(&self) -> GlobalKey<'static> { - self.key.clone() - } - - /// Get the signal that backs this global. - pub fn memo(&self) -> Memo { - let key = self.key(); - - let context = get_global_context(); - - let read = context.signal.borrow(); - match read.get(&key) { - Some(signal) => *signal.downcast_ref::>().unwrap(), - None => { - drop(read); - // Constructors are always run in the root scope - let signal = ScopeId::ROOT.in_runtime(|| Signal::memo(self.selector)); - context.signal.borrow_mut().insert(key, Box::new(signal)); - signal - } - } - } - - /// Get the scope the signal was created in. - pub fn origin_scope(&self) -> ScopeId { - ScopeId::ROOT - } - /// Get the generational id of the signal. pub fn id(&self) -> generational_box::GenerationalBoxId { - self.memo().id() - } -} - -impl Readable for GlobalMemo { - type Target = T; - type Storage = UnsyncStorage; - - #[track_caller] - fn try_read_unchecked( - &self, - ) -> Result, generational_box::BorrowError> { - self.memo().try_read_unchecked() + self.resolve().id() } - #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { - self.memo().try_peek_unchecked() - } -} - -/// Allow calling a signal with memo() syntax -/// -/// Currently only limited to copy types, though could probably specialize for string/arc/rc -impl Deref for GlobalMemo { - type Target = dyn Fn() -> T; - - fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + /// Resolve the global memo. 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. + pub fn memo(&self) -> Memo { + self.resolve() } } diff --git a/packages/signals/src/global/mod.rs b/packages/signals/src/global/mod.rs index 5b7fd6c42..e102fc3b9 100644 --- a/packages/signals/src/global/mod.rs +++ b/packages/signals/src/global/mod.rs @@ -1,5 +1,6 @@ -use dioxus_core::prelude::{provide_root_context, try_consume_context}; -use std::{any::Any, cell::RefCell, collections::HashMap, panic::Location, rc::Rc}; +use dioxus_core::ScopeId; +use generational_box::BorrowResult; +use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc}; mod memo; pub use memo::*; @@ -7,12 +8,182 @@ pub use memo::*; mod signal; pub use signal::*; -use crate::Signal; +use crate::{Readable, ReadableRef, Signal, Writable, WritableRef}; + +/// A trait for an item that can be constructed from an initialization function +pub trait InitializeFromFunction { + /// Create an instance of this type from an initialization function + fn initialize_from_function(f: fn() -> T) -> Self; +} + +impl InitializeFromFunction for T { + fn initialize_from_function(f: fn() -> T) -> Self { + f() + } +} + +/// A lazy value that is created once per application and can be accessed from anywhere in that application +pub struct Global { + constructor: fn() -> R, + key: GlobalKey<'static>, + phantom: std::marker::PhantomData T>, +} + +/// Allow calling a signal with signal() syntax +/// +/// Currently only limited to copy types, though could probably specialize for string/arc/rc +impl Deref for Global +where + T: Readable + InitializeFromFunction, +{ + type Target = dyn Fn() -> R; + + fn deref(&self) -> &Self::Target { + unsafe { Readable::deref_impl(self) } + } +} + +impl Readable for Global +where + T: Readable + InitializeFromFunction, +{ + type Target = R; + type Storage = T::Storage; + + #[track_caller] + fn try_read_unchecked( + &self, + ) -> Result, generational_box::BorrowError> { + self.resolve().try_read_unchecked() + } + + #[track_caller] + fn try_peek_unchecked(&self) -> BorrowResult> { + self.resolve().try_peek_unchecked() + } +} + +impl Writable for Global +where + T: Writable + InitializeFromFunction, +{ + type Mut<'a, Read: ?Sized + 'static> = T::Mut<'a, Read>; + + fn map_mut &mut U>( + ref_: Self::Mut<'_, I>, + f: F, + ) -> Self::Mut<'_, U> { + T::map_mut(ref_, f) + } + + fn try_map_mut< + I: ?Sized + 'static, + U: ?Sized + 'static, + F: FnOnce(&mut I) -> Option<&mut U>, + >( + ref_: Self::Mut<'_, I>, + f: F, + ) -> Option> { + T::try_map_mut(ref_, f) + } + + fn downcast_lifetime_mut<'a: 'b, 'b, Read: ?Sized + 'static>( + mut_: Self::Mut<'a, Read>, + ) -> Self::Mut<'b, Read> { + T::downcast_lifetime_mut(mut_) + } + + #[track_caller] + fn try_write_unchecked( + &self, + ) -> Result, generational_box::BorrowMutError> { + self.resolve().try_write_unchecked() + } +} + +impl Global +where + T: Writable + InitializeFromFunction, +{ + /// Write this value + pub fn write(&self) -> T::Mut<'static, R> { + self.resolve().try_write_unchecked().unwrap() + } + + /// Run a closure with a mutable reference to the signal's value. + /// If the signal has been dropped, this will panic. + #[track_caller] + pub fn with_mut(&self, f: impl FnOnce(&mut R) -> O) -> O { + self.resolve().with_mut(f) + } +} + +impl Global +where + T: InitializeFromFunction, +{ + #[track_caller] + /// Create a new global value + pub const fn new(constructor: fn() -> R) -> Self { + let key = std::panic::Location::caller(); + Self { + constructor, + key: GlobalKey::new(key), + phantom: std::marker::PhantomData, + } + } + + /// Create this global signal with a specific key. + /// This is useful for ensuring that the signal is unique across the application and accessible from + /// outside the application too. + #[track_caller] + pub const fn with_key(constructor: fn() -> R, key: &'static str) -> Self { + Self { + constructor, + key: GlobalKey::new_from_str(key), + phantom: std::marker::PhantomData, + } + } + + /// Get the key for this global + pub fn key(&self) -> GlobalKey<'static> { + self.key.clone() + } + + /// 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 { + let key = self.key(); + + let context = get_global_context(); + + // Get the entry if it already exists + { + let read = context.map.borrow(); + if let Some(signal) = read.get(&key) { + return signal.downcast_ref::().cloned().unwrap(); + } + } + // Otherwise, create it + // Constructors are always run in the root scope + let signal = ScopeId::ROOT.in_runtime(|| T::initialize_from_function(self.constructor)); + context + .map + .borrow_mut() + .insert(key, Box::new(signal.clone())); + signal + } + + /// Get the scope the signal was created in. + pub fn origin_scope(&self) -> ScopeId { + ScopeId::ROOT + } +} /// The context for global signals -#[derive(Clone)] -pub struct GlobalSignalContext { - signal: Rc, Box>>>, +#[derive(Clone, Default)] +pub struct GlobalLazyContext { + map: Rc, Box>>>, } /// A key used to identify a signal in the global signal context @@ -57,18 +228,18 @@ impl From<&'static Location<'static>> for GlobalKey<'static> { } } -impl GlobalSignalContext { +impl GlobalLazyContext { /// Get a signal with the given string key /// The key will be converted to a UUID with the appropriate internal namespace pub fn get_signal_with_key(&self, key: &str) -> Option> { let key = GlobalKey::new_from_str(key); - self.signal.borrow().get(&key).map(|f| { + self.map.borrow().get(&key).map(|f| { *f.downcast_ref::>().unwrap_or_else(|| { panic!( "Global signal with key {:?} is not of the expected type. Keys are {:?}", key, - self.signal.borrow().keys() + self.map.borrow().keys() ) }) }) @@ -76,15 +247,10 @@ impl GlobalSignalContext { } /// Get the global context for signals -pub fn get_global_context() -> GlobalSignalContext { - match try_consume_context() { +pub fn get_global_context() -> GlobalLazyContext { + match ScopeId::ROOT.has_context() { Some(context) => context, - None => { - let context = GlobalSignalContext { - signal: Rc::new(RefCell::new(HashMap::new())), - }; - provide_root_context(context) - } + None => ScopeId::ROOT.provide_context(Default::default()), } } diff --git a/packages/signals/src/global/signal.rs b/packages/signals/src/global/signal.rs index 09b4fae2a..66063f414 100644 --- a/packages/signals/src/global/signal.rs +++ b/packages/signals/src/global/signal.rs @@ -1,171 +1,26 @@ -use crate::{read::Readable, ReadableRef}; -use crate::{write::Writable, GlobalKey}; -use crate::{WritableRef, Write}; -use dioxus_core::{prelude::ScopeId, Runtime}; -use generational_box::{BorrowResult, UnsyncStorage}; -use std::ops::Deref; - -use super::get_global_context; +use super::{Global, InitializeFromFunction}; +use crate::read::Readable; use crate::read_impls; use crate::Signal; -/// A signal that can be accessed from anywhere in the application and created in a static -pub struct GlobalSignal { - initializer: fn() -> T, - key: GlobalKey<'static>, - created_at: &'static std::panic::Location<'static>, +impl InitializeFromFunction for Signal { + fn initialize_from_function(f: fn() -> T) -> Self { + Signal::new(f()) + } } +/// A signal that can be accessed from anywhere in the application and created in a static +pub type GlobalSignal = Global, T>; + impl GlobalSignal { - /// Create a new global signal with the given initializer. - #[track_caller] - pub const fn new(initializer: fn() -> T) -> GlobalSignal { - let key = std::panic::Location::caller(); - GlobalSignal { - initializer, - key: GlobalKey::new(key), - created_at: key, - } - } - - /// Get the key for this global - pub fn key(&self) -> GlobalKey<'static> { - self.key.clone() - } - - /// Create this global signal with a specific key. - /// This is useful for ensuring that the signal is unique across the application and accessible from - /// outside the application too. - #[track_caller] - pub const fn with_key(initializer: fn() -> T, key: &'static str) -> GlobalSignal { - GlobalSignal { - initializer, - key: GlobalKey::new_from_str(key), - created_at: std::panic::Location::caller(), - } - } - - /// Get the signal that backs this . - pub fn signal(&self) -> Signal { - let key = self.key(); - let context = get_global_context(); - - let read = context.signal.borrow(); - - match read.get(&key) { - Some(signal) => *signal.downcast_ref::>().unwrap(), - None => { - drop(read); - - // Constructors are always run in the root scope - // The signal also exists in the root scope - let value = ScopeId::ROOT.in_runtime(self.initializer); - let signal = Signal::new_maybe_sync_in_scope_with_caller( - value, - ScopeId::ROOT, - self.created_at, - ); - - let entry = context.signal.borrow_mut().insert(key, Box::new(signal)); - debug_assert!(entry.is_none(), "Global signal already exists"); - - signal - } - } - } - - #[doc(hidden)] - pub fn maybe_with_rt(&self, f: impl FnOnce(&T) -> O) -> O { - if Runtime::current().is_err() { - f(&(self.initializer)()) - } else { - self.with(f) - } - } - - /// Write this value - pub fn write(&self) -> Write<'static, T, UnsyncStorage> { - self.signal().try_write_unchecked().unwrap() - } - - /// Get the scope the signal was created in. - pub fn origin_scope(&self) -> ScopeId { - ScopeId::ROOT - } - - /// Run a closure with a mutable reference to the signal's value. - /// If the signal has been dropped, this will panic. - #[track_caller] - pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { - self.signal().with_mut(f) - } - /// Get the generational id of the signal. pub fn id(&self) -> generational_box::GenerationalBoxId { - self.signal().id() - } -} - -impl Readable for GlobalSignal { - type Target = T; - type Storage = UnsyncStorage; - - #[track_caller] - fn try_read_unchecked( - &self, - ) -> Result, generational_box::BorrowError> { - self.signal().try_read_unchecked() + self.resolve().id() } - #[track_caller] - fn try_peek_unchecked(&self) -> BorrowResult> { - self.signal().try_peek_unchecked() - } -} - -impl Writable for GlobalSignal { - type Mut<'a, R: ?Sized + 'static> = Write<'a, R, UnsyncStorage>; - - fn map_mut &mut U>( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Self::Mut<'_, U> { - Write::map(ref_, f) - } - - fn try_map_mut< - I: ?Sized + 'static, - U: ?Sized + 'static, - F: FnOnce(&mut I) -> Option<&mut U>, - >( - ref_: Self::Mut<'_, I>, - f: F, - ) -> Option> { - Write::filter_map(ref_, f) - } - - fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>( - mut_: Self::Mut<'a, R>, - ) -> Self::Mut<'b, R> { - Write::downcast_lifetime(mut_) - } - - #[track_caller] - fn try_write_unchecked( - &self, - ) -> Result, generational_box::BorrowMutError> { - self.signal().try_write_unchecked() - } -} - -/// Allow calling a signal with signal() syntax -/// -/// Currently only limited to copy types, though could probably specialize for string/arc/rc -impl Deref for GlobalSignal { - type Target = dyn Fn() -> T; - - fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + /// Resolve the global signal. 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. + pub fn signal(&self) -> Signal { + self.resolve() } } diff --git a/packages/signals/src/map.rs b/packages/signals/src/map.rs index 06e0feb4d..05949edf2 100644 --- a/packages/signals/src/map.rs +++ b/packages/signals/src/map.rs @@ -88,7 +88,7 @@ where type Target = dyn Fn() -> O; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/signals/src/memo.rs b/packages/signals/src/memo.rs index 60b5ea61f..2e9bbe793 100644 --- a/packages/signals/src/memo.rs +++ b/packages/signals/src/memo.rs @@ -1,6 +1,6 @@ -use crate::read_impls; use crate::write::Writable; use crate::{read::Readable, ReadableRef, Signal}; +use crate::{read_impls, GlobalMemo}; use crate::{CopyValue, ReadOnlySignal}; use std::{ cell::RefCell, @@ -92,6 +92,39 @@ impl Memo { memo } + /// Creates a new [`GlobalMemo`] that can be used anywhere inside your dioxus app. This memo will automatically be created once per app the first time you use it. + /// + /// # Example + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// static SIGNAL: GlobalSignal = Signal::global(|| 0); + /// // Create a new global memo that can be used anywhere in your app + /// static DOUBLED: GlobalMemo = Memo::global(|| SIGNAL() * 2); + /// + /// fn App() -> Element { + /// rsx! { + /// button { + /// // When SIGNAL changes, the memo will update because the SIGNAL is read inside DOUBLED + /// onclick: move |_| *SIGNAL.write() += 1, + /// "{DOUBLED}" + /// } + /// } + /// } + /// ``` + /// + ///
+ /// + /// Global memos are generally not recommended for use in libraries because it makes it more difficult to allow multiple instances of components you define in your library. + /// + ///
+ #[track_caller] + pub const fn global(constructor: fn() -> T) -> GlobalMemo + where + T: PartialEq, + { + GlobalMemo::new(constructor) + } + /// Rerun the computation and update the value of the memo if the result has changed. #[tracing::instrument(skip(self))] fn recompute(&self) @@ -200,7 +233,7 @@ where type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/signals/src/read.rs b/packages/signals/src/read.rs index 4ac8482fb..baae79773 100644 --- a/packages/signals/src/read.rs +++ b/packages/signals/src/read.rs @@ -221,8 +221,11 @@ pub trait Readable { ::map(self.read(), |v| v.index(index)) } + /// SAFETY: You must call this function directly with `self` as the argument. + /// This function relies on the size of the object you return from the deref + /// being the same as the object you pass in #[doc(hidden)] - fn deref_impl<'a>(&self) -> &'a dyn Fn() -> Self::Target + unsafe fn deref_impl<'a>(&self) -> &'a dyn Fn() -> Self::Target where Self: Sized + 'a, Self::Target: Clone, diff --git a/packages/signals/src/read_only_signal.rs b/packages/signals/src/read_only_signal.rs index 5399d0858..4879dffa5 100644 --- a/packages/signals/src/read_only_signal.rs +++ b/packages/signals/src/read_only_signal.rs @@ -132,7 +132,7 @@ impl> + 'static> Deref for ReadOnlySignal T; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index f6d19d17f..65f6cd85b 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,4 +1,4 @@ -use crate::{default_impl, fmt_impls, write_impls}; +use crate::{default_impl, fmt_impls, write_impls, Global}; use crate::{read::*, write::*, CopyValue, GlobalMemo, GlobalSignal, ReadableRef}; use crate::{Memo, WritableRef}; use dioxus_core::prelude::*; @@ -87,7 +87,7 @@ impl Signal { /// #[track_caller] pub const fn global(constructor: fn() -> T) -> GlobalSignal { - GlobalSignal::new(constructor) + Global::new(constructor) } } @@ -118,7 +118,10 @@ impl Signal { /// /// #[track_caller] - pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo { + pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo + where + T: PartialEq, + { GlobalMemo::new(constructor) } @@ -462,7 +465,7 @@ impl> + 'static> Deref for Signal { type Target = dyn Fn() -> T; fn deref(&self) -> &Self::Target { - Readable::deref_impl(self) + unsafe { Readable::deref_impl(self) } } }