dioxus_signals/global/
mod.rs

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