dioxus_signals/global/
mod.rs

1use dioxus_core::ScopeId;
2use generational_box::BorrowResult;
3use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc};
4
5mod memo;
6pub use memo::*;
7
8mod signal;
9pub use signal::*;
10
11use crate::{Readable, ReadableRef, Signal, Writable, WritableRef};
12
13/// A trait for an item that can be constructed from an initialization function
14pub trait InitializeFromFunction<T> {
15    /// Create an instance of this type from an initialization function
16    fn initialize_from_function(f: fn() -> T) -> Self;
17}
18
19impl<T> InitializeFromFunction<T> for T {
20    fn initialize_from_function(f: fn() -> T) -> Self {
21        f()
22    }
23}
24
25/// A lazy value that is created once per application and can be accessed from anywhere in that application
26pub struct Global<T, R = T> {
27    constructor: fn() -> R,
28    key: GlobalKey<'static>,
29    phantom: std::marker::PhantomData<fn() -> T>,
30}
31
32/// Allow calling a signal with signal() syntax
33///
34/// Currently only limited to copy types, though could probably specialize for string/arc/rc
35impl<T: Clone + 'static, R: Clone + 'static> Deref for Global<T, R>
36where
37    T: Readable<Target = R> + InitializeFromFunction<R>,
38{
39    type Target = dyn Fn() -> R;
40
41    fn deref(&self) -> &Self::Target {
42        unsafe { Readable::deref_impl(self) }
43    }
44}
45
46impl<T: Clone + 'static, R: 'static> Readable for Global<T, R>
47where
48    T: Readable<Target = R> + InitializeFromFunction<R>,
49{
50    type Target = R;
51    type Storage = T::Storage;
52
53    #[track_caller]
54    fn try_read_unchecked(
55        &self,
56    ) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
57        self.resolve().try_read_unchecked()
58    }
59
60    #[track_caller]
61    fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
62        self.resolve().try_peek_unchecked()
63    }
64}
65
66impl<T: Clone + 'static, R: 'static> Writable for Global<T, R>
67where
68    T: Writable<Target = R> + InitializeFromFunction<R>,
69{
70    type Mut<'a, Read: ?Sized + 'static> = T::Mut<'a, Read>;
71
72    fn map_mut<I: ?Sized, U: ?Sized + 'static, F: FnOnce(&mut I) -> &mut U>(
73        ref_: Self::Mut<'_, I>,
74        f: F,
75    ) -> Self::Mut<'_, U> {
76        T::map_mut(ref_, f)
77    }
78
79    fn try_map_mut<
80        I: ?Sized + 'static,
81        U: ?Sized + 'static,
82        F: FnOnce(&mut I) -> Option<&mut U>,
83    >(
84        ref_: Self::Mut<'_, I>,
85        f: F,
86    ) -> Option<Self::Mut<'_, U>> {
87        T::try_map_mut(ref_, f)
88    }
89
90    fn downcast_lifetime_mut<'a: 'b, 'b, Read: ?Sized + 'static>(
91        mut_: Self::Mut<'a, Read>,
92    ) -> Self::Mut<'b, Read> {
93        T::downcast_lifetime_mut(mut_)
94    }
95
96    #[track_caller]
97    fn try_write_unchecked(
98        &self,
99    ) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
100        self.resolve().try_write_unchecked()
101    }
102}
103
104impl<T: Clone + 'static, R: 'static> Global<T, R>
105where
106    T: Writable<Target = R> + InitializeFromFunction<R>,
107{
108    /// Write this value
109    pub fn write(&self) -> T::Mut<'static, R> {
110        self.resolve().try_write_unchecked().unwrap()
111    }
112
113    /// Run a closure with a mutable reference to the signal's value.
114    /// If the signal has been dropped, this will panic.
115    #[track_caller]
116    pub fn with_mut<O>(&self, f: impl FnOnce(&mut R) -> O) -> O {
117        self.resolve().with_mut(f)
118    }
119}
120
121impl<T: Clone + 'static, R> Global<T, R>
122where
123    T: InitializeFromFunction<R>,
124{
125    #[track_caller]
126    /// Create a new global value
127    pub const fn new(constructor: fn() -> R) -> Self {
128        let key = std::panic::Location::caller();
129        Self {
130            constructor,
131            key: GlobalKey::new(key),
132            phantom: std::marker::PhantomData,
133        }
134    }
135
136    /// Create this global signal with a specific key.
137    /// This is useful for ensuring that the signal is unique across the application and accessible from
138    /// outside the application too.
139    #[track_caller]
140    pub const fn with_name(constructor: fn() -> R, key: &'static str) -> Self {
141        Self {
142            constructor,
143            key: GlobalKey::File {
144                file: key,
145                line: 0,
146                column: 0,
147                index: 0,
148            },
149            phantom: std::marker::PhantomData,
150        }
151    }
152
153    /// Create this global signal with a specific key.
154    /// This is useful for ensuring that the signal is unique across the application and accessible from
155    /// outside the application too.
156    #[track_caller]
157    pub const fn with_location(
158        constructor: fn() -> R,
159        file: &'static str,
160        line: u32,
161        column: u32,
162        index: usize,
163    ) -> Self {
164        Self {
165            constructor,
166            key: GlobalKey::File {
167                file,
168                line: line as _,
169                column: column as _,
170                index: index as _,
171            },
172            phantom: std::marker::PhantomData,
173        }
174    }
175
176    /// Get the key for this global
177    pub fn key(&self) -> GlobalKey<'static> {
178        self.key.clone()
179    }
180
181    /// 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.
182    // NOTE: This is not called "get" or "value" because those methods overlap with Readable and Writable
183    pub fn resolve(&self) -> T {
184        let key = self.key();
185
186        let context = get_global_context();
187
188        // Get the entry if it already exists
189        {
190            let read = context.map.borrow();
191            if let Some(signal) = read.get(&key) {
192                return signal.downcast_ref::<T>().cloned().unwrap();
193            }
194        }
195        // Otherwise, create it
196        // Constructors are always run in the root scope
197        let signal = ScopeId::ROOT.in_runtime(|| T::initialize_from_function(self.constructor));
198        context
199            .map
200            .borrow_mut()
201            .insert(key, Box::new(signal.clone()));
202        signal
203    }
204
205    /// Get the scope the signal was created in.
206    pub fn origin_scope(&self) -> ScopeId {
207        ScopeId::ROOT
208    }
209}
210
211/// The context for global signals
212#[derive(Clone, Default)]
213pub struct GlobalLazyContext {
214    map: Rc<RefCell<HashMap<GlobalKey<'static>, Box<dyn Any>>>>,
215}
216
217/// A key used to identify a signal in the global signal context
218#[derive(Clone, Debug, PartialEq, Eq, Hash)]
219pub enum GlobalKey<'a> {
220    /// A key derived from a `std::panic::Location` type
221    File {
222        /// The file name
223        file: &'a str,
224
225        /// The line number
226        line: u32,
227
228        /// The column number
229        column: u32,
230
231        /// The index of the signal in the file - used to disambiguate macro calls
232        index: u32,
233    },
234
235    /// A raw key derived just from a string
236    Raw(&'a str),
237}
238
239impl<'a> GlobalKey<'a> {
240    /// Create a new key from a location
241    pub const fn new(key: &'a Location<'a>) -> Self {
242        GlobalKey::File {
243            file: key.file(),
244            line: key.line(),
245            column: key.column(),
246            index: 0,
247        }
248    }
249}
250
251impl From<&'static Location<'static>> for GlobalKey<'static> {
252    fn from(key: &'static Location<'static>) -> Self {
253        Self::new(key)
254    }
255}
256
257impl GlobalLazyContext {
258    /// Get a signal with the given string key
259    /// The key will be converted to a UUID with the appropriate internal namespace
260    pub fn get_signal_with_key<T>(&self, key: GlobalKey) -> Option<Signal<T>> {
261        self.map.borrow().get(&key).map(|f| {
262            *f.downcast_ref::<Signal<T>>().unwrap_or_else(|| {
263                panic!(
264                    "Global signal with key {:?} is not of the expected type. Keys are {:?}",
265                    key,
266                    self.map.borrow().keys()
267                )
268            })
269        })
270    }
271
272    #[doc(hidden)]
273    /// Clear all global signals of a given type.
274    pub fn clear<T: 'static>(&self) {
275        self.map.borrow_mut().retain(|_k, v| !v.is::<T>());
276    }
277}
278
279/// Get the global context for signals
280pub fn get_global_context() -> GlobalLazyContext {
281    match ScopeId::ROOT.has_context() {
282        Some(context) => context,
283        None => ScopeId::ROOT.provide_context(Default::default()),
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    /// Test that keys of global signals are correctly generated and different from one another.
292    /// We don't want signals to merge, but we also want them to use both string IDs and memory addresses.
293    #[test]
294    fn test_global_keys() {
295        // we're using consts since it's harder than statics due to merging - these won't be merged
296        const MYSIGNAL: GlobalSignal<i32> = GlobalSignal::new(|| 42);
297        const MYSIGNAL2: GlobalSignal<i32> = GlobalSignal::new(|| 42);
298        const MYSIGNAL3: GlobalSignal<i32> = GlobalSignal::with_name(|| 42, "custom-keyed");
299
300        let a = MYSIGNAL.key();
301        let b = MYSIGNAL.key();
302        let c = MYSIGNAL.key();
303        assert_eq!(a, b);
304        assert_eq!(b, c);
305
306        let d = MYSIGNAL2.key();
307        assert_ne!(a, d);
308
309        let e = MYSIGNAL3.key();
310        assert_ne!(a, e);
311    }
312}