Merge pull request #1300 from Demonthos/signals
Complete Signals implementation
This commit is contained in:
41 files changed
+2841
-423
No files matched your search
@@ -25,6 +25,7 @@ members = [
|
|||||||
"packages/native-core",
|
"packages/native-core",
|
||||||
"packages/native-core-macro",
|
"packages/native-core-macro",
|
||||||
"packages/rsx-rosetta",
|
"packages/rsx-rosetta",
|
||||||
|
"packages/generational-box",
|
||||||
"packages/signals",
|
"packages/signals",
|
||||||
"packages/hot-reload",
|
"packages/hot-reload",
|
||||||
"packages/fullstack",
|
"packages/fullstack",
|
||||||
@@ -76,6 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
|
|||||||
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
|
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
|
||||||
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
|
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
|
||||||
dioxus-signals = { path = "packages/signals" }
|
dioxus-signals = { path = "packages/signals" }
|
||||||
|
generational-box = { path = "packages/generational-box" }
|
||||||
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
|
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
|
||||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
||||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
||||||
|
|||||||
+1
-7
@@ -1,17 +1,11 @@
|
|||||||
//! Example: README.md showcase
|
|
||||||
//!
|
|
||||||
//! The example from the README.md.
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_signals::{use_init_signal_rt, use_signal};
|
use dioxus_signals::use_signal;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
use_init_signal_rt(cx);
|
|
||||||
|
|
||||||
let mut count = use_signal(cx, || 0);
|
let mut count = use_signal(cx, || 0);
|
||||||
|
|
||||||
use_future!(cx, || async move {
|
use_future!(cx, || async move {
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button { onclick: move |_| count += 1, "Up high!" }
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
button { onclick: move |_| count -= 1, "Down low!" }
|
||||||
|
|
||||||
if count() > 5 {
|
if count.value() > 5 {
|
||||||
rsx!{ h2 { "High five!" } }
|
rsx!{ h2 { "High five!" } }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ pub(crate) struct ElementRef {
|
|||||||
|
|
||||||
// The actual template
|
// The actual template
|
||||||
pub template: Option<NonNull<VNode<'static>>>,
|
pub template: Option<NonNull<VNode<'static>>>,
|
||||||
|
|
||||||
|
// The scope the element belongs to
|
||||||
|
pub scope: ScopeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -32,6 +35,7 @@ impl ElementRef {
|
|||||||
Self {
|
Self {
|
||||||
template: None,
|
template: None,
|
||||||
path: ElementPath::Root(0),
|
path: ElementPath::Root(0),
|
||||||
|
scope: ScopeId(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,11 +60,13 @@ impl VirtualDom {
|
|||||||
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
||||||
let entry = self.elements.vacant_entry();
|
let entry = self.elements.vacant_entry();
|
||||||
let id = entry.key();
|
let id = entry.key();
|
||||||
|
let scope = self.runtime.current_scope_id().unwrap_or(ScopeId(0));
|
||||||
|
|
||||||
entry.insert(ElementRef {
|
entry.insert(ElementRef {
|
||||||
// We know this is non-null because it comes from a reference
|
// We know this is non-null because it comes from a reference
|
||||||
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
|
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
|
||||||
path,
|
path,
|
||||||
|
scope,
|
||||||
});
|
});
|
||||||
ElementId(id)
|
ElementId(id)
|
||||||
}
|
}
|
||||||
@@ -91,7 +97,7 @@ impl VirtualDom {
|
|||||||
// Note: This will not remove any ids from the arena
|
// Note: This will not remove any ids from the arena
|
||||||
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
||||||
self.dirty_scopes.remove(&DirtyScope {
|
self.dirty_scopes.remove(&DirtyScope {
|
||||||
height: self.scopes[id.0].height,
|
height: self.scopes[id.0].height(),
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,10 +116,13 @@ impl VirtualDom {
|
|||||||
// Drop all the hooks once the children are dropped
|
// Drop all the hooks once the children are dropped
|
||||||
// this means we'll drop hooks bottom-up
|
// this means we'll drop hooks bottom-up
|
||||||
scope.hooks.get_mut().clear();
|
scope.hooks.get_mut().clear();
|
||||||
|
{
|
||||||
|
let context = scope.context();
|
||||||
|
|
||||||
// Drop all the futures once the hooks are dropped
|
// Drop all the futures once the hooks are dropped
|
||||||
for task_id in scope.spawned_tasks.borrow_mut().drain() {
|
for task_id in context.spawned_tasks.borrow_mut().drain() {
|
||||||
scope.tasks.remove(task_id);
|
context.tasks.remove(task_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scopes.remove(id.0);
|
self.scopes.remove(id.0);
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ impl<'b> VirtualDom {
|
|||||||
///
|
///
|
||||||
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
||||||
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
|
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
|
||||||
self.scope_stack.push(scope);
|
self.runtime.scope_stack.borrow_mut().push(scope);
|
||||||
let out = self.create(template);
|
let nodes = self.create(template);
|
||||||
self.scope_stack.pop();
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
out
|
nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create this template and write its mutations
|
/// Create this template and write its mutations
|
||||||
@@ -522,7 +522,7 @@ impl<'b> VirtualDom {
|
|||||||
.take()
|
.take()
|
||||||
.map(|props| {
|
.map(|props| {
|
||||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||||
self.new_scope(unbounded_props, component.name).id
|
self.new_scope(unbounded_props, component.name).context().id
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| component.scope.get().unwrap())
|
.unwrap_or_else(|| component.scope.get().unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ use DynamicNode::*;
|
|||||||
|
|
||||||
impl<'b> VirtualDom {
|
impl<'b> VirtualDom {
|
||||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||||
let scope_state = &mut self.scopes[scope.0];
|
let scope_state = &mut self.get_scope(scope).unwrap();
|
||||||
|
|
||||||
self.scope_stack.push(scope);
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Load the old and new bump arenas
|
// Load the old and new bump arenas
|
||||||
let old = scope_state
|
let old = scope_state
|
||||||
@@ -47,7 +45,6 @@ impl<'b> VirtualDom {
|
|||||||
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.scope_stack.pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
||||||
@@ -210,7 +207,7 @@ impl<'b> VirtualDom {
|
|||||||
self.diff_scope(scope_id);
|
self.diff_scope(scope_id);
|
||||||
|
|
||||||
self.dirty_scopes.remove(&DirtyScope {
|
self.dirty_scopes.remove(&DirtyScope {
|
||||||
height: self.scopes[scope_id.0].height,
|
height: self.runtime.get_context(scope_id).unwrap().height,
|
||||||
id: scope_id,
|
id: scope_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -714,7 +711,12 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
Component(comp) => {
|
Component(comp) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe {
|
||||||
|
self.get_scope(scope)
|
||||||
|
.unwrap()
|
||||||
|
.root_node()
|
||||||
|
.extend_lifetime_ref()
|
||||||
|
} {
|
||||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||||
RenderReturn::Aborted(_node) => todo!(),
|
RenderReturn::Aborted(_node) => todo!(),
|
||||||
}
|
}
|
||||||
@@ -915,7 +917,12 @@ impl<'b> VirtualDom {
|
|||||||
.expect("VComponents to always have a scope");
|
.expect("VComponents to always have a scope");
|
||||||
|
|
||||||
// Remove the component from the dom
|
// Remove the component from the dom
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe {
|
||||||
|
self.get_scope(scope)
|
||||||
|
.unwrap()
|
||||||
|
.root_node()
|
||||||
|
.extend_lifetime_ref()
|
||||||
|
} {
|
||||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||||
};
|
};
|
||||||
@@ -936,7 +943,12 @@ impl<'b> VirtualDom {
|
|||||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||||
Some(Component(comp)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe {
|
||||||
|
self.get_scope(scope)
|
||||||
|
.unwrap()
|
||||||
|
.root_node()
|
||||||
|
.extend_lifetime_ref()
|
||||||
|
} {
|
||||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
}
|
}
|
||||||
@@ -952,7 +964,12 @@ impl<'b> VirtualDom {
|
|||||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||||
Some(Component(comp)) => {
|
Some(Component(comp)) => {
|
||||||
let scope = comp.scope.get().unwrap();
|
let scope = comp.scope.get().unwrap();
|
||||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
match unsafe {
|
||||||
|
self.get_scope(scope)
|
||||||
|
.unwrap()
|
||||||
|
.root_node()
|
||||||
|
.extend_lifetime_ref()
|
||||||
|
} {
|
||||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||||
_ => todo!("cannot handle nonstandard nodes"),
|
_ => todo!("cannot handle nonstandard nodes"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::{runtime::with_runtime, ScopeId};
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -135,12 +136,14 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
pub struct EventHandler<'bump, T = ()> {
|
pub struct EventHandler<'bump, T = ()> {
|
||||||
|
pub(crate) origin: ScopeId,
|
||||||
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for EventHandler<'_, T> {
|
impl<T> Default for EventHandler<'_, T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
origin: ScopeId(0),
|
||||||
callback: Default::default(),
|
callback: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +157,13 @@ impl<T> EventHandler<'_, T> {
|
|||||||
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
|
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
|
||||||
pub fn call(&self, event: T) {
|
pub fn call(&self, event: T) {
|
||||||
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
||||||
|
with_runtime(|rt| {
|
||||||
|
rt.scope_stack.borrow_mut().push(self.origin);
|
||||||
|
});
|
||||||
callback(event);
|
callback(event);
|
||||||
|
with_runtime(|rt| {
|
||||||
|
rt.scope_stack.borrow_mut().pop();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ mod lazynodes;
|
|||||||
mod mutations;
|
mod mutations;
|
||||||
mod nodes;
|
mod nodes;
|
||||||
mod properties;
|
mod properties;
|
||||||
|
mod runtime;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod scope_arena;
|
mod scope_arena;
|
||||||
|
mod scope_context;
|
||||||
mod scopes;
|
mod scopes;
|
||||||
mod virtual_dom;
|
mod virtual_dom;
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ pub(crate) mod innerlude {
|
|||||||
pub use crate::nodes::*;
|
pub use crate::nodes::*;
|
||||||
pub use crate::properties::*;
|
pub use crate::properties::*;
|
||||||
pub use crate::scheduler::*;
|
pub use crate::scheduler::*;
|
||||||
|
pub use crate::scope_context::*;
|
||||||
pub use crate::scopes::*;
|
pub use crate::scopes::*;
|
||||||
pub use crate::virtual_dom::*;
|
pub use crate::virtual_dom::*;
|
||||||
|
|
||||||
@@ -70,10 +73,11 @@ pub(crate) mod innerlude {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError,
|
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
|
||||||
Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation,
|
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
|
||||||
Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
|
||||||
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
|
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
|
||||||
|
VirtualDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The purpose of this module is to alleviate imports of many common types
|
/// The purpose of this module is to alleviate imports of many common types
|
||||||
@@ -81,9 +85,12 @@ pub use crate::innerlude::{
|
|||||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment,
|
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||||
IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
||||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
||||||
|
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
|
||||||
|
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
|
||||||
|
TemplateNode, Throw, VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
use std::cell::{Cell, Ref, RefCell};
|
||||||
|
|
||||||
|
use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = RefCell::new(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a new scope onto the stack
|
||||||
|
pub(crate) fn push_runtime(runtime: Rc<Runtime>) {
|
||||||
|
RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops a scope off the stack
|
||||||
|
pub(crate) fn pop_runtime() {
|
||||||
|
RUNTIMES.with(|stack| stack.borrow_mut().pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs a function with the current runtime
|
||||||
|
pub(crate) fn with_runtime<F, R>(f: F) -> Option<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&Runtime) -> R,
|
||||||
|
{
|
||||||
|
RUNTIMES.with(|stack| {
|
||||||
|
let stack = stack.borrow();
|
||||||
|
stack.last().map(|r| f(r))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs a function with the current scope
|
||||||
|
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&ScopeContext) -> R,
|
||||||
|
{
|
||||||
|
with_runtime(|runtime| {
|
||||||
|
runtime
|
||||||
|
.current_scope_id()
|
||||||
|
.and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc)))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Runtime {
|
||||||
|
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
|
||||||
|
pub(crate) scheduler: Rc<Scheduler>,
|
||||||
|
|
||||||
|
// We use this to track the current scope
|
||||||
|
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
||||||
|
pub(crate) rendering: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
pub(crate) fn new(scheduler: Rc<Scheduler>) -> Rc<Self> {
|
||||||
|
Rc::new(Self {
|
||||||
|
scheduler,
|
||||||
|
|
||||||
|
scope_contexts: Default::default(),
|
||||||
|
|
||||||
|
scope_stack: Default::default(),
|
||||||
|
|
||||||
|
rendering: Cell::new(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a scope context. This slab is synchronized with the scope slab.
|
||||||
|
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
|
||||||
|
let mut contexts = self.scope_contexts.borrow_mut();
|
||||||
|
if contexts.len() <= id.0 {
|
||||||
|
contexts.resize_with(id.0 + 1, Default::default);
|
||||||
|
}
|
||||||
|
contexts[id.0] = Some(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_context(&self, id: ScopeId) {
|
||||||
|
self.scope_contexts.borrow_mut()[id.0] = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current scope id
|
||||||
|
pub fn current_scope_id(&self) -> Option<ScopeId> {
|
||||||
|
self.scope_stack.borrow().last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the context for any scope given its ID
|
||||||
|
///
|
||||||
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
|
pub fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
|
||||||
|
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
|
||||||
|
contexts.get(id.0).and_then(|f| f.as_ref())
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RuntimeGuard(Rc<Runtime>);
|
||||||
|
|
||||||
|
impl RuntimeGuard {
|
||||||
|
pub(crate) fn new(runtime: Rc<Runtime>) -> Self {
|
||||||
|
push_runtime(runtime.clone());
|
||||||
|
Self(runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RuntimeGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
pop_runtime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{TaskId, VirtualDom};
|
use crate::{runtime::RuntimeGuard, TaskId, VirtualDom};
|
||||||
use std::task::Context;
|
use std::task::Context;
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
@@ -7,7 +7,8 @@ impl VirtualDom {
|
|||||||
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
||||||
/// queue
|
/// queue
|
||||||
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
||||||
let mut tasks = self.scheduler.tasks.borrow_mut();
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
|
let mut tasks = self.runtime.scheduler.tasks.borrow_mut();
|
||||||
|
|
||||||
let task = match tasks.get(id.0) {
|
let task = match tasks.get(id.0) {
|
||||||
Some(task) => task,
|
Some(task) => task,
|
||||||
@@ -17,14 +18,22 @@ impl VirtualDom {
|
|||||||
|
|
||||||
let mut cx = Context::from_waker(&task.waker);
|
let mut cx = Context::from_waker(&task.waker);
|
||||||
|
|
||||||
|
// update the scope stack
|
||||||
|
self.runtime.scope_stack.borrow_mut().push(task.scope);
|
||||||
|
self.runtime.rendering.set(false);
|
||||||
|
|
||||||
// If the task completes...
|
// If the task completes...
|
||||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||||
let scope = &self.scopes[task.scope.0];
|
let scope = &self.get_scope(task.scope).unwrap();
|
||||||
scope.spawned_tasks.borrow_mut().remove(&id);
|
scope.context().spawned_tasks.borrow_mut().remove(&id);
|
||||||
|
|
||||||
// Remove it from the scheduler
|
// Remove it from the scheduler
|
||||||
tasks.try_remove(id.0);
|
tasks.try_remove(id.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the scope from the stack
|
||||||
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
|
self.runtime.rendering.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ use crate::{
|
|||||||
bump_frame::BumpFrame,
|
bump_frame::BumpFrame,
|
||||||
innerlude::DirtyScope,
|
innerlude::DirtyScope,
|
||||||
nodes::RenderReturn,
|
nodes::RenderReturn,
|
||||||
|
scope_context::ScopeContext,
|
||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
};
|
};
|
||||||
@@ -13,48 +14,49 @@ impl VirtualDom {
|
|||||||
props: Box<dyn AnyProps<'static>>,
|
props: Box<dyn AnyProps<'static>>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
) -> &ScopeState {
|
) -> &ScopeState {
|
||||||
let parent = self.acquire_current_scope_raw();
|
let parent_id = self.runtime.current_scope_id();
|
||||||
|
let height = parent_id
|
||||||
|
.and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1))
|
||||||
|
.unwrap_or(0);
|
||||||
let entry = self.scopes.vacant_entry();
|
let entry = self.scopes.vacant_entry();
|
||||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
|
||||||
let id = ScopeId(entry.key());
|
let id = ScopeId(entry.key());
|
||||||
|
|
||||||
entry.insert(Box::new(ScopeState {
|
let scope = entry.insert(Box::new(ScopeState {
|
||||||
parent,
|
runtime: self.runtime.clone(),
|
||||||
id,
|
context_id: id,
|
||||||
height,
|
|
||||||
name,
|
|
||||||
props: Some(props),
|
props: Some(props),
|
||||||
tasks: self.scheduler.clone(),
|
|
||||||
node_arena_1: BumpFrame::new(0),
|
node_arena_1: BumpFrame::new(0),
|
||||||
node_arena_2: BumpFrame::new(0),
|
node_arena_2: BumpFrame::new(0),
|
||||||
spawned_tasks: Default::default(),
|
|
||||||
suspended: Default::default(),
|
|
||||||
render_cnt: Default::default(),
|
render_cnt: Default::default(),
|
||||||
hooks: Default::default(),
|
hooks: Default::default(),
|
||||||
hook_idx: Default::default(),
|
hook_idx: Default::default(),
|
||||||
shared_contexts: Default::default(),
|
|
||||||
borrowed_props: Default::default(),
|
borrowed_props: Default::default(),
|
||||||
attributes_to_drop: Default::default(),
|
attributes_to_drop: Default::default(),
|
||||||
}))
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
let context =
|
||||||
let id = self.scope_stack.last().copied()?;
|
ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone());
|
||||||
let scope = self.scopes.get(id.0)?;
|
self.runtime.create_context_at(id, context);
|
||||||
Some(scope.as_ref())
|
|
||||||
|
scope
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||||
|
self.runtime.scope_stack.borrow_mut().push(scope_id);
|
||||||
// Cycle to the next frame and then reset it
|
// Cycle to the next frame and then reset it
|
||||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||||
// Remove all the outdated listeners
|
// Remove all the outdated listeners
|
||||||
self.ensure_drop_safety(scope_id);
|
self.ensure_drop_safety(scope_id);
|
||||||
|
|
||||||
let new_nodes = unsafe {
|
let new_nodes = unsafe {
|
||||||
self.scopes[scope_id.0].previous_frame().bump_mut().reset();
|
|
||||||
|
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id.0];
|
||||||
scope.suspended.set(false);
|
scope.previous_frame().bump_mut().reset();
|
||||||
|
|
||||||
|
scope.context().suspended.set(false);
|
||||||
|
|
||||||
scope.hook_idx.set(0);
|
scope.hook_idx.set(0);
|
||||||
|
|
||||||
@@ -77,21 +79,26 @@ impl VirtualDom {
|
|||||||
// And move the render generation forward by one
|
// And move the render generation forward by one
|
||||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||||
|
|
||||||
|
let context = scope.context();
|
||||||
// remove this scope from dirty scopes
|
// remove this scope from dirty scopes
|
||||||
self.dirty_scopes.remove(&DirtyScope {
|
self.dirty_scopes.remove(&DirtyScope {
|
||||||
height: scope.height,
|
height: context.height,
|
||||||
id: scope.id,
|
id: context.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if scope.suspended.get() {
|
if context.suspended.get() {
|
||||||
if matches!(allocated, RenderReturn::Aborted(_)) {
|
if matches!(allocated, RenderReturn::Aborted(_)) {
|
||||||
self.suspended_scopes.insert(scope.id);
|
self.suspended_scopes.insert(context.id);
|
||||||
}
|
}
|
||||||
} else if !self.suspended_scopes.is_empty() {
|
} else if !self.suspended_scopes.is_empty() {
|
||||||
_ = self.suspended_scopes.remove(&scope.id);
|
_ = self.suspended_scopes.remove(&context.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebind the lifetime now that its stored internally
|
// rebind the lifetime now that its stored internally
|
||||||
unsafe { allocated.extend_lifetime_ref() }
|
let result = unsafe { allocated.extend_lifetime_ref() };
|
||||||
|
|
||||||
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
use crate::{
|
||||||
|
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||||
|
runtime::{with_current_scope, with_runtime},
|
||||||
|
Element, ScopeId, TaskId,
|
||||||
|
};
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
use std::{
|
||||||
|
any::Any,
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
fmt::Debug,
|
||||||
|
future::Future,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A component's state separate from its props.
|
||||||
|
///
|
||||||
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
|
pub(crate) struct ScopeContext {
|
||||||
|
pub(crate) name: &'static str,
|
||||||
|
|
||||||
|
pub(crate) id: ScopeId,
|
||||||
|
pub(crate) parent_id: Option<ScopeId>,
|
||||||
|
|
||||||
|
pub(crate) height: u32,
|
||||||
|
pub(crate) suspended: Cell<bool>,
|
||||||
|
|
||||||
|
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
|
||||||
|
|
||||||
|
pub(crate) tasks: Rc<Scheduler>,
|
||||||
|
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopeContext {
|
||||||
|
pub(crate) fn new(
|
||||||
|
name: &'static str,
|
||||||
|
id: ScopeId,
|
||||||
|
parent_id: Option<ScopeId>,
|
||||||
|
height: u32,
|
||||||
|
tasks: Rc<Scheduler>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
parent_id,
|
||||||
|
height,
|
||||||
|
suspended: Cell::new(false),
|
||||||
|
shared_contexts: RefCell::new(vec![]),
|
||||||
|
tasks,
|
||||||
|
spawned_tasks: RefCell::new(FxHashSet::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_id(&self) -> Option<ScopeId> {
|
||||||
|
self.parent_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope_id(&self) -> ScopeId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a subscription that schedules a future render for the reference component
|
||||||
|
///
|
||||||
|
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||||
|
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||||
|
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||||
|
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule an update for any component given its [`ScopeId`].
|
||||||
|
///
|
||||||
|
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||||
|
///
|
||||||
|
/// This method should be used when you want to schedule an update for a component
|
||||||
|
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||||
|
let chan = self.tasks.sender.clone();
|
||||||
|
Arc::new(move |id| {
|
||||||
|
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark this scope as dirty, and schedule a render for it.
|
||||||
|
pub fn needs_update(&self) {
|
||||||
|
self.needs_update_any(self.scope_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [`ScopeId`] of a mounted component.
|
||||||
|
///
|
||||||
|
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||||
|
pub fn needs_update_any(&self, id: ScopeId) {
|
||||||
|
self.tasks
|
||||||
|
.sender
|
||||||
|
.unbounded_send(SchedulerMsg::Immediate(id))
|
||||||
|
.expect("Scheduler to exist if scope exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return any context of type T if it exists on this scope
|
||||||
|
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||||
|
self.shared_contexts
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find_map(|any| any.downcast_ref::<T>())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||||
|
///
|
||||||
|
/// Clones the state if it exists.
|
||||||
|
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||||
|
if let Some(this_ctx) = self.has_context() {
|
||||||
|
return Some(this_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut search_parent = self.parent_id;
|
||||||
|
with_runtime(|runtime| {
|
||||||
|
while let Some(parent_id) = search_parent {
|
||||||
|
let parent = runtime.get_context(parent_id).unwrap();
|
||||||
|
if let Some(shared) = parent
|
||||||
|
.shared_contexts
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find_map(|any| any.downcast_ref::<T>())
|
||||||
|
{
|
||||||
|
return Some(shared.clone());
|
||||||
|
}
|
||||||
|
search_parent = parent.parent_id;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||||
|
///
|
||||||
|
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||||
|
///
|
||||||
|
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// struct SharedState(&'static str);
|
||||||
|
///
|
||||||
|
/// static App: Component = |cx| {
|
||||||
|
/// cx.use_hook(|| cx.provide_context(SharedState("world")));
|
||||||
|
/// render!(Child {})
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// static Child: Component = |cx| {
|
||||||
|
/// let state = cx.consume_state::<SharedState>();
|
||||||
|
/// render!(div { "hello {state.0}" })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||||
|
let mut contexts = self.shared_contexts.borrow_mut();
|
||||||
|
|
||||||
|
// If the context exists, swap it out for the new value
|
||||||
|
for ctx in contexts.iter_mut() {
|
||||||
|
// Swap the ptr directly
|
||||||
|
if let Some(ctx) = ctx.downcast_mut::<T>() {
|
||||||
|
std::mem::swap(ctx, &mut value.clone());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, just push it
|
||||||
|
contexts.push(Box::new(value.clone()));
|
||||||
|
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide a context to the root and then consume it
|
||||||
|
///
|
||||||
|
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
|
||||||
|
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
|
||||||
|
///
|
||||||
|
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||||
|
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||||
|
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||||
|
with_runtime(|runtime| {
|
||||||
|
runtime
|
||||||
|
.get_context(ScopeId(0))
|
||||||
|
.unwrap()
|
||||||
|
.provide_context(context)
|
||||||
|
})
|
||||||
|
.expect("Runtime to exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||||
|
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||||
|
let id = self.tasks.spawn(self.id, fut);
|
||||||
|
self.spawned_tasks.borrow_mut().insert(id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the future but does not return the [`TaskId`]
|
||||||
|
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||||
|
self.push_future(fut);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||||
|
///
|
||||||
|
/// This is good for tasks that need to be run after the component has been dropped.
|
||||||
|
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||||
|
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||||
|
let id = self.tasks.spawn(ScopeId(0), fut);
|
||||||
|
|
||||||
|
// wake up the scheduler if it is sleeping
|
||||||
|
self.tasks
|
||||||
|
.sender
|
||||||
|
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||||
|
.expect("Scheduler should exist");
|
||||||
|
|
||||||
|
self.spawned_tasks.borrow_mut().insert(id);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||||
|
///
|
||||||
|
/// This drops the task immediately.
|
||||||
|
pub fn remove_future(&self, id: TaskId) {
|
||||||
|
self.tasks.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inject an error into the nearest error boundary and quit rendering
|
||||||
|
///
|
||||||
|
/// The error doesn't need to implement Error or any specific traits since the boundary
|
||||||
|
/// itself will downcast the error into a trait object.
|
||||||
|
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
|
||||||
|
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
|
||||||
|
cx.insert_error(self.scope_id(), Box::new(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return none during a throw
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark this component as suspended and then return None
|
||||||
|
pub fn suspend(&self) -> Option<Element> {
|
||||||
|
self.suspended.set(true);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule an update for any component given its [`ScopeId`].
|
||||||
|
///
|
||||||
|
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`crate::scopes::ScopeState::scope_id`] method.
|
||||||
|
///
|
||||||
|
/// This method should be used when you want to schedule an update for a component
|
||||||
|
pub fn schedule_update_any() -> Option<Arc<dyn Fn(ScopeId) + Send + Sync>> {
|
||||||
|
with_current_scope(|cx| cx.schedule_update_any())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current scope id
|
||||||
|
pub fn current_scope_id() -> Option<ScopeId> {
|
||||||
|
with_runtime(|rt| rt.current_scope_id()).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Check if the virtual dom is currently inside of the body of a component
|
||||||
|
pub fn vdom_is_rendering() -> bool {
|
||||||
|
with_runtime(|rt| rt.rendering.get()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume context from the current scope
|
||||||
|
pub fn consume_context<T: 'static + Clone>() -> Option<T> {
|
||||||
|
with_current_scope(|cx| cx.consume_context::<T>()).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume context from the current scope
|
||||||
|
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
|
||||||
|
with_runtime(|rt| {
|
||||||
|
rt.get_context(scope_id)
|
||||||
|
.and_then(|cx| cx.consume_context::<T>())
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the current scope has a context
|
||||||
|
pub fn has_context<T: 'static + Clone>() -> Option<T> {
|
||||||
|
with_current_scope(|cx| cx.has_context::<T>()).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide context to the current scope
|
||||||
|
pub fn provide_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||||
|
with_current_scope(|cx| cx.provide_context(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide context to the the given scope
|
||||||
|
pub fn provide_context_to_scope<T: 'static + Clone>(scope_id: ScopeId, value: T) -> Option<T> {
|
||||||
|
with_runtime(|rt| rt.get_context(scope_id).map(|cx| cx.provide_context(value))).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide a context to the root scope
|
||||||
|
pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||||
|
with_current_scope(|cx| cx.provide_root_context(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Suspends the current component
|
||||||
|
pub fn suspend() -> Option<Element<'static>> {
|
||||||
|
with_current_scope(|cx| {
|
||||||
|
cx.suspend();
|
||||||
|
});
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Throw an error into the nearest error boundary
|
||||||
|
pub fn throw(error: impl Debug + 'static) -> Option<()> {
|
||||||
|
with_current_scope(|cx| cx.throw(error)).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||||
|
pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||||
|
with_current_scope(|cx| cx.push_future(fut))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the future but does not return the [`TaskId`]
|
||||||
|
pub fn spawn(fut: impl Future<Output = ()> + 'static) {
|
||||||
|
with_current_scope(|cx| cx.spawn(fut));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||||
|
///
|
||||||
|
/// This is good for tasks that need to be run after the component has been dropped.
|
||||||
|
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||||
|
with_current_scope(|cx| cx.spawn_forever(fut))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||||
|
///
|
||||||
|
/// This drops the task immediately.
|
||||||
|
pub fn remove_future(id: TaskId) {
|
||||||
|
with_current_scope(|cx| cx.remove_future(id));
|
||||||
|
}
|
||||||
+40
-100
@@ -2,17 +2,18 @@ use crate::{
|
|||||||
any_props::AnyProps,
|
any_props::AnyProps,
|
||||||
any_props::VProps,
|
any_props::VProps,
|
||||||
bump_frame::BumpFrame,
|
bump_frame::BumpFrame,
|
||||||
|
innerlude::ErrorBoundary,
|
||||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
|
||||||
lazynodes::LazyNodes,
|
lazynodes::LazyNodes,
|
||||||
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||||
|
runtime::Runtime,
|
||||||
|
scope_context::ScopeContext,
|
||||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
use rustc_hash::FxHashSet;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::Any,
|
||||||
cell::{Cell, RefCell, UnsafeCell},
|
cell::{Cell, Ref, RefCell, UnsafeCell},
|
||||||
fmt::{Arguments, Debug},
|
fmt::{Arguments, Debug},
|
||||||
future::Future,
|
future::Future,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -66,33 +67,34 @@ pub struct ScopeId(pub usize);
|
|||||||
///
|
///
|
||||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
pub struct ScopeState {
|
pub struct ScopeState {
|
||||||
|
pub(crate) runtime: Rc<Runtime>,
|
||||||
|
pub(crate) context_id: ScopeId,
|
||||||
|
|
||||||
pub(crate) render_cnt: Cell<usize>,
|
pub(crate) render_cnt: Cell<usize>,
|
||||||
pub(crate) name: &'static str,
|
|
||||||
|
|
||||||
pub(crate) node_arena_1: BumpFrame,
|
pub(crate) node_arena_1: BumpFrame,
|
||||||
pub(crate) node_arena_2: BumpFrame,
|
pub(crate) node_arena_2: BumpFrame,
|
||||||
|
|
||||||
pub(crate) parent: Option<*const ScopeState>,
|
|
||||||
pub(crate) id: ScopeId,
|
|
||||||
|
|
||||||
pub(crate) height: u32,
|
|
||||||
pub(crate) suspended: Cell<bool>,
|
|
||||||
|
|
||||||
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
|
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
|
||||||
pub(crate) hook_idx: Cell<usize>,
|
pub(crate) hook_idx: Cell<usize>,
|
||||||
|
|
||||||
pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
|
|
||||||
|
|
||||||
pub(crate) tasks: Rc<Scheduler>,
|
|
||||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
|
||||||
|
|
||||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
||||||
|
|
||||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for ScopeState {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.runtime.remove_context(self.context_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'src> ScopeState {
|
impl<'src> ScopeState {
|
||||||
|
pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
|
||||||
|
self.runtime.get_context(self.context_id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
||||||
match self.render_cnt.get() % 2 {
|
match self.render_cnt.get() % 2 {
|
||||||
0 => &self.node_arena_1,
|
0 => &self.node_arena_1,
|
||||||
@@ -111,7 +113,7 @@ impl<'src> ScopeState {
|
|||||||
|
|
||||||
/// Get the name of this component
|
/// Get the name of this component
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
self.name
|
self.context().name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current render since the inception of this component
|
/// Get the current render since the inception of this component
|
||||||
@@ -174,7 +176,7 @@ impl<'src> ScopeState {
|
|||||||
/// assert_eq!(base.height(), 0);
|
/// assert_eq!(base.height(), 0);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn height(&self) -> u32 {
|
pub fn height(&self) -> u32 {
|
||||||
self.height
|
self.context().height
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
|
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
|
||||||
@@ -195,7 +197,7 @@ impl<'src> ScopeState {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn parent(&self) -> Option<ScopeId> {
|
pub fn parent(&self) -> Option<ScopeId> {
|
||||||
// safety: the pointer to our parent is *always* valid thanks to the bump arena
|
// safety: the pointer to our parent is *always* valid thanks to the bump arena
|
||||||
self.parent.map(|p| unsafe { &*p }.id)
|
self.context().parent_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
|
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
|
||||||
@@ -212,15 +214,14 @@ impl<'src> ScopeState {
|
|||||||
/// assert_eq!(base.scope_id(), 0);
|
/// assert_eq!(base.scope_id(), 0);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scope_id(&self) -> ScopeId {
|
pub fn scope_id(&self) -> ScopeId {
|
||||||
self.id
|
self.context().scope_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a subscription that schedules a future render for the reference component
|
/// Create a subscription that schedules a future render for the reference component
|
||||||
///
|
///
|
||||||
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
self.context().schedule_update()
|
||||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Schedule an update for any component given its [`ScopeId`].
|
/// Schedule an update for any component given its [`ScopeId`].
|
||||||
@@ -229,61 +230,31 @@ impl<'src> ScopeState {
|
|||||||
///
|
///
|
||||||
/// This method should be used when you want to schedule an update for a component
|
/// This method should be used when you want to schedule an update for a component
|
||||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||||
let chan = self.tasks.sender.clone();
|
self.context().schedule_update_any()
|
||||||
Arc::new(move |id| {
|
|
||||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark this scope as dirty, and schedule a render for it.
|
/// Mark this scope as dirty, and schedule a render for it.
|
||||||
pub fn needs_update(&self) {
|
pub fn needs_update(&self) {
|
||||||
self.needs_update_any(self.scope_id());
|
self.context().needs_update()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [`ScopeId`] of a mounted component.
|
/// Get the [`ScopeId`] of a mounted component.
|
||||||
///
|
///
|
||||||
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||||
pub fn needs_update_any(&self, id: ScopeId) {
|
pub fn needs_update_any(&self, id: ScopeId) {
|
||||||
self.tasks
|
self.context().needs_update_any(id)
|
||||||
.sender
|
|
||||||
.unbounded_send(SchedulerMsg::Immediate(id))
|
|
||||||
.expect("Scheduler to exist if scope exists");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return any context of type T if it exists on this scope
|
/// Return any context of type T if it exists on this scope
|
||||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||||
self.shared_contexts
|
self.context().has_context()
|
||||||
.borrow()
|
|
||||||
.iter()
|
|
||||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
|
||||||
.map(|(_, v)| v)?
|
|
||||||
.downcast_ref::<T>()
|
|
||||||
.cloned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||||
///
|
///
|
||||||
/// Clones the state if it exists.
|
/// Clones the state if it exists.
|
||||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||||
if let Some(this_ctx) = self.has_context() {
|
self.context().consume_context()
|
||||||
return Some(this_ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut search_parent = self.parent;
|
|
||||||
while let Some(parent_ptr) = search_parent {
|
|
||||||
// safety: all parent pointers are valid thanks to the bump arena
|
|
||||||
let parent = unsafe { &*parent_ptr };
|
|
||||||
if let Some(shared) = parent
|
|
||||||
.shared_contexts
|
|
||||||
.borrow()
|
|
||||||
.iter()
|
|
||||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
|
||||||
{
|
|
||||||
return shared.1.downcast_ref::<T>().cloned();
|
|
||||||
}
|
|
||||||
search_parent = parent.parent;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||||
@@ -308,21 +279,7 @@ impl<'src> ScopeState {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||||
let mut contexts = self.shared_contexts.borrow_mut();
|
self.context().provide_context(value)
|
||||||
|
|
||||||
// If the context exists, swap it out for the new value
|
|
||||||
for ctx in contexts.iter_mut() {
|
|
||||||
// Swap the ptr directly
|
|
||||||
if let Some(ctx) = ctx.1.downcast_mut::<T>() {
|
|
||||||
std::mem::swap(ctx, &mut value.clone());
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else, just push it
|
|
||||||
contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
|
|
||||||
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide a context to the root and then consume it
|
/// Provide a context to the root and then consume it
|
||||||
@@ -333,52 +290,31 @@ impl<'src> ScopeState {
|
|||||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||||
let mut parent = self;
|
self.context().provide_root_context(context)
|
||||||
|
|
||||||
// Walk upwards until there is no more parent - and tada we have the root
|
|
||||||
while let Some(next_parent) = parent.parent {
|
|
||||||
parent = unsafe { &*next_parent };
|
|
||||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.provide_context(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||||
let id = self.tasks.spawn(self.id, fut);
|
self.context().push_future(fut)
|
||||||
self.spawned_tasks.borrow_mut().insert(id);
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the future but does not return the [`TaskId`]
|
/// Spawns the future but does not return the [`TaskId`]
|
||||||
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||||
self.push_future(fut);
|
self.context().spawn(fut);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||||
///
|
///
|
||||||
/// This is good for tasks that need to be run after the component has been dropped.
|
/// This is good for tasks that need to be run after the component has been dropped.
|
||||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
self.context().spawn_forever(fut)
|
||||||
let id = self.tasks.spawn(ScopeId(0), fut);
|
|
||||||
|
|
||||||
// wake up the scheduler if it is sleeping
|
|
||||||
self.tasks
|
|
||||||
.sender
|
|
||||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
|
||||||
.expect("Scheduler should exist");
|
|
||||||
|
|
||||||
self.spawned_tasks.borrow_mut().insert(id);
|
|
||||||
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||||
///
|
///
|
||||||
/// This drops the task immediately.
|
/// This drops the task immediately.
|
||||||
pub fn remove_future(&self, id: TaskId) {
|
pub fn remove_future(&self, id: TaskId) {
|
||||||
self.tasks.remove(id);
|
self.context().remove_future(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
|
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
|
||||||
@@ -511,7 +447,10 @@ impl<'src> ScopeState {
|
|||||||
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
|
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
|
||||||
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
||||||
let callback = RefCell::new(Some(caller));
|
let callback = RefCell::new(Some(caller));
|
||||||
EventHandler { callback }
|
EventHandler {
|
||||||
|
callback,
|
||||||
|
origin: self.context().id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`AttributeValue`] with the listener variant from a callback
|
/// Create a new [`AttributeValue`] with the listener variant from a callback
|
||||||
@@ -565,7 +504,8 @@ impl<'src> ScopeState {
|
|||||||
|
|
||||||
/// Mark this component as suspended and then return None
|
/// Mark this component as suspended and then return None
|
||||||
pub fn suspend(&self) -> Option<Element> {
|
pub fn suspend(&self) -> Option<Element> {
|
||||||
self.suspended.set(true);
|
let cx = self.context();
|
||||||
|
cx.suspend();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
nodes::RenderReturn,
|
nodes::RenderReturn,
|
||||||
nodes::{Template, TemplateId},
|
nodes::{Template, TemplateId},
|
||||||
|
runtime::{Runtime, RuntimeGuard},
|
||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
AttributeValue, Element, Event, Scope,
|
AttributeValue, Element, Event, Scope,
|
||||||
};
|
};
|
||||||
@@ -174,24 +175,24 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct VirtualDom {
|
pub struct VirtualDom {
|
||||||
|
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||||
|
|
||||||
|
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||||
|
|
||||||
// Maps a template path to a map of byteindexes to templates
|
// Maps a template path to a map of byteindexes to templates
|
||||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
|
||||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
|
||||||
pub(crate) scheduler: Rc<Scheduler>,
|
|
||||||
|
|
||||||
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
|
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
|
||||||
pub(crate) elements: Slab<ElementRef>,
|
pub(crate) elements: Slab<ElementRef>,
|
||||||
|
|
||||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
pub(crate) mutations: Mutations<'static>,
|
||||||
pub(crate) scope_stack: Vec<ScopeId>,
|
|
||||||
|
pub(crate) runtime: Rc<Runtime>,
|
||||||
|
|
||||||
// Currently suspended scopes
|
// Currently suspended scopes
|
||||||
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
|
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
|
||||||
|
|
||||||
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
||||||
|
|
||||||
pub(crate) mutations: Mutations<'static>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
@@ -251,16 +252,16 @@ impl VirtualDom {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
||||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||||
|
let scheduler = Scheduler::new(tx);
|
||||||
let mut dom = Self {
|
let mut dom = Self {
|
||||||
rx,
|
rx,
|
||||||
scheduler: Scheduler::new(tx),
|
runtime: Runtime::new(scheduler),
|
||||||
templates: Default::default(),
|
|
||||||
scopes: Default::default(),
|
scopes: Default::default(),
|
||||||
|
dirty_scopes: Default::default(),
|
||||||
|
templates: Default::default(),
|
||||||
elements: Default::default(),
|
elements: Default::default(),
|
||||||
scope_stack: Vec::new(),
|
|
||||||
dirty_scopes: BTreeSet::new(),
|
|
||||||
suspended_scopes: FxHashSet::default(),
|
|
||||||
mutations: Mutations::default(),
|
mutations: Mutations::default(),
|
||||||
|
suspended_scopes: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = dom.new_scope(
|
let root = dom.new_scope(
|
||||||
@@ -281,7 +282,7 @@ impl VirtualDom {
|
|||||||
///
|
///
|
||||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||||
self.scopes.get(id.0).map(|f| f.as_ref())
|
self.scopes.get(id.0).map(|s| &**s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||||
@@ -301,10 +302,10 @@ impl VirtualDom {
|
|||||||
|
|
||||||
/// Manually mark a scope as requiring a re-render
|
/// Manually mark a scope as requiring a re-render
|
||||||
///
|
///
|
||||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
/// Whenever the Runtime "works", it will re-render this scope
|
||||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||||
if let Some(scope) = self.get_scope(id) {
|
if let Some(scope) = self.get_scope(id) {
|
||||||
let height = scope.height;
|
let height = scope.height();
|
||||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,6 +326,8 @@ impl VirtualDom {
|
|||||||
element: ElementId,
|
element: ElementId,
|
||||||
bubbles: bool,
|
bubbles: bool,
|
||||||
) {
|
) {
|
||||||
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------------------
|
------------------------
|
||||||
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
|
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
|
||||||
@@ -387,9 +390,14 @@ impl VirtualDom {
|
|||||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||||
for listener in listeners.drain(..).rev() {
|
for listener in listeners.drain(..).rev() {
|
||||||
if let AttributeValue::Listener(listener) = listener {
|
if let AttributeValue::Listener(listener) = listener {
|
||||||
|
let origin = el_ref.scope;
|
||||||
|
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||||
|
self.runtime.rendering.set(false);
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
cb(uievent.clone());
|
cb(uievent.clone());
|
||||||
}
|
}
|
||||||
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
|
self.runtime.rendering.set(true);
|
||||||
|
|
||||||
if !uievent.propagates.get() {
|
if !uievent.propagates.get() {
|
||||||
return;
|
return;
|
||||||
@@ -418,9 +426,14 @@ impl VirtualDom {
|
|||||||
// Only call the listener if this is the exact target element.
|
// Only call the listener if this is the exact target element.
|
||||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||||
if let AttributeValue::Listener(listener) = &attr.value {
|
if let AttributeValue::Listener(listener) = &attr.value {
|
||||||
|
let origin = el_ref.scope;
|
||||||
|
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||||
|
self.runtime.rendering.set(false);
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
cb(uievent.clone());
|
cb(uievent.clone());
|
||||||
}
|
}
|
||||||
|
self.runtime.scope_stack.borrow_mut().pop();
|
||||||
|
self.runtime.rendering.set(true);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -501,10 +514,11 @@ impl VirtualDom {
|
|||||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||||
== template.name.rsplit_once(':').unwrap().0
|
== template.name.rsplit_once(':').unwrap().0
|
||||||
{
|
{
|
||||||
let height = scope.height;
|
let context = scope.context();
|
||||||
|
let height = context.height;
|
||||||
self.dirty_scopes.insert(DirtyScope {
|
self.dirty_scopes.insert(DirtyScope {
|
||||||
height,
|
height,
|
||||||
id: scope.id,
|
id: context.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,6 +546,7 @@ impl VirtualDom {
|
|||||||
/// apply_edits(edits);
|
/// apply_edits(edits);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn rebuild(&mut self) -> Mutations {
|
pub fn rebuild(&mut self) -> Mutations {
|
||||||
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
||||||
// Rebuilding implies we append the created elements to the root
|
// Rebuilding implies we append the created elements to the root
|
||||||
RenderReturn::Ready(node) => {
|
RenderReturn::Ready(node) => {
|
||||||
@@ -610,9 +625,12 @@ impl VirtualDom {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the scope and get the mutations
|
{
|
||||||
self.run_scope(dirty.id);
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
self.diff_scope(dirty.id);
|
// Run the scope and get the mutations
|
||||||
|
self.run_scope(dirty.id);
|
||||||
|
self.diff_scope(dirty.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's more work, then just continue, plenty of work to do
|
// If there's more work, then just continue, plenty of work to do
|
||||||
|
|||||||
@@ -158,8 +158,13 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
|
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
|
||||||
Some(t) if t == "text/plain" => get_mime_by_ext(trimmed),
|
Some(f) => {
|
||||||
Some(f) => f,
|
if f == "text/plain" {
|
||||||
|
get_mime_by_ext(trimmed)
|
||||||
|
} else {
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
None => get_mime_by_ext(trimmed),
|
None => get_mime_by_ext(trimmed),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use dioxus::{events::MouseData, prelude::*};
|
use dioxus::{events::MouseData, prelude::*};
|
||||||
use dioxus_core::Event;
|
use dioxus_core::Event;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
use std::fmt::Write;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -9,7 +10,12 @@ fn main() {
|
|||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
fn to_str(c: &[i32; 3]) -> String {
|
fn to_str(c: &[i32; 3]) -> String {
|
||||||
"#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::<String>()
|
let mut result = String::new();
|
||||||
|
result += "#";
|
||||||
|
for c in c.iter() {
|
||||||
|
write!(result, "{c:02X?}").unwrap();
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_brightness(m: &Rc<MouseData>) -> i32 {
|
fn get_brightness(m: &Rc<MouseData>) -> i32 {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "generational-box"
|
||||||
|
authors = ["Evan Almloff"]
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bumpalo = { version = "3.6" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["check_generation"]
|
||||||
|
check_generation = []
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Generational Box
|
||||||
|
|
||||||
|
Generational Box is a runtime for Rust that allows any static type to implement `Copy`. It can be combined with a global runtime to create an ergonomic state solution like `dioxus-signals`. This crate contains no `unsafe` code.
|
||||||
|
|
||||||
|
Three main types manage state in Generational Box:
|
||||||
|
|
||||||
|
- Store: Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
|
||||||
|
- Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped.
|
||||||
|
- GenerationalBox: The core Copy state type. The generational box will be dropped when the owner is dropped.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Create a store for this thread
|
||||||
|
let store = Store::default();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Create an owner for some state for a scope
|
||||||
|
let owner = store.owner();
|
||||||
|
|
||||||
|
// Create some non-copy data, move it into a owner, and work with copy data
|
||||||
|
let data: String = "hello world".to_string();
|
||||||
|
let key = owner.insert(data);
|
||||||
|
|
||||||
|
// The generational box can be read from and written to like a RefCell
|
||||||
|
let value = key.read();
|
||||||
|
assert_eq!(*value, "hello world");
|
||||||
|
}
|
||||||
|
// Reading value at this point will cause a panic
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Internally
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cell::{Cell, Ref, RefCell, RefMut},
|
||||||
|
fmt::Debug,
|
||||||
|
marker::PhantomData,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bumpalo::Bump;
|
||||||
|
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// let data = String::from("hello world");
|
||||||
|
/// let store = Store::default();
|
||||||
|
/// let owner = store.owner();
|
||||||
|
/// let key = owner.insert(&data);
|
||||||
|
/// drop(data);
|
||||||
|
/// assert_eq!(*key.read(), "hello world");
|
||||||
|
/// ```
|
||||||
|
#[allow(unused)]
|
||||||
|
fn compile_fail() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reused() {
|
||||||
|
let store = Store::default();
|
||||||
|
let first_ptr;
|
||||||
|
{
|
||||||
|
let owner = store.owner();
|
||||||
|
first_ptr = owner.insert(1).raw.data.as_ptr();
|
||||||
|
drop(owner);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let owner = store.owner();
|
||||||
|
let second_ptr = owner.insert(1234).raw.data.as_ptr();
|
||||||
|
assert_eq!(first_ptr, second_ptr);
|
||||||
|
drop(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaking_is_ok() {
|
||||||
|
let data = String::from("hello world");
|
||||||
|
let store = Store::default();
|
||||||
|
let key;
|
||||||
|
{
|
||||||
|
// create an owner
|
||||||
|
let owner = store.owner();
|
||||||
|
// insert data into the store
|
||||||
|
key = owner.insert(data);
|
||||||
|
// don't drop the owner
|
||||||
|
std::mem::forget(owner);
|
||||||
|
}
|
||||||
|
assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drops() {
|
||||||
|
let data = String::from("hello world");
|
||||||
|
let store = Store::default();
|
||||||
|
let key;
|
||||||
|
{
|
||||||
|
// create an owner
|
||||||
|
let owner = store.owner();
|
||||||
|
// insert data into the store
|
||||||
|
key = owner.insert(data);
|
||||||
|
// drop the owner
|
||||||
|
}
|
||||||
|
assert!(key.try_read().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn works() {
|
||||||
|
let store = Store::default();
|
||||||
|
let owner = store.owner();
|
||||||
|
let key = owner.insert(1);
|
||||||
|
|
||||||
|
assert_eq!(*key.read(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_while_reading() {
|
||||||
|
let store = Store::default();
|
||||||
|
let owner = store.owner();
|
||||||
|
let key;
|
||||||
|
{
|
||||||
|
let data: String = "hello world".to_string();
|
||||||
|
key = owner.insert(data);
|
||||||
|
}
|
||||||
|
let value = key.read();
|
||||||
|
owner.insert(&1);
|
||||||
|
assert_eq!(*value, "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn panics() {
|
||||||
|
let store = Store::default();
|
||||||
|
let owner = store.owner();
|
||||||
|
let key = owner.insert(1);
|
||||||
|
drop(owner);
|
||||||
|
|
||||||
|
assert_eq!(*key.read(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzz() {
|
||||||
|
fn maybe_owner_scope(
|
||||||
|
store: &Store,
|
||||||
|
valid_keys: &mut Vec<GenerationalBox<String>>,
|
||||||
|
invalid_keys: &mut Vec<GenerationalBox<String>>,
|
||||||
|
path: &mut Vec<u8>,
|
||||||
|
) {
|
||||||
|
let branch_cutoff = 5;
|
||||||
|
let children = if path.len() < branch_cutoff {
|
||||||
|
rand::random::<u8>() % 4
|
||||||
|
} else {
|
||||||
|
rand::random::<u8>() % 2
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..children {
|
||||||
|
let owner = store.owner();
|
||||||
|
let key = owner.insert(format!("hello world {path:?}"));
|
||||||
|
valid_keys.push(key);
|
||||||
|
path.push(i);
|
||||||
|
// read all keys
|
||||||
|
println!("{:?}", path);
|
||||||
|
for key in valid_keys.iter() {
|
||||||
|
let value = key.read();
|
||||||
|
println!("{:?}", value);
|
||||||
|
assert!(value.starts_with("hello world"));
|
||||||
|
}
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
for key in invalid_keys.iter() {
|
||||||
|
assert!(!key.validate());
|
||||||
|
}
|
||||||
|
maybe_owner_scope(store, valid_keys, invalid_keys, path);
|
||||||
|
invalid_keys.push(valid_keys.pop().unwrap());
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
let store = Store::default();
|
||||||
|
maybe_owner_scope(&store, &mut Vec::new(), &mut Vec::new(), &mut Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The core Copy state type. The generational box will be dropped when the [Owner] is dropped.
|
||||||
|
pub struct GenerationalBox<T> {
|
||||||
|
raw: MemoryLocation,
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
generation: u32,
|
||||||
|
_marker: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Debug for GenerationalBox<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{:?}@{:?}",
|
||||||
|
self.raw.data.as_ptr(),
|
||||||
|
self.generation
|
||||||
|
))?;
|
||||||
|
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||||
|
f.write_fmt(format_args!("{:?}", self.raw.data.as_ptr()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> GenerationalBox<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate(&self) -> bool {
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
{
|
||||||
|
self.raw.generation.get() == self.generation
|
||||||
|
}
|
||||||
|
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||||
|
{
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to read the value. Returns None if the value is no longer valid.
|
||||||
|
pub fn try_read(&self) -> Option<Ref<'_, T>> {
|
||||||
|
self.validate()
|
||||||
|
.then(|| {
|
||||||
|
Ref::filter_map(self.raw.data.borrow(), |any| {
|
||||||
|
any.as_ref()?.downcast_ref::<T>()
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the value. Panics if the value is no longer valid.
|
||||||
|
pub fn read(&self) -> Ref<'_, T> {
|
||||||
|
self.try_read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to write the value. Returns None if the value is no longer valid.
|
||||||
|
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
|
||||||
|
self.validate()
|
||||||
|
.then(|| {
|
||||||
|
RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
|
||||||
|
any.as_mut()?.downcast_mut::<T>()
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the value. Panics if the value is no longer valid.
|
||||||
|
pub fn write(&self) -> RefMut<'_, T> {
|
||||||
|
self.try_write().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value. Panics if the value is no longer valid.
|
||||||
|
pub fn set(&self, value: T) {
|
||||||
|
self.validate().then(|| {
|
||||||
|
*self.raw.data.borrow_mut() = Some(Box::new(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the pointer is equal to the other pointer.
|
||||||
|
pub fn ptr_eq(&self, other: &Self) -> bool {
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
{
|
||||||
|
self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation
|
||||||
|
}
|
||||||
|
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||||
|
{
|
||||||
|
self.raw.data.as_ptr() == other.raw.data.as_ptr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for GenerationalBox<T> {}
|
||||||
|
|
||||||
|
impl<T> Clone for GenerationalBox<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct MemoryLocation {
|
||||||
|
data: &'static RefCell<Option<Box<dyn std::any::Any>>>,
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
generation: &'static Cell<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryLocation {
|
||||||
|
#[allow(unused)]
|
||||||
|
fn drop(&self) {
|
||||||
|
let old = self.data.borrow_mut().take();
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
if old.is_some() {
|
||||||
|
drop(old);
|
||||||
|
let new_generation = self.generation.get() + 1;
|
||||||
|
self.generation.set(new_generation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace<T: 'static>(&mut self, value: T) -> GenerationalBox<T> {
|
||||||
|
let mut inner_mut = self.data.borrow_mut();
|
||||||
|
|
||||||
|
let raw = Box::new(value);
|
||||||
|
let old = inner_mut.replace(raw);
|
||||||
|
assert!(old.is_none());
|
||||||
|
GenerationalBox {
|
||||||
|
raw: *self,
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
generation: self.generation.get(),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Store {
|
||||||
|
bump: &'static Bump,
|
||||||
|
recycled: Rc<RefCell<Vec<MemoryLocation>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Store {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bump: Box::leak(Box::new(Bump::new())),
|
||||||
|
recycled: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
fn recycle(&self, location: MemoryLocation) {
|
||||||
|
location.drop();
|
||||||
|
self.recycled.borrow_mut().push(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn claim(&self) -> MemoryLocation {
|
||||||
|
if let Some(location) = self.recycled.borrow_mut().pop() {
|
||||||
|
location
|
||||||
|
} else {
|
||||||
|
let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None));
|
||||||
|
MemoryLocation {
|
||||||
|
data,
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
generation: self.bump.alloc(Cell::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates.
|
||||||
|
pub fn owner(&self) -> Owner {
|
||||||
|
Owner {
|
||||||
|
store: self.clone(),
|
||||||
|
owned: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped.
|
||||||
|
pub struct Owner {
|
||||||
|
store: Store,
|
||||||
|
owned: Rc<RefCell<Vec<MemoryLocation>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Owner {
|
||||||
|
/// Insert a value into the store. The value will be dropped when the owner is dropped.
|
||||||
|
pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T> {
|
||||||
|
let mut location = self.store.claim();
|
||||||
|
let key = location.replace(value);
|
||||||
|
self.owned.borrow_mut().push(location);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
|
||||||
|
pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
|
||||||
|
let location = self.store.claim();
|
||||||
|
GenerationalBox {
|
||||||
|
raw: location,
|
||||||
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
|
generation: location.generation.get(),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Owner {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for location in self.owned.borrow().iter() {
|
||||||
|
self.store.recycle(*location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Tracked and computed state in Dioxus
|
||||||
|
|
||||||
use dioxus_core::{ScopeId, ScopeState};
|
use dioxus_core::{ScopeId, ScopeState};
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{
|
use std::{
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ macro_rules! to_owned {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mod computed;
|
pub mod computed;
|
||||||
pub use computed::*;
|
|
||||||
|
|
||||||
mod use_on_unmount;
|
mod use_on_unmount;
|
||||||
pub use use_on_unmount::*;
|
pub use use_on_unmount::*;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator {
|
|||||||
Rc::new(move |script: &str| {
|
Rc::new(move |script: &str| {
|
||||||
eval_provider
|
eval_provider
|
||||||
.new_evaluator(script.to_string())
|
.new_evaluator(script.to_string())
|
||||||
.map(|evaluator| UseEval::new(evaluator))
|
.map(UseEval::new)
|
||||||
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>
|
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,7 +446,6 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||||||
drop(tree);
|
drop(tree);
|
||||||
children.reverse();
|
children.reverse();
|
||||||
if let Some(node) = self.get_mut(id) {
|
if let Some(node) = self.get_mut(id) {
|
||||||
let node = node;
|
|
||||||
f(node);
|
f(node);
|
||||||
stack.extend(children.iter());
|
stack.extend(children.iter());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,10 +288,7 @@ impl DynamicMapping {
|
|||||||
let idx = self.last_attribute_idx;
|
let idx = self.last_attribute_idx;
|
||||||
self.last_attribute_idx += 1;
|
self.last_attribute_idx += 1;
|
||||||
|
|
||||||
self.attribute_to_idx
|
self.attribute_to_idx.entry(attr).or_default().push(idx);
|
||||||
.entry(attr)
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(idx);
|
|
||||||
|
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
@@ -300,10 +297,7 @@ impl DynamicMapping {
|
|||||||
let idx = self.last_element_idx;
|
let idx = self.last_element_idx;
|
||||||
self.last_element_idx += 1;
|
self.last_element_idx += 1;
|
||||||
|
|
||||||
self.node_to_idx
|
self.node_to_idx.entry(node).or_default().push(idx);
|
||||||
.entry(node)
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(idx);
|
|
||||||
|
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,16 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
slab = { workspace = true }
|
generational-box = { workspace = true }
|
||||||
|
log.workspace = true
|
||||||
|
simple_logger = "4.2.0"
|
||||||
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dioxus = { workspace = true }
|
||||||
|
dioxus-desktop = { workspace = true }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
serialize = ["serde"]
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Dioxus Signals
|
||||||
|
|
||||||
|
Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions.
|
||||||
|
|
||||||
|
## Copy Data
|
||||||
|
|
||||||
|
All signals implement Copy, even if the inner value does not implement copy. This makes it easy to move any data into futures or children.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let signal = use_signal(cx, || "hello world".to_string());
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
// signal is Copy even though String is not copy
|
||||||
|
print!("{signal}");
|
||||||
|
});
|
||||||
|
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Subscriptions
|
||||||
|
|
||||||
|
Signals will only subscribe to components when you read from the signal in that component. It will never subscribe to a component when reading data in a future or event handler.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
// Because signal is never read in this component, this component will not rerun when the signal changes
|
||||||
|
let signal = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*signal.write() += 1;
|
||||||
|
},
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
for id in 0..10 {
|
||||||
|
Child {
|
||||||
|
signal: signal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
struct ChildProps {
|
||||||
|
signal: Signal<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
// This component does read from the signal, so when the signal changes it will rerun
|
||||||
|
render! {
|
||||||
|
"{cx.props.signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because subscriptions happen when you read from (not create) the data, you can provide signals through the normal context API:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
// Because signal is never read in this component, this component will not rerun when the signal changes
|
||||||
|
use_context_provider(cx, || Signal::new(0));
|
||||||
|
|
||||||
|
render! {
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope) -> Element {
|
||||||
|
let signal: Signal<i32> = *use_context(cx).unwrap();
|
||||||
|
// This component does read from the signal, so when the signal changes it will rerun
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Computed Data
|
||||||
|
|
||||||
|
In addition to local subscriptions in components, `dioxus-signals` provides a way to derive data with local subscriptions.
|
||||||
|
|
||||||
|
The use_selector hook will only rerun when any signals inside of the hook change:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let signal = use_signal(cx, || 0);
|
||||||
|
let doubled = use_selector(cx, || signal * 2);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| *signal.write() += 1,
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
Child {
|
||||||
|
signal: signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn Child(cx: Scope, signal: ReadOnlySignal<usize>) -> Element {
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::Signal;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
// Because signal is never read in this component, this component will not rerun when the signal changes
|
||||||
|
use_context_provider(cx, || Signal::new(0));
|
||||||
|
|
||||||
|
render! {
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope) -> Element {
|
||||||
|
let signal: Signal<i32> = *use_context(cx).unwrap();
|
||||||
|
// This component does read from the signal, so when the signal changes it will rerun
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let signal = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
use_future!(cx, || async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
*signal.write() += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let local_state = use_state(cx, || 0);
|
||||||
|
let computed =
|
||||||
|
use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| {
|
||||||
|
local_state * 2 + signal.value()
|
||||||
|
});
|
||||||
|
println!("Running app");
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
local_state.set(local_state.get() + 1);
|
||||||
|
},
|
||||||
|
"Add one"
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
"{computed}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let signal = use_signal(cx, || 0);
|
||||||
|
let doubled = use_selector(cx, move || signal * 2);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| *signal.write() += 1,
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
Child {
|
||||||
|
signal: doubled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline_props]
|
||||||
|
fn Child(cx: Scope, signal: ReadOnlySignal<usize>) -> Element {
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::Signal;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct ApplicationData {
|
||||||
|
first_data: Signal<i32>,
|
||||||
|
second_data: Signal<i32>,
|
||||||
|
many_signals: Signal<Vec<Signal<i32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_app_data(cx: Scope) -> ApplicationData {
|
||||||
|
*use_context(cx).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
use_context_provider(cx, ApplicationData::default);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
ReadsFirst {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
ReadsSecond {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
ReadsManySignals {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ReadsFirst(cx: Scope) -> Element {
|
||||||
|
println!("running first");
|
||||||
|
let data = use_app_data(cx);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.first_data.write() += 1;
|
||||||
|
},
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.first_data.write() -= 1;
|
||||||
|
},
|
||||||
|
"Decrease"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.first_data.write() = 0;
|
||||||
|
},
|
||||||
|
"Reset"
|
||||||
|
}
|
||||||
|
"{data.first_data}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ReadsSecond(cx: Scope) -> Element {
|
||||||
|
println!("running second");
|
||||||
|
let data = use_app_data(cx);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.second_data.write() += 1;
|
||||||
|
},
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.second_data.write() -= 1;
|
||||||
|
},
|
||||||
|
"Decrease"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*data.second_data.write() = 0;
|
||||||
|
},
|
||||||
|
"Reset"
|
||||||
|
}
|
||||||
|
"{data.second_data}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ReadsManySignals(cx: Scope) -> Element {
|
||||||
|
println!("running many signals");
|
||||||
|
let data = use_app_data(cx);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
data.many_signals.write().push(Signal::new(0));
|
||||||
|
},
|
||||||
|
"Create"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
data.many_signals.write().pop();
|
||||||
|
},
|
||||||
|
"Destroy"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
if let Some(first) = data.many_signals.read().get(0) {
|
||||||
|
*first.write() += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Increase First Item"
|
||||||
|
}
|
||||||
|
for signal in data.many_signals {
|
||||||
|
Child {
|
||||||
|
count: signal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, PartialEq)]
|
||||||
|
struct ChildProps {
|
||||||
|
count: Signal<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
println!("running child");
|
||||||
|
let count = cx.props.count;
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
"Child: {count}"
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*count.write() += 1;
|
||||||
|
},
|
||||||
|
"Increase"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
*count.write() -= 1;
|
||||||
|
},
|
||||||
|
"Decrease"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/// A dependency is a trait that can be used to determine if a effect or selector should be re-run.
|
||||||
|
pub trait Dependency: Sized + Clone {
|
||||||
|
/// The output of the dependency
|
||||||
|
type Out: Clone + PartialEq;
|
||||||
|
/// Returns the output of the dependency.
|
||||||
|
fn out(&self) -> Self::Out;
|
||||||
|
/// Returns true if the dependency has changed.
|
||||||
|
fn changed(&self, other: &Self::Out) -> bool {
|
||||||
|
self.out() != *other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dependency for () {
|
||||||
|
type Out = ();
|
||||||
|
fn out(&self) -> Self::Out {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dependency is a trait that can be used to determine if a effect or selector should be re-run.
|
||||||
|
pub trait Dep: 'static + PartialEq + Clone {}
|
||||||
|
impl<T> Dep for T where T: 'static + PartialEq + Clone {}
|
||||||
|
|
||||||
|
impl<A: Dep> Dependency for &A {
|
||||||
|
type Out = A;
|
||||||
|
fn out(&self) -> Self::Out {
|
||||||
|
(*self).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_dep {
|
||||||
|
(
|
||||||
|
$($el:ident=$name:ident $other:ident,)*
|
||||||
|
) => {
|
||||||
|
impl< $($el),* > Dependency for ($(&$el,)*)
|
||||||
|
where
|
||||||
|
$(
|
||||||
|
$el: Dep
|
||||||
|
),*
|
||||||
|
{
|
||||||
|
type Out = ($($el,)*);
|
||||||
|
|
||||||
|
fn out(&self) -> Self::Out {
|
||||||
|
let ($($name,)*) = self;
|
||||||
|
($((*$name).clone(),)*)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed(&self, other: &Self::Out) -> bool {
|
||||||
|
let ($($name,)*) = self;
|
||||||
|
let ($($other,)*) = other;
|
||||||
|
$(
|
||||||
|
if *$name != $other {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_dep!(A = a1 a2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2,);
|
||||||
|
impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2, H = h1 h2,);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use core::{self, fmt::Debug};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fmt::{self, Formatter};
|
||||||
|
use std::rc::Rc;
|
||||||
|
//
|
||||||
|
use dioxus_core::prelude::*;
|
||||||
|
|
||||||
|
use crate::use_signal;
|
||||||
|
use crate::{dependency::Dependency, CopyValue};
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct EffectStack {
|
||||||
|
pub(crate) effects: Rc<RefCell<Vec<Effect>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_effect_stack() -> EffectStack {
|
||||||
|
match consume_context() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
let store = EffectStack::default();
|
||||||
|
provide_root_context(store).expect("in a virtual dom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
|
||||||
|
/// The signal will be owned by the current component and will be dropped when the component is dropped.
|
||||||
|
pub fn use_effect(cx: &ScopeState, callback: impl FnMut() + 'static) {
|
||||||
|
cx.use_hook(|| Effect::new(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
|
||||||
|
/// The signal will be owned by the current component and will be dropped when the component is dropped.
|
||||||
|
pub fn use_effect_with_dependencies<D: Dependency>(
|
||||||
|
cx: &ScopeState,
|
||||||
|
dependencies: D,
|
||||||
|
mut callback: impl FnMut(D::Out) + 'static,
|
||||||
|
) where
|
||||||
|
D::Out: 'static,
|
||||||
|
{
|
||||||
|
let dependencies_signal = use_signal(cx, || dependencies.out());
|
||||||
|
cx.use_hook(|| {
|
||||||
|
Effect::new(move || {
|
||||||
|
let deref = &*dependencies_signal.read();
|
||||||
|
callback(deref.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let changed = { dependencies.changed(&*dependencies_signal.read()) };
|
||||||
|
if changed {
|
||||||
|
dependencies_signal.set(dependencies.out());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effects allow you to run code when a signal changes. Effects are run immediately and whenever any signal it reads changes.
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub struct Effect {
|
||||||
|
pub(crate) callback: CopyValue<Box<dyn FnMut()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Effect {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_fmt(format_args!("{:?}", self.callback.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Effect {
|
||||||
|
pub(crate) fn current() -> Option<Self> {
|
||||||
|
get_effect_stack().effects.borrow().last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
|
||||||
|
///
|
||||||
|
/// The signal will be owned by the current component and will be dropped when the component is dropped.
|
||||||
|
pub fn new(callback: impl FnMut() + 'static) -> Self {
|
||||||
|
let myself = Self {
|
||||||
|
callback: CopyValue::new(Box::new(callback)),
|
||||||
|
};
|
||||||
|
|
||||||
|
myself.try_run();
|
||||||
|
|
||||||
|
myself
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the effect callback immediately. Returns `true` if the effect was run. Returns `false` is the effect is dead.
|
||||||
|
pub fn try_run(&self) {
|
||||||
|
if let Some(mut callback) = self.callback.try_write() {
|
||||||
|
{
|
||||||
|
get_effect_stack().effects.borrow_mut().push(*self);
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
{
|
||||||
|
get_effect_stack().effects.borrow_mut().pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
use crate::rt::CopyValue;
|
||||||
|
use crate::signal::{ReadOnlySignal, Signal, Write};
|
||||||
|
|
||||||
|
use std::cell::{Ref, RefMut};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
ops::{Add, Div, Mul, Sub},
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! read_impls {
|
||||||
|
($ty:ident) => {
|
||||||
|
impl<T: Default + 'static> Default for $ty<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::clone::Clone for $ty<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for $ty<T> {}
|
||||||
|
|
||||||
|
impl<T: Display + 'static> Display for $ty<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.with(|v| Display::fmt(v, f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug + 'static> Debug for $ty<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.with(|v| Debug::fmt(v, f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> $ty<Vec<T>> {
|
||||||
|
/// Read a value from the inner vector.
|
||||||
|
pub fn get(&self, index: usize) -> Option<Ref<'_, T>> {
|
||||||
|
Ref::filter_map(self.read(), |v| v.get(index)).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> $ty<Option<T>> {
|
||||||
|
/// Unwraps the inner value and clones it.
|
||||||
|
pub fn unwrap(&self) -> T
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
self.with(|v| v.clone()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attemps to read the inner value of the Option.
|
||||||
|
pub fn as_ref(&self) -> Option<Ref<'_, T>> {
|
||||||
|
Ref::filter_map(self.read(), |v| v.as_ref()).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! write_impls {
|
||||||
|
($ty:ident) => {
|
||||||
|
impl<T: Add<Output = T> + Copy + 'static> std::ops::Add<T> for $ty<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn add(self, rhs: T) -> Self::Output {
|
||||||
|
self.with(|v| *v + rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Add<Output = T> + Copy + 'static> std::ops::AddAssign<T> for $ty<T> {
|
||||||
|
fn add_assign(&mut self, rhs: T) {
|
||||||
|
self.with_mut(|v| *v = *v + rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Sub<Output = T> + Copy + 'static> std::ops::SubAssign<T> for $ty<T> {
|
||||||
|
fn sub_assign(&mut self, rhs: T) {
|
||||||
|
self.with_mut(|v| *v = *v - rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Sub<Output = T> + Copy + 'static> std::ops::Sub<T> for $ty<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn sub(self, rhs: T) -> Self::Output {
|
||||||
|
self.with(|v| *v - rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Mul<Output = T> + Copy + 'static> std::ops::MulAssign<T> for $ty<T> {
|
||||||
|
fn mul_assign(&mut self, rhs: T) {
|
||||||
|
self.with_mut(|v| *v = *v * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Mul<Output = T> + Copy + 'static> std::ops::Mul<T> for $ty<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn mul(self, rhs: T) -> Self::Output {
|
||||||
|
self.with(|v| *v * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Div<Output = T> + Copy + 'static> std::ops::DivAssign<T> for $ty<T> {
|
||||||
|
fn div_assign(&mut self, rhs: T) {
|
||||||
|
self.with_mut(|v| *v = *v / rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Div<Output = T> + Copy + 'static> std::ops::Div<T> for $ty<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn div(self, rhs: T) -> Self::Output {
|
||||||
|
self.with(|v| *v / rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> $ty<Vec<T>> {
|
||||||
|
/// Pushes a new value to the end of the vector.
|
||||||
|
pub fn push(&self, value: T) {
|
||||||
|
self.with_mut(|v| v.push(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops the last value from the vector.
|
||||||
|
pub fn pop(&self) -> Option<T> {
|
||||||
|
self.with_mut(|v| v.pop())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a new value at the given index.
|
||||||
|
pub fn insert(&self, index: usize, value: T) {
|
||||||
|
self.with_mut(|v| v.insert(index, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the value at the given index.
|
||||||
|
pub fn remove(&self, index: usize) -> T {
|
||||||
|
self.with_mut(|v| v.remove(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the vector, removing all values.
|
||||||
|
pub fn clear(&self) {
|
||||||
|
self.with_mut(|v| v.clear())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extends the vector with the given iterator.
|
||||||
|
pub fn extend(&self, iter: impl IntoIterator<Item = T>) {
|
||||||
|
self.with_mut(|v| v.extend(iter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncates the vector to the given length.
|
||||||
|
pub fn truncate(&self, len: usize) {
|
||||||
|
self.with_mut(|v| v.truncate(len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swaps two values in the vector.
|
||||||
|
pub fn swap_remove(&self, index: usize) -> T {
|
||||||
|
self.with_mut(|v| v.swap_remove(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retains only the values that match the given predicate.
|
||||||
|
pub fn retain(&self, f: impl FnMut(&T) -> bool) {
|
||||||
|
self.with_mut(|v| v.retain(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the vector into two at the given index.
|
||||||
|
pub fn split_off(&self, at: usize) -> Vec<T> {
|
||||||
|
self.with_mut(|v| v.split_off(at))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> $ty<Option<T>> {
|
||||||
|
/// Takes the value out of the Option.
|
||||||
|
pub fn take(&self) -> Option<T> {
|
||||||
|
self.with_mut(|v| v.take())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the value in the Option.
|
||||||
|
pub fn replace(&self, value: T) -> Option<T> {
|
||||||
|
self.with_mut(|v| v.replace(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the value out of the Option, or inserts the given value if the Option is empty.
|
||||||
|
pub fn get_or_insert(&self, default: T) -> Ref<'_, T> {
|
||||||
|
self.get_or_insert_with(|| default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty.
|
||||||
|
pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> {
|
||||||
|
let borrow = self.read();
|
||||||
|
if borrow.is_none() {
|
||||||
|
drop(borrow);
|
||||||
|
self.with_mut(|v| *v = Some(default()));
|
||||||
|
Ref::map(self.read(), |v| v.as_ref().unwrap())
|
||||||
|
} else {
|
||||||
|
Ref::map(borrow, |v| v.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
read_impls!(CopyValue);
|
||||||
|
write_impls!(CopyValue);
|
||||||
|
read_impls!(Signal);
|
||||||
|
write_impls!(Signal);
|
||||||
|
read_impls!(ReadOnlySignal);
|
||||||
|
|
||||||
|
/// An iterator over the values of a `CopyValue<Vec<T>>`.
|
||||||
|
pub struct CopyValueIterator<T: 'static> {
|
||||||
|
index: usize,
|
||||||
|
value: CopyValue<Vec<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Iterator for CopyValueIterator<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let index = self.index;
|
||||||
|
self.index += 1;
|
||||||
|
self.value.get(index).map(|v| v.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static> IntoIterator for CopyValue<Vec<T>> {
|
||||||
|
type IntoIter = CopyValueIterator<T>;
|
||||||
|
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
CopyValueIterator {
|
||||||
|
index: 0,
|
||||||
|
value: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> CopyValue<Vec<T>> {
|
||||||
|
/// Write to an element in the inner vector.
|
||||||
|
pub fn get_mut(&self, index: usize) -> Option<RefMut<'_, T>> {
|
||||||
|
RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> CopyValue<Option<T>> {
|
||||||
|
/// Deref the inner value mutably.
|
||||||
|
pub fn as_mut(&self) -> Option<RefMut<'_, T>> {
|
||||||
|
RefMut::filter_map(self.write(), |v| v.as_mut()).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over items in a `Signal<Vec<T>>`.
|
||||||
|
pub struct SignalIterator<T: 'static> {
|
||||||
|
index: usize,
|
||||||
|
value: Signal<Vec<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Iterator for SignalIterator<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let index = self.index;
|
||||||
|
self.index += 1;
|
||||||
|
self.value.get(index).map(|v| v.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static> IntoIterator for Signal<Vec<T>> {
|
||||||
|
type IntoIter = SignalIterator<T>;
|
||||||
|
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
SignalIterator {
|
||||||
|
index: 0,
|
||||||
|
value: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Signal<Vec<T>> {
|
||||||
|
/// Returns a reference to an element or `None` if out of bounds.
|
||||||
|
pub fn get_mut(&self, index: usize) -> Option<Write<'_, T, Vec<T>>> {
|
||||||
|
Write::filter_map(self.write(), |v| v.get_mut(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Signal<Option<T>> {
|
||||||
|
/// Returns a reference to an element or `None` if out of bounds.
|
||||||
|
pub fn as_mut(&self) -> Option<Write<'_, T, Option<T>>> {
|
||||||
|
Write::filter_map(self.write(), |v| v.as_mut())
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-128
@@ -1,131 +1,14 @@
|
|||||||
use std::{
|
#![doc = include_str!("../README.md")]
|
||||||
cell::{Ref, RefMut},
|
#![warn(missing_docs)]
|
||||||
fmt::Display,
|
|
||||||
marker::PhantomData,
|
|
||||||
ops::{Add, Div, Mul, Sub},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod rt;
|
mod rt;
|
||||||
|
|
||||||
use dioxus_core::ScopeState;
|
|
||||||
pub use rt::*;
|
pub use rt::*;
|
||||||
|
mod effect;
|
||||||
pub fn use_init_signal_rt(cx: &ScopeState) {
|
pub use effect::*;
|
||||||
cx.use_hook(|| {
|
mod impls;
|
||||||
let rt = claim_rt(cx.schedule_update_any());
|
mod selector;
|
||||||
cx.provide_context(rt);
|
pub use selector::*;
|
||||||
});
|
pub(crate) mod signal;
|
||||||
}
|
pub use signal::*;
|
||||||
|
mod dependency;
|
||||||
pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
|
pub use dependency::*;
|
||||||
cx.use_hook(|| {
|
|
||||||
let rt: &'static SignalRt = match cx.consume_context() {
|
|
||||||
Some(rt) => rt,
|
|
||||||
None => cx.provide_context(claim_rt(cx.schedule_update_any())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = rt.init(f());
|
|
||||||
rt.subscribe(id, cx.scope_id());
|
|
||||||
|
|
||||||
struct SignalHook<T> {
|
|
||||||
signal: Signal<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Drop for SignalHook<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.signal.rt.remove(self.signal.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalHook {
|
|
||||||
signal: Signal {
|
|
||||||
id,
|
|
||||||
rt,
|
|
||||||
t: PhantomData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.signal
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Signal<T> {
|
|
||||||
id: usize,
|
|
||||||
rt: &'static SignalRt,
|
|
||||||
t: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static> Signal<T> {
|
|
||||||
pub fn read(&self) -> Ref<T> {
|
|
||||||
self.rt.read(self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(&self) -> RefMut<T> {
|
|
||||||
self.rt.write(self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(&mut self, value: T) {
|
|
||||||
self.rt.set(self.id, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
|
||||||
let write = self.read();
|
|
||||||
f(&*write)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update<O>(&self, _f: impl FnOnce(&mut T) -> O) -> O {
|
|
||||||
let mut write = self.write();
|
|
||||||
_f(&mut *write)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone + 'static> Signal<T> {
|
|
||||||
pub fn get(&self) -> T {
|
|
||||||
self.rt.get(self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone + 'static> std::ops::Deref for Signal<T> {
|
|
||||||
type Target = dyn Fn() -> T;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.rt.getter(self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> std::clone::Clone for Signal<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Copy for Signal<T> {}
|
|
||||||
|
|
||||||
impl<T: Display + 'static> Display for Signal<T> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.rt.with::<T, _>(self.id, |v| T::fmt(v, f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Add<Output = T> + Copy + 'static> std::ops::AddAssign<T> for Signal<T> {
|
|
||||||
fn add_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.get() + rhs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Sub<Output = T> + Copy + 'static> std::ops::SubAssign<T> for Signal<T> {
|
|
||||||
fn sub_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.get() - rhs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Mul<Output = T> + Copy + 'static> std::ops::MulAssign<T> for Signal<T> {
|
|
||||||
fn mul_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.get() * rhs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Div<Output = T> + Copy + 'static> std::ops::DivAssign<T> for Signal<T> {
|
|
||||||
fn div_assign(&mut self, rhs: T) {
|
|
||||||
self.set(self.get() / rhs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+134
-96
@@ -1,121 +1,159 @@
|
|||||||
use std::{any::Any, cell::RefCell, sync::Arc};
|
use std::cell::{Ref, RefMut};
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus_core::prelude::{
|
||||||
|
consume_context, consume_context_from_scope, current_scope_id, provide_context,
|
||||||
|
provide_context_to_scope, provide_root_context,
|
||||||
|
};
|
||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use slab::Slab;
|
|
||||||
|
|
||||||
thread_local! {
|
use generational_box::{GenerationalBox, Owner, Store};
|
||||||
// we cannot drop these since any future might be using them
|
|
||||||
static RUNTIMES: RefCell<Vec<&'static SignalRt>> = RefCell::new(Vec::new());
|
fn current_store() -> Store {
|
||||||
|
match consume_context() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
let store = Store::default();
|
||||||
|
provide_root_context(store).expect("in a virtual dom")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide the runtime for signals
|
fn current_owner() -> Rc<Owner> {
|
||||||
|
match consume_context() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
let owner = Rc::new(current_store().owner());
|
||||||
|
provide_context(owner).expect("in a virtual dom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owner_in_scope(scope: ScopeId) -> Rc<Owner> {
|
||||||
|
match consume_context_from_scope(scope) {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
let owner = Rc::new(current_store().owner());
|
||||||
|
provide_context_to_scope(scope, owner).expect("in a virtual dom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CopyValue is a wrapper around a value to make the value mutable and Copy.
|
||||||
///
|
///
|
||||||
/// This will reuse dead runtimes
|
/// It is internally backed by [`generational_box::GenerationalBox`].
|
||||||
pub fn claim_rt(update_any: Arc<dyn Fn(ScopeId)>) -> &'static SignalRt {
|
pub struct CopyValue<T: 'static> {
|
||||||
RUNTIMES.with(|runtimes| {
|
pub(crate) value: GenerationalBox<T>,
|
||||||
if let Some(rt) = runtimes.borrow_mut().pop() {
|
origin_scope: ScopeId,
|
||||||
return rt;
|
|
||||||
}
|
|
||||||
|
|
||||||
Box::leak(Box::new(SignalRt {
|
|
||||||
signals: RefCell::new(Slab::new()),
|
|
||||||
update_any,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push this runtime into the global runtime list
|
#[cfg(feature = "serde")]
|
||||||
pub fn reclam_rt(_rt: &'static SignalRt) {
|
impl<T: 'static> serde::Serialize for CopyValue<T>
|
||||||
RUNTIMES.with(|runtimes| {
|
where
|
||||||
runtimes.borrow_mut().push(_rt);
|
T: serde::Serialize,
|
||||||
});
|
{
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
self.value.read().serialize(serializer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SignalRt {
|
#[cfg(feature = "serde")]
|
||||||
pub(crate) signals: RefCell<Slab<Inner>>,
|
impl<'de, T: 'static> serde::Deserialize<'de> for CopyValue<T>
|
||||||
pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
|
where
|
||||||
|
T: serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let value = T::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok(Self::new(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignalRt {
|
impl<T: 'static> CopyValue<T> {
|
||||||
pub fn init<T: 'static>(&'static self, val: T) -> usize {
|
/// Create a new CopyValue. The value will be stored in the current component.
|
||||||
self.signals.borrow_mut().insert(Inner {
|
///
|
||||||
value: Box::new(val),
|
/// Once the component this value is created in is dropped, the value will be dropped.
|
||||||
subscribers: Vec::new(),
|
pub fn new(value: T) -> Self {
|
||||||
getter: None,
|
let owner = current_owner();
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe(&self, id: usize, subscriber: ScopeId) {
|
Self {
|
||||||
self.signals.borrow_mut()[id].subscribers.push(subscriber);
|
value: owner.insert(value),
|
||||||
}
|
origin_scope: current_scope_id().expect("in a virtual dom"),
|
||||||
|
|
||||||
pub fn get<T: Clone + 'static>(&self, id: usize) -> T {
|
|
||||||
self.signals.borrow()[id]
|
|
||||||
.value
|
|
||||||
.downcast_ref::<T>()
|
|
||||||
.cloned()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set<T: 'static>(&self, id: usize, value: T) {
|
|
||||||
let mut signals = self.signals.borrow_mut();
|
|
||||||
let inner = &mut signals[id];
|
|
||||||
inner.value = Box::new(value);
|
|
||||||
|
|
||||||
for subscriber in inner.subscribers.iter() {
|
|
||||||
(self.update_any)(*subscriber);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, id: usize) {
|
/// Create a new CopyValue. The value will be stored in the given scope. When the specified scope is dropped, the value will be dropped.
|
||||||
self.signals.borrow_mut().remove(id);
|
pub fn new_in_scope(value: T, scope: ScopeId) -> Self {
|
||||||
}
|
let owner = owner_in_scope(scope);
|
||||||
|
|
||||||
pub fn with<T: 'static, O>(&self, id: usize, f: impl FnOnce(&T) -> O) -> O {
|
Self {
|
||||||
let signals = self.signals.borrow();
|
value: owner.insert(value),
|
||||||
let inner = &signals[id];
|
origin_scope: scope,
|
||||||
let inner = inner.value.downcast_ref::<T>().unwrap();
|
|
||||||
f(inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn read<T: 'static>(&self, id: usize) -> std::cell::Ref<T> {
|
|
||||||
let signals = self.signals.borrow();
|
|
||||||
std::cell::Ref::map(signals, |signals| {
|
|
||||||
signals[id].value.downcast_ref::<T>().unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn write<T: 'static>(&self, id: usize) -> std::cell::RefMut<T> {
|
|
||||||
let signals = self.signals.borrow_mut();
|
|
||||||
std::cell::RefMut::map(signals, |signals| {
|
|
||||||
signals[id].value.downcast_mut::<T>().unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn getter<T: 'static + Clone>(&self, id: usize) -> &dyn Fn() -> T {
|
|
||||||
let mut signals = self.signals.borrow_mut();
|
|
||||||
let inner = &mut signals[id];
|
|
||||||
let r = inner.getter.as_mut();
|
|
||||||
|
|
||||||
if r.is_none() {
|
|
||||||
let rt = self;
|
|
||||||
let r = move || rt.get::<T>(id);
|
|
||||||
let getter: Box<dyn Fn() -> T> = Box::new(r);
|
|
||||||
let getter: Box<dyn Fn()> = unsafe { std::mem::transmute(getter) };
|
|
||||||
|
|
||||||
inner.getter = Some(getter);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let r = inner.getter.as_ref().unwrap();
|
pub(crate) fn invalid() -> Self {
|
||||||
|
let owner = current_owner();
|
||||||
|
|
||||||
unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) }
|
Self {
|
||||||
|
value: owner.invalid(),
|
||||||
|
origin_scope: current_scope_id().expect("in a virtual dom"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scope this value was created in.
|
||||||
|
pub fn origin_scope(&self) -> ScopeId {
|
||||||
|
self.origin_scope
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to read the value. If the value has been dropped, this will return None.
|
||||||
|
pub fn try_read(&self) -> Option<Ref<'_, T>> {
|
||||||
|
self.value.try_read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the value. If the value has been dropped, this will panic.
|
||||||
|
pub fn read(&self) -> Ref<'_, T> {
|
||||||
|
self.value.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to write the value. If the value has been dropped, this will return None.
|
||||||
|
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
|
||||||
|
self.value.try_write()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the value. If the value has been dropped, this will panic.
|
||||||
|
pub fn write(&self) -> RefMut<'_, T> {
|
||||||
|
self.value.write()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value. If the value has been dropped, this will panic.
|
||||||
|
pub fn set(&mut self, value: T) {
|
||||||
|
*self.write() = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a function with a reference to the value. If the value has been dropped, this will panic.
|
||||||
|
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||||
|
let write = self.read();
|
||||||
|
f(&*write)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a function with a mutable reference to the value. If the value has been dropped, this will panic.
|
||||||
|
pub fn with_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
|
||||||
|
let mut write = self.write();
|
||||||
|
f(&mut *write)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Inner {
|
impl<T: Clone + 'static> CopyValue<T> {
|
||||||
pub value: Box<dyn Any>,
|
/// Get the value. If the value has been dropped, this will panic.
|
||||||
pub subscribers: Vec<ScopeId>,
|
pub fn value(&self) -> T {
|
||||||
|
self.read().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// todo: this has a soundness hole in it that you might not run into
|
impl<T: 'static> PartialEq for CopyValue<T> {
|
||||||
pub getter: Option<Box<dyn Fn()>>,
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.value.ptr_eq(&other.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use dioxus_core::prelude::*;
|
||||||
|
|
||||||
|
use crate::dependency::Dependency;
|
||||||
|
use crate::use_signal;
|
||||||
|
use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySignal, Signal};
|
||||||
|
|
||||||
|
/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes.
|
||||||
|
///
|
||||||
|
/// Selectors can be used to efficiently compute derived data from signals.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_signals::*;
|
||||||
|
///
|
||||||
|
/// fn App(cx: Scope) -> Element {
|
||||||
|
/// let mut count = use_signal(cx, || 0);
|
||||||
|
/// let double = use_selector(cx, move || count * 2);
|
||||||
|
/// count += 1;
|
||||||
|
/// assert_eq!(double.value(), count * 2);
|
||||||
|
///
|
||||||
|
/// render! { "{double}" }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn use_selector<R: PartialEq>(
|
||||||
|
cx: &ScopeState,
|
||||||
|
f: impl FnMut() -> R + 'static,
|
||||||
|
) -> ReadOnlySignal<R> {
|
||||||
|
*cx.use_hook(|| selector(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
|
||||||
|
///
|
||||||
|
/// Selectors can be used to efficiently compute derived data from signals.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_signals::*;
|
||||||
|
///
|
||||||
|
/// fn App(cx: Scope) -> Element {
|
||||||
|
/// let mut local_state = use_state(cx, || 0);
|
||||||
|
/// let double = use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
|
||||||
|
/// local_state.set(1);
|
||||||
|
///
|
||||||
|
/// render! { "{double}" }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn use_selector_with_dependencies<R: PartialEq, D: Dependency>(
|
||||||
|
cx: &ScopeState,
|
||||||
|
dependencies: D,
|
||||||
|
mut f: impl FnMut(D::Out) -> R + 'static,
|
||||||
|
) -> ReadOnlySignal<R>
|
||||||
|
where
|
||||||
|
D::Out: 'static,
|
||||||
|
{
|
||||||
|
let dependencies_signal = use_signal(cx, || dependencies.out());
|
||||||
|
let selector = *cx.use_hook(|| {
|
||||||
|
selector(move || {
|
||||||
|
let deref = &*dependencies_signal.read();
|
||||||
|
f(deref.clone())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let changed = { dependencies.changed(&*dependencies_signal.read()) };
|
||||||
|
if changed {
|
||||||
|
dependencies_signal.set(dependencies.out());
|
||||||
|
}
|
||||||
|
selector
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes.
|
||||||
|
///
|
||||||
|
/// Selectors can be used to efficiently compute derived data from signals.
|
||||||
|
pub fn selector<R: PartialEq>(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal<R> {
|
||||||
|
let state = Signal::<R> {
|
||||||
|
inner: CopyValue::invalid(),
|
||||||
|
};
|
||||||
|
let effect = Effect {
|
||||||
|
callback: CopyValue::invalid(),
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
get_effect_stack().effects.borrow_mut().push(effect);
|
||||||
|
}
|
||||||
|
state.inner.value.set(SignalData {
|
||||||
|
subscribers: Default::default(),
|
||||||
|
effect_subscribers: Default::default(),
|
||||||
|
update_any: schedule_update_any().expect("in a virtual dom"),
|
||||||
|
value: f(),
|
||||||
|
});
|
||||||
|
{
|
||||||
|
get_effect_stack().effects.borrow_mut().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
effect.callback.value.set(Box::new(move || {
|
||||||
|
let value = f();
|
||||||
|
let changed = {
|
||||||
|
let old = state.inner.read();
|
||||||
|
value != old.value
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
state.set(value)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
ReadOnlySignal::new(state)
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
use std::{
|
||||||
|
cell::{Ref, RefCell, RefMut},
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dioxus_core::{
|
||||||
|
prelude::{current_scope_id, has_context, provide_context, schedule_update_any},
|
||||||
|
ScopeId, ScopeState,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{CopyValue, Effect};
|
||||||
|
|
||||||
|
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_signals::*;
|
||||||
|
///
|
||||||
|
/// fn App(cx: Scope) -> Element {
|
||||||
|
/// let mut count = use_signal(cx, || 0);
|
||||||
|
///
|
||||||
|
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
|
||||||
|
/// // The app component will never be rerendered in this example.
|
||||||
|
/// render! { Child { state: count } }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[inline_props]
|
||||||
|
/// fn Child(cx: Scope, state: Signal<u32>) -> Element {
|
||||||
|
/// let state = *state;
|
||||||
|
///
|
||||||
|
/// use_future!(cx, |()| async move {
|
||||||
|
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
|
||||||
|
/// *state.write() += 1;
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// render! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |_| *state.write() += 1,
|
||||||
|
/// "{state}"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
|
||||||
|
*cx.use_hook(|| Signal::new(f()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Unsubscriber {
|
||||||
|
scope: ScopeId,
|
||||||
|
subscribers: UnsubscriberArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnsubscriberArray = Rc<RefCell<Vec<Rc<RefCell<Vec<ScopeId>>>>>>;
|
||||||
|
|
||||||
|
impl Drop for Unsubscriber {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for subscribers in self.subscribers.borrow().iter() {
|
||||||
|
subscribers.borrow_mut().retain(|s| *s != self.scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_unsubscriber() -> Unsubscriber {
|
||||||
|
match has_context() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
let owner = Unsubscriber {
|
||||||
|
scope: current_scope_id().expect("in a virtual dom"),
|
||||||
|
subscribers: Default::default(),
|
||||||
|
};
|
||||||
|
provide_context(owner).expect("in a virtual dom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SignalData<T> {
|
||||||
|
pub(crate) subscribers: Rc<RefCell<Vec<ScopeId>>>,
|
||||||
|
pub(crate) effect_subscribers: Rc<RefCell<Vec<Effect>>>,
|
||||||
|
pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
|
||||||
|
pub(crate) value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_signals::*;
|
||||||
|
///
|
||||||
|
/// fn App(cx: Scope) -> Element {
|
||||||
|
/// let mut count = use_signal(cx, || 0);
|
||||||
|
///
|
||||||
|
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
|
||||||
|
/// // The app component will never be rerendered in this example.
|
||||||
|
/// render! { Child { state: count } }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[inline_props]
|
||||||
|
/// fn Child(cx: Scope, state: Signal<u32>) -> Element {
|
||||||
|
/// let state = *state;
|
||||||
|
///
|
||||||
|
/// use_future!(cx, |()| async move {
|
||||||
|
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
|
||||||
|
/// *state.write() += 1;
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// render! {
|
||||||
|
/// button {
|
||||||
|
/// onclick: move |_| *state.write() += 1,
|
||||||
|
/// "{state}"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct Signal<T: 'static> {
|
||||||
|
pub(crate) inner: CopyValue<SignalData<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<T: serde::Serialize + 'static> serde::Serialize for Signal<T> {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
self.read().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de, T: serde::Deserialize<'de> + 'static> serde::Deserialize<'de> for Signal<T> {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
Ok(Self::new(T::deserialize(deserializer)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Signal<T> {
|
||||||
|
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: CopyValue::new(SignalData {
|
||||||
|
subscribers: Default::default(),
|
||||||
|
effect_subscribers: Default::default(),
|
||||||
|
update_any: schedule_update_any().expect("in a virtual dom"),
|
||||||
|
value,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scope the signal was created in.
|
||||||
|
pub fn origin_scope(&self) -> ScopeId {
|
||||||
|
self.inner.origin_scope()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of the signal. This will subscribe the current scope to the signal.
|
||||||
|
/// If the signal has been dropped, this will panic.
|
||||||
|
pub fn read(&self) -> Ref<T> {
|
||||||
|
let inner = self.inner.read();
|
||||||
|
if let Some(effect) = Effect::current() {
|
||||||
|
let mut effect_subscribers = inner.effect_subscribers.borrow_mut();
|
||||||
|
if !effect_subscribers.contains(&effect) {
|
||||||
|
effect_subscribers.push(effect);
|
||||||
|
}
|
||||||
|
} else if let Some(current_scope_id) = current_scope_id() {
|
||||||
|
// only subscribe if the vdom is rendering
|
||||||
|
if dioxus_core::vdom_is_rendering() {
|
||||||
|
log::trace!(
|
||||||
|
"{:?} subscribed to {:?}",
|
||||||
|
self.inner.value,
|
||||||
|
current_scope_id
|
||||||
|
);
|
||||||
|
let mut subscribers = inner.subscribers.borrow_mut();
|
||||||
|
if !subscribers.contains(¤t_scope_id) {
|
||||||
|
subscribers.push(current_scope_id);
|
||||||
|
drop(subscribers);
|
||||||
|
let unsubscriber = current_unsubscriber();
|
||||||
|
inner.subscribers.borrow_mut().push(unsubscriber.scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ref::map(inner, |v| &v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the signal's value.
|
||||||
|
/// If the signal has been dropped, this will panic.
|
||||||
|
pub fn write(&self) -> Write<'_, T> {
|
||||||
|
let inner = self.inner.write();
|
||||||
|
let borrow = RefMut::map(inner, |v| &mut v.value);
|
||||||
|
Write {
|
||||||
|
write: borrow,
|
||||||
|
signal: SignalSubscriberDrop { signal: *self },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_subscribers(&self) {
|
||||||
|
{
|
||||||
|
let inner = self.inner.read();
|
||||||
|
for &scope_id in &*inner.subscribers.borrow() {
|
||||||
|
log::trace!(
|
||||||
|
"Write on {:?} triggered update on {:?}",
|
||||||
|
self.inner.value,
|
||||||
|
scope_id
|
||||||
|
);
|
||||||
|
(inner.update_any)(scope_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscribers = {
|
||||||
|
let self_read = self.inner.read();
|
||||||
|
let mut effects = self_read.effect_subscribers.borrow_mut();
|
||||||
|
std::mem::take(&mut *effects)
|
||||||
|
};
|
||||||
|
for effect in subscribers {
|
||||||
|
log::trace!(
|
||||||
|
"Write on {:?} triggered effect {:?}",
|
||||||
|
self.inner.value,
|
||||||
|
effect
|
||||||
|
);
|
||||||
|
effect.try_run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value of the signal. This will trigger an update on all subscribers.
|
||||||
|
pub fn set(&self, value: T) {
|
||||||
|
*self.write() = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a closure with a reference to the signal's value.
|
||||||
|
/// If the signal has been dropped, this will panic.
|
||||||
|
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||||
|
let write = self.read();
|
||||||
|
f(&*write)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a closure with a mutable reference to the signal's value.
|
||||||
|
/// If the signal has been dropped, this will panic.
|
||||||
|
pub fn with_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
|
||||||
|
let mut write = self.write();
|
||||||
|
f(&mut *write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static> Signal<T> {
|
||||||
|
/// Get the current value of the signal. This will subscribe the current scope to the signal.
|
||||||
|
/// If the signal has been dropped, this will panic.
|
||||||
|
pub fn value(&self) -> T {
|
||||||
|
self.read().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> PartialEq for Signal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SignalSubscriberDrop<T: 'static> {
|
||||||
|
signal: Signal<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Drop for SignalSubscriberDrop<T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.signal.update_subscribers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mutable reference to a signal's value.
|
||||||
|
pub struct Write<'a, T: 'static, I: 'static = T> {
|
||||||
|
write: RefMut<'a, T>,
|
||||||
|
signal: SignalSubscriberDrop<I>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'static, I: 'static> Write<'a, T, I> {
|
||||||
|
/// Map the mutable reference to the signal's value to a new type.
|
||||||
|
pub fn map<O>(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, I> {
|
||||||
|
let Self { write, signal } = myself;
|
||||||
|
Write {
|
||||||
|
write: RefMut::map(write, f),
|
||||||
|
signal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to map the mutable reference to the signal's value to a new type
|
||||||
|
pub fn filter_map<O>(
|
||||||
|
myself: Self,
|
||||||
|
f: impl FnOnce(&mut T) -> Option<&mut O>,
|
||||||
|
) -> Option<Write<'a, O, I>> {
|
||||||
|
let Self { write, signal } = myself;
|
||||||
|
let write = RefMut::filter_map(write, f).ok();
|
||||||
|
write.map(|write| Write { write, signal })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'static> Deref for Write<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.write
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Write<'_, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.write
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A signal that can only be read from.
|
||||||
|
pub struct ReadOnlySignal<T: 'static> {
|
||||||
|
inner: Signal<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> ReadOnlySignal<T> {
|
||||||
|
/// Create a new read-only signal.
|
||||||
|
pub fn new(signal: Signal<T>) -> Self {
|
||||||
|
Self { inner: signal }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scope that the signal was created in.
|
||||||
|
pub fn origin_scope(&self) -> ScopeId {
|
||||||
|
self.inner.origin_scope()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of the signal. This will subscribe the current scope to the signal.
|
||||||
|
pub fn read(&self) -> Ref<T> {
|
||||||
|
self.inner.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a closure with a reference to the signal's value.
|
||||||
|
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||||
|
self.inner.with(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static> ReadOnlySignal<T> {
|
||||||
|
/// Get the current value of the signal. This will subscribe the current scope to the signal.
|
||||||
|
pub fn value(&self) -> T {
|
||||||
|
self.read().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> PartialEq for ReadOnlySignal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_signals_global() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
render! {
|
||||||
|
for _ in 0..10 {
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn Child(cx: Scope) -> Element {
|
||||||
|
let signal = create_without_cx();
|
||||||
|
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _edits = dom.rebuild().santize();
|
||||||
|
|
||||||
|
fn create_without_cx() -> Signal<String> {
|
||||||
|
Signal::new("hello world".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drop_signals() {
|
||||||
|
let mut dom = VirtualDom::new(|cx| {
|
||||||
|
let generation = cx.generation();
|
||||||
|
|
||||||
|
let count = if generation % 2 == 0 { 10 } else { 0 };
|
||||||
|
render! {
|
||||||
|
for _ in 0..count {
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn Child(cx: Scope) -> Element {
|
||||||
|
let signal = create_without_cx();
|
||||||
|
|
||||||
|
render! {
|
||||||
|
"{signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = dom.rebuild().santize();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
dom.render_immediate();
|
||||||
|
|
||||||
|
fn create_without_cx() -> Signal<String> {
|
||||||
|
Signal::new("hello world".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn effects_rerun() {
|
||||||
|
simple_logger::SimpleLogger::new().init().unwrap();
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RunCounter {
|
||||||
|
component: usize,
|
||||||
|
effect: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||||
|
let mut dom = VirtualDom::new_with_props(
|
||||||
|
|cx| {
|
||||||
|
let counter = cx.props;
|
||||||
|
counter.borrow_mut().component += 1;
|
||||||
|
|
||||||
|
let mut signal = use_signal(cx, || 0);
|
||||||
|
cx.use_hook(move || {
|
||||||
|
to_owned![counter];
|
||||||
|
Effect::new(move || {
|
||||||
|
counter.borrow_mut().effect += 1;
|
||||||
|
println!("Signal: {:?}", signal);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
signal += 1;
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
counter.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = dom.rebuild().santize();
|
||||||
|
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.component, 1);
|
||||||
|
assert_eq!(current_counter.effect, 2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::html::p;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memos_rerun() {
|
||||||
|
let _ = simple_logger::SimpleLogger::new().init();
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RunCounter {
|
||||||
|
component: usize,
|
||||||
|
effect: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||||
|
let mut dom = VirtualDom::new_with_props(
|
||||||
|
|cx| {
|
||||||
|
let counter = cx.props;
|
||||||
|
counter.borrow_mut().component += 1;
|
||||||
|
|
||||||
|
let mut signal = use_signal(cx, || 0);
|
||||||
|
let memo = cx.use_hook(move || {
|
||||||
|
to_owned![counter];
|
||||||
|
selector(move || {
|
||||||
|
counter.borrow_mut().effect += 1;
|
||||||
|
println!("Signal: {:?}", signal);
|
||||||
|
signal.value()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert_eq!(memo.value(), 0);
|
||||||
|
signal += 1;
|
||||||
|
assert_eq!(memo.value(), 1);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
counter.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = dom.rebuild().santize();
|
||||||
|
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.component, 1);
|
||||||
|
assert_eq!(current_counter.effect, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memos_prevents_component_rerun() {
|
||||||
|
let _ = simple_logger::SimpleLogger::new().init();
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RunCounter {
|
||||||
|
component: usize,
|
||||||
|
effect: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||||
|
let mut dom = VirtualDom::new_with_props(
|
||||||
|
|cx| {
|
||||||
|
let mut signal = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
if cx.generation() == 1 {
|
||||||
|
*signal.write() = 0;
|
||||||
|
}
|
||||||
|
if cx.generation() == 2 {
|
||||||
|
println!("Writing to signal");
|
||||||
|
*signal.write() = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
render! {
|
||||||
|
Child {
|
||||||
|
signal: signal,
|
||||||
|
counter: cx.props.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
counter.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Default, Props)]
|
||||||
|
struct ChildProps {
|
||||||
|
signal: Signal<usize>,
|
||||||
|
counter: Rc<RefCell<RunCounter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ChildProps {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.signal == other.signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
let counter = &cx.props.counter;
|
||||||
|
let signal = cx.props.signal;
|
||||||
|
counter.borrow_mut().component += 1;
|
||||||
|
|
||||||
|
let memo = cx.use_hook(move || {
|
||||||
|
to_owned![counter];
|
||||||
|
selector(move || {
|
||||||
|
counter.borrow_mut().effect += 1;
|
||||||
|
println!("Signal: {:?}", signal);
|
||||||
|
signal.value()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
match cx.generation() {
|
||||||
|
0 => {
|
||||||
|
assert_eq!(memo.value(), 0);
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
assert_eq!(memo.value(), 1);
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected generation"),
|
||||||
|
}
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = dom.rebuild().santize();
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
dom.render_immediate();
|
||||||
|
|
||||||
|
{
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.component, 1);
|
||||||
|
assert_eq!(current_counter.effect, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
dom.render_immediate();
|
||||||
|
dom.render_immediate();
|
||||||
|
|
||||||
|
{
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.component, 2);
|
||||||
|
assert_eq!(current_counter.effect, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_signals::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_subscribes() {
|
||||||
|
simple_logger::SimpleLogger::new().init().unwrap();
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RunCounter {
|
||||||
|
parent: usize,
|
||||||
|
children: HashMap<ScopeId, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||||
|
let mut dom = VirtualDom::new_with_props(
|
||||||
|
|cx| {
|
||||||
|
let mut signal = use_signal(cx, || 0);
|
||||||
|
|
||||||
|
println!("Parent: {:?}", cx.scope_id());
|
||||||
|
if cx.generation() == 1 {
|
||||||
|
signal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.props.borrow_mut().parent += 1;
|
||||||
|
|
||||||
|
render! {
|
||||||
|
for id in 0..10 {
|
||||||
|
Child {
|
||||||
|
signal: signal,
|
||||||
|
counter: cx.props.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
counter.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Props, Clone)]
|
||||||
|
struct ChildProps {
|
||||||
|
signal: Signal<usize>,
|
||||||
|
counter: Rc<RefCell<RunCounter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ChildProps {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.signal == other.signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Child(cx: Scope<ChildProps>) -> Element {
|
||||||
|
println!("Child: {:?}", cx.scope_id());
|
||||||
|
*cx.props
|
||||||
|
.counter
|
||||||
|
.borrow_mut()
|
||||||
|
.children
|
||||||
|
.entry(cx.scope_id())
|
||||||
|
.or_default() += 1;
|
||||||
|
|
||||||
|
render! {
|
||||||
|
"{cx.props.signal}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = dom.rebuild().santize();
|
||||||
|
|
||||||
|
{
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.parent, 1);
|
||||||
|
|
||||||
|
for (scope_id, rerun_count) in current_counter.children.iter() {
|
||||||
|
assert_eq!(rerun_count, &1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.mark_dirty(ScopeId(0));
|
||||||
|
dom.render_immediate();
|
||||||
|
dom.render_immediate();
|
||||||
|
|
||||||
|
{
|
||||||
|
let current_counter = counter.borrow();
|
||||||
|
assert_eq!(current_counter.parent, 2);
|
||||||
|
|
||||||
|
for (scope_id, rerun_count) in current_counter.children.iter() {
|
||||||
|
assert_eq!(rerun_count, &2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user