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>,
}