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        let mut evicted_stale_entry = false;
186        {
187            let read = context.map.borrow();
188            if let Some(signal) = read.get(&key) {
189                if let Some(signal) = signal.downcast_ref::<T>() {
190                    return signal.clone();
191                }
192                evicted_stale_entry = true;
193            }
194        }
195
196        if evicted_stale_entry {
197            context.map.borrow_mut().remove(&key);
198        }
199        // Otherwise, create it
200        // Constructors are always run in the root scope
201        let signal = dioxus_core::Runtime::current().in_scope(ScopeId::ROOT, || {
202            T::initialize_from_function(self.constructor)
203        });
204        context
205            .map
206            .borrow_mut()
207            .insert(key, Box::new(signal.clone()));
208        signal
209    }
210
211    /// Get the scope the signal was created in.
212    pub fn origin_scope(&self) -> ScopeId {
213        ScopeId::ROOT
214    }
215}
216
217/// The context for global signals
218#[derive(Clone, Default)]
219pub struct GlobalLazyContext {
220    map: Rc<RefCell<HashMap<GlobalKey<'static>, Box<dyn Any>>>>,
221}
222
223/// A key used to identify a signal in the global signal context
224#[derive(Clone, Debug, PartialEq, Eq, Hash)]
225pub enum GlobalKey<'a> {
226    /// A key derived from a `std::panic::Location` type
227    File {
228        /// The file name
229        file: &'a str,
230
231        /// The line number
232        line: u32,
233
234        /// The column number
235        column: u32,
236
237        /// The index of the signal in the file - used to disambiguate macro calls
238        index: u32,
239    },
240
241    /// A raw key derived just from a string
242    Raw(&'a str),
243}
244
245impl<'a> GlobalKey<'a> {
246    /// Create a new key from a location
247    pub const fn new(key: &'a Location<'a>) -> Self {
248        GlobalKey::File {
249            file: key.file(),
250            line: key.line(),
251            column: key.column(),
252            index: 0,
253        }
254    }
255}
256
257impl From<&'static Location<'static>> for GlobalKey<'static> {
258    fn from(key: &'static Location<'static>) -> Self {
259        Self::new(key)
260    }
261}
262
263impl GlobalLazyContext {
264    /// Get a signal with the given string key
265    /// The key will be converted to a UUID with the appropriate internal namespace
266    pub fn get_signal_with_key<T: 'static>(&self, key: GlobalKey) -> Option<Signal<T>> {
267        self.map.borrow().get(&key).map(|f| {
268            *f.downcast_ref::<Signal<T>>().unwrap_or_else(|| {
269                panic!(
270                    "Global signal with key {:?} is not of the expected type. Keys are {:?}",
271                    key,
272                    self.map.borrow().keys()
273                )
274            })
275        })
276    }
277
278    #[doc(hidden)]
279    /// Clear all global signals of a given type.
280    pub fn clear<T: 'static>(&self) {
281        self.map.borrow_mut().retain(|_k, v| !v.is::<T>());
282    }
283}
284
285/// Get the global context for signals
286pub fn get_global_context() -> GlobalLazyContext {
287    let rt = Runtime::current();
288    match rt.has_context(ScopeId::ROOT) {
289        Some(context) => context,
290        None => rt.provide_context(ScopeId::ROOT, Default::default()),
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    /// Test that keys of global signals are correctly generated and different from one another.
299    /// We don't want signals to merge, but we also want them to use both string IDs and memory addresses.
300    #[test]
301    fn test_global_keys() {
302        // we're using consts since it's harder than statics due to merging - these won't be merged
303        const MYSIGNAL: GlobalSignal<i32> = GlobalSignal::new(|| 42);
304        const MYSIGNAL2: GlobalSignal<i32> = GlobalSignal::new(|| 42);
305        const MYSIGNAL3: GlobalSignal<i32> = GlobalSignal::with_name(|| 42, "custom-keyed");
306
307        let a = MYSIGNAL.key();
308        let b = MYSIGNAL.key();
309        let c = MYSIGNAL.key();
310        assert_eq!(a, b);
311        assert_eq!(b, c);
312
313        let d = MYSIGNAL2.key();
314        assert_ne!(a, d);
315
316        let e = MYSIGNAL3.key();
317        assert_ne!(a, e);
318    }
319}