diff --git a/examples/global.rs b/examples/global.rs index 662da21f7..947d839c1 100644 --- a/examples/global.rs +++ b/examples/global.rs @@ -5,6 +5,7 @@ use dioxus::prelude::*; static COUNT: GlobalSignal = Signal::global(|| 0); +static DOUBLED_COUNT: GlobalSelector = Signal::global_selector(|| COUNT() * 2); fn main() { launch_desktop(app); @@ -12,7 +13,7 @@ fn main() { fn app() -> Element { rsx! { - h1 { "High-Five counter: {COUNT}" } + h1 { "{COUNT} x 2 = {DOUBLED_COUNT}" } button { onclick: move |_| *COUNT.write() += 1, "Up high!" } button { onclick: move |_| *COUNT.write() -= 1, "Down low!" } } diff --git a/packages/signals/src/global.rs b/packages/signals/src/global.rs index fe0f77f86..0c81f8077 100644 --- a/packages/signals/src/global.rs +++ b/packages/signals/src/global.rs @@ -13,7 +13,7 @@ use dioxus_core::{ }; use generational_box::{GenerationalRef, GenerationalRefMut}; -use crate::{MappedSignal, Signal, Write}; +use crate::{selector, MappedSignal, ReadOnlySignal, Signal, Write}; /// A signal that can be accessed from anywhere in the application and created in a static pub struct GlobalSignal { @@ -48,15 +48,20 @@ impl GlobalSignal { let key = self as *const _ as *const (); let context = get_global_context(); - let mut write = context.signal.borrow_mut(); - let signal = write.entry(key).or_insert_with(|| { - // Constructors are always run in the root scope - let value = ScopeId::ROOT.in_runtime(self.initializer); - let signal = Signal::new(value); - Box::new(signal) - }); - *signal.downcast_ref::>().unwrap() + 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 value = ScopeId::ROOT.in_runtime(self.initializer); + let signal = Signal::new(value); + context.signal.borrow_mut().insert(key, Box::new(signal)); + signal + } + } } /// Get the scope the signal was created in. @@ -193,3 +198,129 @@ impl Deref for GlobalSignal { reference_to_closure as &Self::Target } } + +/// A signal that can be accessed from anywhere in the application and created in a static +pub struct GlobalSelector { + selector: fn() -> T, +} + +impl GlobalSelector { + /// Create a new global signal + pub const fn new(selector: fn() -> T) -> GlobalSelector + where + T: PartialEq, + { + GlobalSelector { selector } + } + + /// Get the signal that backs this global. + pub fn signal(&self) -> ReadOnlySignal { + let key = self as *const _ as *const (); + + 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(|| selector(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 current value of the signal. This will subscribe the current scope to the signal. If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead. + /// + /// If the signal has been dropped, this will panic. + #[track_caller] + pub fn read(&self) -> GenerationalRef> { + self.signal().read() + } + + /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.** + /// + /// If the signal has been dropped, this will panic. + pub fn peek(&self) -> GenerationalRef> { + self.signal().peek() + } + + /// Run a closure with a reference to the signal's value. + /// If the signal has been dropped, this will panic. + #[track_caller] + pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { + self.signal().with(f) + } + + /// Get the generational id of the signal. + pub fn id(&self) -> generational_box::GenerationalBoxId { + self.signal().id() + } +} + +impl IntoAttributeValue for GlobalSelector +where + T: Clone + IntoAttributeValue, +{ + fn into_value(self) -> dioxus_core::AttributeValue { + self.signal().into_value() + } +} + +impl GlobalSelector { + /// Get the current value of the signal. This will subscribe the current scope to the signal. + /// If the signal has been dropped, this will panic. + #[track_caller] + pub fn cloned(&self) -> T { + self.read().clone() + } +} + +impl PartialEq for GlobalSelector { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self, other) + } +} + +/// Allow calling a signal with signal() syntax +/// +/// Currently only limited to copy types, though could probably specialize for string/arc/rc +impl Deref for GlobalSelector { + type Target = dyn Fn() -> T; + + fn deref(&self) -> &Self::Target { + // https://github.com/dtolnay/case-studies/tree/master/callable-types + + // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). + let uninit_callable = MaybeUninit::::uninit(); + // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. + let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }).clone(); + + // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. + let size_of_closure = std::mem::size_of_val(&uninit_closure); + assert_eq!(size_of_closure, std::mem::size_of::()); + + // Then cast the lifetime of the closure to the lifetime of &self. + fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { + b + } + let reference_to_closure = cast_lifetime( + { + // The real closure that we will never use. + &uninit_closure + }, + // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &self. + unsafe { std::mem::transmute(self) }, + ); + + // Cast the closure to a trait object. + reference_to_closure as &Self::Target + } +} diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 856b41056..f21bccf25 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -1,6 +1,6 @@ use crate::rt::CopyValue; use crate::signal::{ReadOnlySignal, Signal, Write}; -use crate::{GlobalSignal, SignalData}; +use crate::{GlobalSelector, GlobalSignal, SignalData}; use generational_box::{GenerationalRef, Mappable}; use generational_box::{MappableMut, Storage}; @@ -11,7 +11,7 @@ use std::{ }; macro_rules! read_impls { - ($ty:ident $(, $bound_ty:ident : $bound:path, $vec_bound_ty:ident : $vec_bound:path)?) => { + ($ty:ident $(: $extra_bounds:path)? $(, $bound_ty:ident : $bound:path, $vec_bound_ty:ident : $vec_bound:path)?) => { $( impl Default for $ty { #[track_caller] @@ -21,37 +21,37 @@ macro_rules! read_impls { } )? - impl std::clone::Clone for $ty { + impl std::clone::Clone for $ty { #[track_caller] fn clone(&self) -> Self { *self } } - impl Copy for $ty {} + impl Copy for $ty {} - impl Display for $ty { + impl Display for $ty { #[track_caller] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.with(|v| Display::fmt(v, f)) } } - impl Debug for $ty { + impl Debug for $ty { #[track_caller] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.with(|v| Debug::fmt(v, f)) } } - impl PartialEq for $ty { + impl PartialEq for $ty { #[track_caller] fn eq(&self, other: &T) -> bool { self.with(|v| *v == *other) } } - impl $ty, $($vec_bound_ty)?> { + impl $ty, $($vec_bound_ty)?> { /// Returns the length of the inner vector. #[track_caller] pub fn len(&self) -> usize { @@ -414,6 +414,30 @@ impl GlobalSignal> { } } +read_impls!(GlobalSelector: PartialEq); + +impl GlobalSelector> { + /// Read a value from the inner vector. + pub fn get(&'static self, index: usize) -> Option>> { + GenerationalRef::, Ref<'static, Vec>>::try_map(self.read(), move |v| v.get(index)) + } +} + +impl GlobalSelector> { + /// Unwraps the inner value and clones it. + pub fn unwrap(&'static self) -> T + where + T: Clone, + { + self.with(|v| v.clone()).unwrap() + } + + /// Attempts to read the inner value of the Option. + pub fn as_ref(&'static self) -> Option>> { + GenerationalRef::, Ref<'static, Option>>::try_map(self.read(), |v| v.as_ref()) + } +} + /// An iterator over the values of a `CopyValue>`. pub struct CopyValueIterator>> { index: usize, diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 74955d586..ff34a75d2 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,4 +1,4 @@ -use crate::{GlobalSignal, MappedSignal}; +use crate::{GlobalSelector, GlobalSignal, MappedSignal}; use std::{ cell::RefCell, marker::PhantomData, @@ -225,6 +225,14 @@ impl Signal { pub const fn global(constructor: fn() -> T) -> GlobalSignal { GlobalSignal::new(constructor) } + + /// Creates a new global Signal that can be used in a global static. + pub const fn global_selector(constructor: fn() -> T) -> GlobalSelector + where + T: PartialEq, + { + GlobalSelector::new(constructor) + } } impl>> Signal { @@ -626,6 +634,20 @@ impl>> ReadOnlySignal { pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { self.inner.with(f) } + + /// Get the id of the signal. + pub fn id(&self) -> generational_box::GenerationalBoxId { + self.inner.id() + } +} + +impl IntoAttributeValue for ReadOnlySignal +where + T: Clone + IntoAttributeValue, +{ + fn into_value(self) -> dioxus_core::AttributeValue { + self.with(|f| f.clone().into_value()) + } } impl>> ReadOnlySignal {