1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
use dioxus_core::prelude::{
current_scope_id, has_context, provide_context, schedule_update_any, ScopeId,
};
use futures_channel::mpsc::UnboundedReceiver;
use generational_box::SyncStorage;
use std::{cell::RefCell, hash::Hash};
use crate::{CopyValue, Writable};
/// A context for signal reads and writes to be directed to
///
/// When a signal calls .read(), it will look for the current ReactiveContext to read from.
/// If it doesn't find it, then it will try and insert a context into the nearest component scope via context api.
///
/// When the ReactiveContext drops, it will remove itself from the associated contexts attached to signal
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ReactiveContext {
inner: CopyValue<Inner, SyncStorage>,
}
thread_local! {
static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
}
impl std::fmt::Display for ReactiveContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(debug_assertions)]
{
use crate::Readable;
if let Ok(read) = self.inner.try_read() {
return write!(f, "ReactiveContext created at {}", read.origin);
}
}
write!(f, "ReactiveContext")
}
}
impl ReactiveContext {
/// Create a new reactive context
#[track_caller]
pub fn new() -> (Self, UnboundedReceiver<()>) {
Self::new_with_origin(std::panic::Location::caller())
}
/// Create a new reactive context with a location for debugging purposes
/// This is useful for reactive contexts created within closures
pub fn new_with_origin(
origin: &'static std::panic::Location<'static>,
) -> (Self, UnboundedReceiver<()>) {
let (tx, rx) = futures_channel::mpsc::unbounded();
let callback = move || {
let _ = tx.unbounded_send(());
};
let _self = Self::new_with_callback(callback, current_scope_id().unwrap(), origin);
(_self, rx)
}
/// Create a new reactive context that may update a scope. When any signal that this context subscribes to changes, the callback will be run
pub fn new_with_callback(
callback: impl FnMut() + Send + Sync + 'static,
scope: ScopeId,
#[allow(unused)] origin: &'static std::panic::Location<'static>,
) -> Self {
let inner = Inner {
self_: None,
update: Box::new(callback),
#[cfg(debug_assertions)]
origin,
};
let mut self_ = Self {
inner: CopyValue::new_maybe_sync_in_scope(inner, scope),
};
self_.inner.write().self_ = Some(self_);
self_
}
/// Get the current reactive context
///
/// If this was set manually, then that value will be returned.
///
/// If there's no current reactive context, then a new one will be created for the current scope and returned.
pub fn current() -> Option<Self> {
let cur = CURRENT.with(|current| current.borrow().last().cloned());
// If we're already inside a reactive context, then return that
if let Some(cur) = cur {
return Some(cur);
}
// If we're rendering, then try and use the reactive context attached to this component
if !dioxus_core::vdom_is_rendering() {
return None;
}
if let Some(cx) = has_context() {
return Some(cx);
}
let update_any = schedule_update_any();
let scope_id = current_scope_id().unwrap();
let update_scope = move || {
tracing::trace!("Marking scope {:?} as dirty", scope_id);
update_any(scope_id)
};
// Otherwise, create a new context at the current scope
Some(provide_context(ReactiveContext::new_with_callback(
update_scope,
scope_id,
std::panic::Location::caller(),
)))
}
/// Run this function in the context of this reactive context
///
/// This will set the current reactive context to this context for the duration of the function.
/// You can then get information about the current subscriptions.
pub fn run_in<O>(&self, f: impl FnOnce() -> O) -> O {
CURRENT.with(|current| current.borrow_mut().push(*self));
let out = f();
CURRENT.with(|current| current.borrow_mut().pop());
out
}
/// Marks this reactive context as dirty
///
/// If there's a scope associated with this context, then it will be marked as dirty too
///
/// Returns true if the context was marked as dirty, or false if the context has been dropped
pub fn mark_dirty(&self) -> bool {
if let Ok(mut self_write) = self.inner.try_write_unchecked() {
#[cfg(debug_assertions)]
{
tracing::trace!(
"Marking reactive context created at {} as dirty",
self_write.origin
);
}
(self_write.update)();
true
} else {
false
}
}
/// Get the scope that inner CopyValue is associated with
pub fn origin_scope(&self) -> ScopeId {
self.inner.origin_scope()
}
}
impl Hash for ReactiveContext {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner.id().hash(state);
}
}
struct Inner {
self_: Option<ReactiveContext>,
// Futures will call .changed().await
update: Box<dyn FnMut() + Send + Sync>,
// Debug information for signal subscriptions
#[cfg(debug_assertions)]
origin: &'static std::panic::Location<'static>,
}