>>
- },
- |f| f,
- |_| {},
- );
- // let g = unsafe { Pin::new_unchecked(fut) };
-
- // cx.submit_task(fut);
-
- todo!()
-};
-
-const Task: FC<()> = |cx| {
- let (_task, _res) = use_task(cx, || async { true });
- // task.pause();
- // task.restart();
- // task.stop();
- // task.drop();
-
- //
-
- let _s = use_task(cx, || async { "hello world".to_string() });
-
- todo!()
-};
-
-fn use_mut(_cx: Context
, _f: impl FnOnce() -> T) -> &mut T {
- todo!()
+fn main() {
+ let mut dom = VirtualDom::new(App);
+ dom.rebuild();
}
+
+const App: FC<()> = |cx, props| {
+ let id = cx.scope_id();
+ cx.submit_task(Box::pin(async move { id }));
+
+ let (handle, contents) = use_task(cx, || async { "hello world".to_string() });
+
+ todo!()
+};
diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs
index e30447548..a11f55225 100644
--- a/packages/core/examples/borrowed.rs
+++ b/packages/core/examples/borrowed.rs
@@ -20,7 +20,7 @@ struct ListItem {
age: u32,
}
-fn app(cx: Context) -> DomTree {
+fn app<'a>(cx: Context<'a>, props: &AppProps) -> DomTree<'a> {
// let (val, set_val) = use_state_classic(cx, || 0);
cx.render(LazyNodes::new(move |_nodecx| {
@@ -56,7 +56,7 @@ struct ChildProps {
item_handler: Rc,
}
-fn ChildItem<'a>(cx: Context<'a, ChildProps>) -> DomTree {
+fn ChildItem<'a>(cx: Context<'a>, props: &'a ChildProps) -> DomTree<'a> {
cx.render(LazyNodes::new(move |__cx| todo!()))
}
diff --git a/packages/core/examples/contextapi.rs b/packages/core/examples/contextapi.rs
index 61dc79837..6c90a60c7 100644
--- a/packages/core/examples/contextapi.rs
+++ b/packages/core/examples/contextapi.rs
@@ -6,7 +6,7 @@ struct SomeContext {
}
#[allow(unused)]
-static Example: FC<()> = |cpt| {
+static Example: FC<()> = |cx, props| {
todo!()
// let value = cx.use_context(|c: &SomeContext| c.items.last().unwrap());
diff --git a/packages/core/examples/fragment_from_iter.rs b/packages/core/examples/fragment_from_iter.rs
index 58c4df70f..ae7d761e8 100644
--- a/packages/core/examples/fragment_from_iter.rs
+++ b/packages/core/examples/fragment_from_iter.rs
@@ -2,7 +2,7 @@ use dioxus_core::prelude::*;
fn main() {}
-fn app(cx: Context<()>) -> DomTree {
+fn app<'a>(cx: Context<'a>, props: &()) -> DomTree<'a> {
let vak = use_suspense(
cx,
|| async {},
diff --git a/packages/core/examples/jsframework.rs b/packages/core/examples/jsframework.rs
index 79400afd1..902869f43 100644
--- a/packages/core/examples/jsframework.rs
+++ b/packages/core/examples/jsframework.rs
@@ -10,7 +10,7 @@ fn main() {
assert!(g.edits.len() > 1);
}
-static App: FC<()> = |cx| {
+static App: FC<()> = |cx, props| {
let mut rng = SmallRng::from_entropy();
let rows = (0..10_000).map(|f| {
let label = Label::new(&mut rng);
@@ -35,12 +35,12 @@ struct RowProps {
row_id: usize,
label: Label,
}
-fn Row<'a>(cx: Context<'a, RowProps>) -> DomTree {
+fn Row<'a>(cx: Context<'a>, props: &'a RowProps) -> DomTree<'a> {
cx.render(rsx! {
tr {
- td { class:"col-md-1", "{cx.row_id}" }
+ td { class:"col-md-1", "{props.row_id}" }
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
- a { class: "lbl", "{cx.label}" }
+ a { class: "lbl", "{props.label}" }
}
td { class: "col-md-1"
a { class: "remove", onclick: move |_| {/* remove */}
diff --git a/packages/core/examples/vdom_usage.rs b/packages/core/examples/vdom_usage.rs
index c5d643df5..7175a167d 100644
--- a/packages/core/examples/vdom_usage.rs
+++ b/packages/core/examples/vdom_usage.rs
@@ -4,12 +4,13 @@ use dioxus_core::prelude::*;
#[async_std::main]
async fn main() {
- static App: FC<()> = |cx| cx.render(LazyNodes::new(|f| f.text(format_args!("hello"))));
+ static App: FC<()> = |cx, props| cx.render(LazyNodes::new(|f| f.text(format_args!("hello"))));
let mut dom = VirtualDom::new(App);
dom.rebuild();
let deadline = async_std::task::sleep(Duration::from_millis(50));
- let _fut = dom.run_with_deadline(deadline);
+
+ // let _fut = dom.run_with_deadline(|| deadline.);
}
diff --git a/packages/core/src/bumpframe.rs b/packages/core/src/bumpframe.rs
index 8cb6d6734..a696d82b5 100644
--- a/packages/core/src/bumpframe.rs
+++ b/packages/core/src/bumpframe.rs
@@ -14,6 +14,7 @@ pub(crate) struct BumpFrame {
pub bump: Bump,
pub(crate) head_node: VNode<'static>,
+ #[cfg(test)]
// used internally for debugging
_name: &'static str,
}
@@ -23,11 +24,15 @@ impl ActiveFrame {
let frame_a = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
+
+ #[cfg(test)]
_name: "wip",
};
let frame_b = BumpFrame {
bump: Bump::new(),
head_node: NodeFactory::unstable_place_holder(),
+
+ #[cfg(test)]
_name: "fin",
};
Self {
diff --git a/packages/core/src/childiter.rs b/packages/core/src/childiter.rs
index daf5281c4..e7032e939 100644
--- a/packages/core/src/childiter.rs
+++ b/packages/core/src/childiter.rs
@@ -39,7 +39,7 @@ impl<'a> Iterator for RealChildIterator<'a> {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
should_pop = true;
- returned_node = Some(&*node);
+ returned_node = Some(node);
}
// If we get a fragment we push the next child
@@ -64,7 +64,6 @@ impl<'a> Iterator for RealChildIterator<'a> {
.scopes
.get_scope(sc.associated_scope.get().unwrap())
.unwrap();
- // let scope = self.scopes.get(sc.ass_scope.get().unwrap()).unwrap();
// Simply swap the current node on the stack with the root of the component
*node = scope.frames.fin_head();
diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs
index ee25bd5bd..0ec306229 100644
--- a/packages/core/src/component.rs
+++ b/packages/core/src/component.rs
@@ -7,6 +7,32 @@
use crate::innerlude::{Context, DomTree, LazyNodes, FC};
+/// Create inline fragments using Component syntax.
+///
+/// Fragments capture a series of children without rendering extra nodes.
+///
+/// # Example
+///
+/// ```rust
+/// rsx!{
+/// Fragment { key: "abc" }
+/// }
+/// ```
+///
+/// # Details
+///
+/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
+/// Try to avoid nesting fragments if you can. There is no protection against infinitely nested fragments.
+///
+/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
+///
+/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
+///
+#[allow(non_upper_case_globals, non_snake_case)]
+pub fn Fragment<'a>(cx: Context<'a>, _: &'a ()) -> DomTree<'a> {
+ cx.render(LazyNodes::new(|f| f.fragment_from_iter(cx.children())))
+}
+
/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus
/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the
/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe.
@@ -44,7 +70,7 @@ pub trait Properties: Sized {
const IS_STATIC: bool;
fn builder() -> Self::Builder;
- /// Memoization can only happen if the props are 'static
+ /// Memoization can only happen if the props are valid for the 'static lifetime
/// The user must know if their props are static, but if they make a mistake, UB happens
/// Therefore it's unsafe to memeoize.
unsafe fn memoize(&self, other: &Self) -> bool;
@@ -65,9 +91,7 @@ impl Properties for () {
pub struct EmptyBuilder;
impl EmptyBuilder {
#[inline]
- pub fn build(self) -> () {
- ()
- }
+ pub fn build(self) {}
}
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
@@ -75,24 +99,3 @@ impl EmptyBuilder {
pub fn fc_to_builder(_: FC) -> T::Builder {
T::builder()
}
-
-/// Create inline fragments
-///
-/// Fragments capture a series of children without rendering extra nodes.
-///
-/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
-/// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash.
-///
-/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro.
-///
-/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
-///
-/// ```rust
-/// rsx!{
-/// Fragment { key: "abc" }
-/// }
-/// ```
-#[allow(non_upper_case_globals, non_snake_case)]
-pub fn Fragment<'a>(cx: Context<'a, ()>) -> DomTree<'a> {
- cx.render(LazyNodes::new(move |f| f.fragment_from_iter(cx.children())))
-}
diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs
index 21d89436e..9d16c9c01 100644
--- a/packages/core/src/context.rs
+++ b/packages/core/src/context.rs
@@ -27,31 +27,27 @@ use std::{any::TypeId, ops::Deref, rc::Rc};
/// }
/// }
/// ```
-pub struct Context<'src, T> {
- pub props: &'src T,
+pub struct Context<'src> {
pub scope: &'src Scope,
}
-impl<'src, T> Copy for Context<'src, T> {}
-impl<'src, T> Clone for Context<'src, T> {
+impl<'src> Copy for Context<'src> {}
+impl<'src> Clone for Context<'src> {
fn clone(&self) -> Self {
- Self {
- props: self.props,
- scope: self.scope,
- }
+ Self { scope: self.scope }
}
}
// We currently deref to props, but it might make more sense to deref to Scope?
// This allows for code that takes cx.xyz instead of cx.props.xyz
-impl<'a, T> Deref for Context<'a, T> {
- type Target = &'a T;
+impl<'a> Deref for Context<'a> {
+ type Target = &'a Scope;
fn deref(&self) -> &Self::Target {
- &self.props
+ &self.scope
}
}
-impl<'src, P> Context<'src, P> {
+impl<'src> Context<'src> {
/// Access the children elements passed into the component
///
/// This enables patterns where a component is passed children from its parent.
@@ -69,7 +65,7 @@ impl<'src, P> Context<'src, P> {
/// ## Example
///
/// ```rust
- /// const App: FC<()> = |cx| {
+ /// const App: FC<()> = |cx, props|{
/// cx.render(rsx!{
/// CustomCard {
/// h1 {}
@@ -78,7 +74,7 @@ impl<'src, P> Context<'src, P> {
/// })
/// }
///
- /// const CustomCard: FC<()> = |cx| {
+ /// const CustomCard: FC<()> = |cx, props|{
/// cx.render(rsx!{
/// div {
/// h1 {"Title card"}
@@ -98,15 +94,26 @@ impl<'src, P> Context<'src, P> {
/// Create a subscription that schedules a future render for the reference component
///
/// ## Notice: you should prefer using prepare_update and get_scope_id
- ///
pub fn schedule_update(&self) -> Rc {
self.scope.memoized_updater.clone()
}
- pub fn prepare_update(&self) -> Rc {
+ /// Schedule an update for any component given its ScopeId.
+ ///
+ /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method.
+ ///
+ /// This method should be used when you want to schedule an update for a component
+ pub fn schedule_update_any(&self) -> Rc {
self.scope.shared.schedule_any_immediate.clone()
}
+ /// Get the [`ScopeId`] of a mounted component.
+ ///
+ /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted.
+ pub fn scope_id(&self) -> ScopeId {
+ self.scope.our_arena_idx
+ }
+
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
///
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
@@ -151,22 +158,81 @@ impl<'src, P> Context<'src, P> {
(self.scope.shared.submit_task)(task)
}
- /// Add a state globally accessible to child components via tree walking
- pub fn add_shared_state(self, val: T) {
- self.scope
- .shared_contexts
- .borrow_mut()
- .insert(TypeId::of::(), Rc::new(val))
- .map(|_| {
- log::warn!("A shared state was replaced with itself. This is does not result in a panic, but is probably not what you are trying to do");
- });
+ /// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
+ ///
+ /// This is a hook, so it should not be called conditionally!
+ ///
+ /// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
+ /// so don't put it in a conditional.
+ ///
+ /// When the component is dropped, so is the context. Be aware of this behavior when consuming
+ /// the context via Rc/Weak.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// struct SharedState(&'static str);
+ ///
+ /// static App: FC<()> = |cx, props|{
+ /// cx.use_provide_state(|| SharedState("world"));
+ /// rsx!(cx, Child {})
+ /// }
+ ///
+ /// static Child: FC<()> = |cx, props|{
+ /// let state = cx.use_consume_state::();
+ /// rsx!(cx, div { "hello {state.0}" })
+ /// }
+ /// ```
+ pub fn use_provide_state(self, init: F) -> &'src Rc
+ where
+ T: 'static,
+ F: FnOnce() -> T,
+ {
+ let is_initialized = self.use_hook(
+ |_| false,
+ |s| {
+ let i = *s;
+ *s = true;
+ i
+ },
+ |_| {},
+ );
+
+ if !is_initialized {
+ let existing = self
+ .scope
+ .shared_contexts
+ .borrow_mut()
+ .insert(TypeId::of::(), Rc::new(init()));
+
+ if existing.is_some() {
+ log::warn!(
+ "A shared state was replaced with itself. \
+ This is does not result in a panic, but is probably not what you are trying to do"
+ );
+ }
+ }
+
+ self.use_consume_state().unwrap()
}
- pub fn consume_shared_state(self) -> Option> {
- let getter = &self.scope.shared.get_shared_context;
- let ty = TypeId::of::();
- let idx = self.scope.our_arena_idx;
- getter(idx, ty).map(|f| f.downcast().expect("TypeID already validated"))
+ /// Uses a context, storing the cached value around
+ ///
+ /// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
+ /// context was added later. This allows using another hook as a fallback
+ ///
+ pub fn use_consume_state(self) -> Option<&'src Rc> {
+ struct UseContextHook(Option>);
+ self.use_hook(
+ move |_| {
+ let getter = &self.scope.shared.get_shared_context;
+ let ty = TypeId::of::();
+ let idx = self.scope.our_arena_idx;
+ UseContextHook(getter(idx, ty).map(|f| f.downcast().unwrap()))
+ },
+ move |hook| hook.0.as_ref(),
+ |_| {},
+ )
}
/// Store a value between renders
@@ -177,12 +243,15 @@ impl<'src, P> Context<'src, P> {
/// - Runner: closure used to output a value every time the hook is used
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
///
+ ///
+ /// # Example
+ ///
/// ```ignore
/// // use_ref is the simplest way of storing a value between renders
- /// pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> {
+ /// fn use_ref(initial_value: impl FnOnce() -> T) -> &RefCell {
/// use_hook(
/// || Rc::new(RefCell::new(initial_value())),
- /// |state| state.clone(),
+ /// |state| state,
/// |_| {},
/// )
/// }
@@ -191,29 +260,31 @@ impl<'src, P> Context<'src, P> {
self,
initializer: Init,
runner: Run,
- _cleanup: Cleanup,
+ cleanup: Cleanup,
) -> Output
where
State: 'static,
Output: 'src,
Init: FnOnce(usize) -> State,
Run: FnOnce(&'src mut State) -> Output,
- Cleanup: FnOnce(State),
+ Cleanup: FnOnce(&mut State) + 'static,
{
// If the idx is the same as the hook length, then we need to add the current hook
if self.scope.hooks.at_end() {
- let new_state = initializer(self.scope.hooks.len());
- self.scope.hooks.push(new_state);
+ self.scope.hooks.push_hook(
+ initializer(self.scope.hooks.len()),
+ Box::new(|raw| cleanup(raw.downcast_mut::().unwrap())),
+ );
}
- const ERR_MSG: &str = r###"
+ runner(self.scope.hooks.next::().expect(HOOK_ERR_MSG))
+ }
+}
+
+const HOOK_ERR_MSG: &str = r###"
Unable to retrive the hook that was initialized in this index.
Consult the `rules of hooks` to understand how to use hooks properly.
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Any function prefixed with "use" should not be called conditionally.
"###;
-
- runner(self.scope.hooks.next::().expect(ERR_MSG))
- }
-}
diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs
index bafdfea35..e29b5e18e 100644
--- a/packages/core/src/diff.rs
+++ b/packages/core/src/diff.rs
@@ -109,6 +109,18 @@ pub(crate) struct DiffMachine<'bump> {
pub mutations: Mutations<'bump>,
pub stack: DiffStack<'bump>,
pub seen_scopes: FxHashSet,
+ pub cfg: DiffCfg,
+}
+
+pub(crate) struct DiffCfg {
+ pub force_diff: bool,
+}
+impl Default for DiffCfg {
+ fn default() -> Self {
+ Self {
+ force_diff: Default::default(),
+ }
+ }
}
/// a "saved" form of a diff machine
@@ -124,10 +136,12 @@ impl<'a> SavedDiffWork<'a> {
pub unsafe fn extend(self: SavedDiffWork<'a>) -> SavedDiffWork<'static> {
std::mem::transmute(self)
}
- pub unsafe fn promote<'b>(self, vdom: &'b mut ResourcePool) -> DiffMachine<'b> {
+
+ pub unsafe fn promote<'b>(self, vdom: &'b ResourcePool) -> DiffMachine<'b> {
let extended: SavedDiffWork<'b> = std::mem::transmute(self);
DiffMachine {
vdom,
+ cfg: DiffCfg::default(),
mutations: extended.mutations,
stack: extended.stack,
seen_scopes: extended.seen_scopes,
@@ -136,10 +150,11 @@ impl<'a> SavedDiffWork<'a> {
}
impl<'bump> DiffMachine<'bump> {
- pub(crate) fn new(edits: Mutations<'bump>, shared: &'bump ResourcePool) -> Self {
+ pub(crate) fn new(mutations: Mutations<'bump>, shared: &'bump ResourcePool) -> Self {
Self {
+ mutations,
+ cfg: DiffCfg::default(),
stack: DiffStack::new(),
- mutations: edits,
vdom: shared,
seen_scopes: FxHashSet::default(),
}
@@ -156,7 +171,8 @@ impl<'bump> DiffMachine<'bump> {
pub fn diff_scope(&mut self, id: ScopeId) {
if let Some(component) = self.vdom.get_scope_mut(id) {
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
- self.stack.push(DiffInstruction::DiffNode { new, old });
+ self.stack.push(DiffInstruction::Diff { new, old });
+ self.work(|| false);
}
}
@@ -167,27 +183,18 @@ impl<'bump> DiffMachine<'bump> {
/// We do depth-first to maintain high cache locality (nodes were originally generated recursively).
///
/// Returns a `bool` indicating that the work completed properly.
- pub fn work(&mut self, deadline_expired: &mut impl FnMut() -> bool) -> bool {
+ pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool {
while let Some(instruction) = self.stack.pop() {
- // defer to individual functions so the compiler produces better code
- // large functions tend to be difficult for the compiler to work with
match instruction {
- DiffInstruction::PopScope => {
- self.stack.pop_scope();
- }
-
- DiffInstruction::DiffNode { old, new, .. } => self.diff_node(old, new),
-
- DiffInstruction::DiffChildren { old, new } => self.diff_children(old, new),
-
+ DiffInstruction::Diff { old, new } => self.diff_node(old, new),
DiffInstruction::Create { node } => self.create_node(node),
-
DiffInstruction::Mount { and } => self.mount(and),
-
- DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node),
+ DiffInstruction::PrepareMove { node } => self.prepare_move_node(node),
+ DiffInstruction::PopScope => self.stack.pop_off_scope(),
};
if deadline_expired() {
+ log::debug!("Deadling expired before we could finished!");
return false;
}
}
@@ -210,24 +217,31 @@ impl<'bump> DiffMachine<'bump> {
MountType::Absorb => {
self.stack.add_child_count(nodes_created);
}
+
+ MountType::Replace { old } => {
+ if let Some(old_id) = old.try_mounted_id() {
+ self.mutations.replace_with(old_id, nodes_created as u32);
+ } else {
+ let mut iter = RealChildIterator::new(old, self.vdom);
+ let first = iter.next().unwrap();
+ self.mutations
+ .replace_with(first.mounted_id(), nodes_created as u32);
+ self.remove_nodes(iter);
+ }
+ }
+
+ MountType::ReplaceByElementId { el } => {
+ if let Some(old) = el {
+ self.mutations.replace_with(old, nodes_created as u32);
+ }
+ }
+
MountType::Append => {
self.mutations.edits.push(AppendChildren {
many: nodes_created as u32,
});
}
- MountType::Replace { old } => {
- let mut iter = RealChildIterator::new(old, self.vdom);
- let first = iter.next().unwrap();
- self.mutations
- .replace_with(first.mounted_id(), nodes_created as u32);
- self.remove_nodes(iter);
- }
-
- MountType::ReplaceByElementId { el: old } => {
- self.mutations.replace_with(old, nodes_created as u32);
- }
-
MountType::InsertAfter { other_node } => {
let root = self.find_last_element(other_node).unwrap();
self.mutations.insert_after(root, nodes_created as u32);
@@ -246,37 +260,40 @@ impl<'bump> DiffMachine<'bump> {
fn create_node(&mut self, node: &'bump VNode<'bump>) {
match node {
- VNode::Text(vtext) => self.create_text_node(vtext),
- VNode::Suspended(suspended) => self.create_suspended_node(suspended),
- VNode::Anchor(anchor) => self.create_anchor_node(anchor),
- VNode::Element(element) => self.create_element_node(element),
+ VNode::Text(vtext) => self.create_text_node(vtext, node),
+ VNode::Suspended(suspended) => self.create_suspended_node(suspended, node),
+ VNode::Anchor(anchor) => self.create_anchor_node(anchor, node),
+ VNode::Element(element) => self.create_element_node(element, node),
VNode::Fragment(frag) => self.create_fragment_node(frag),
VNode::Component(component) => self.create_component_node(component),
}
}
- fn create_text_node(&mut self, vtext: &'bump VText<'bump>) {
- let real_id = self.vdom.reserve_node();
+ fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) {
+ let real_id = self.vdom.reserve_node(node);
self.mutations.create_text_node(vtext.text, real_id);
vtext.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
}
- fn create_suspended_node(&mut self, suspended: &'bump VSuspended) {
- let real_id = self.vdom.reserve_node();
+ fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) {
+ let real_id = self.vdom.reserve_node(node);
self.mutations.create_placeholder(real_id);
+
suspended.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
+
+ self.attach_suspended_node_to_scope(suspended);
}
- fn create_anchor_node(&mut self, anchor: &'bump VAnchor) {
- let real_id = self.vdom.reserve_node();
+ fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) {
+ let real_id = self.vdom.reserve_node(node);
self.mutations.create_placeholder(real_id);
anchor.dom_id.set(Some(real_id));
self.stack.add_child_count(1);
}
- fn create_element_node(&mut self, element: &'bump VElement<'bump>) {
+ fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) {
let VElement {
tag_name,
listeners,
@@ -287,7 +304,8 @@ impl<'bump> DiffMachine<'bump> {
..
} = element;
- let real_id = self.vdom.reserve_node();
+ let real_id = self.vdom.reserve_node(node);
+
dom_id.set(Some(real_id));
self.mutations.create_element(tag_name, *namespace, real_id);
@@ -311,7 +329,10 @@ impl<'bump> DiffMachine<'bump> {
self.mutations.set_attribute(attr);
}
- if children.len() > 0 {
+ if !children.is_empty() {
+ // self.stack.element_id_stack.push(real_id);
+ // push our element_id onto the stack
+ // drop our element off the stack
self.stack.create_children(children, MountType::Append);
}
}
@@ -321,13 +342,15 @@ impl<'bump> DiffMachine<'bump> {
}
fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) {
- let caller = vcomponent.caller.clone();
+ let caller = vcomponent.caller;
let parent_idx = self.stack.current_scope().unwrap();
let shared = self.vdom.channel.clone();
+
// Insert a new scope into our component list
let parent_scope = self.vdom.get_scope(parent_idx).unwrap();
+
let new_idx = self.vdom.insert_scope_with_key(|new_idx| {
let height = parent_scope.height + 1;
Scope::new(
@@ -376,11 +399,13 @@ impl<'bump> DiffMachine<'bump> {
match (old_node, new_node) {
// Check the most common cases first
(Text(old), Text(new)) => self.diff_text_nodes(old, new),
- (Component(old), Component(new)) => self.diff_component_nodes(old, new),
+ (Component(old), Component(new)) => {
+ self.diff_component_nodes(old_node, new_node, old, new)
+ }
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
(Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()),
- (Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()),
- (Element(old), Element(new)) => self.diff_element_nodes(old, new),
+ (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new),
+ (Element(old), Element(new)) => self.diff_element_nodes(old, new, new_node),
// Anything else is just a basic replace and create
(
@@ -393,19 +418,24 @@ impl<'bump> DiffMachine<'bump> {
}
fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) {
- let root = old.dom_id.get().unwrap();
+ if let Some(root) = old.dom_id.get() {
+ if old.text != new.text {
+ self.mutations.push_root(root);
+ self.mutations.set_text(new.text);
+ self.mutations.pop();
+ }
- if old.text != new.text {
- self.mutations.push_root(root);
- self.mutations.set_text(new.text);
- self.mutations.pop();
+ new.dom_id.set(Some(root));
}
-
- new.dom_id.set(Some(root));
}
- fn diff_element_nodes(&mut self, old: &'bump VElement<'bump>, new: &'bump VElement<'bump>) {
- let root = old.dom_id.get().unwrap();
+ fn diff_element_nodes(
+ &mut self,
+ old: &'bump VElement<'bump>,
+ new: &'bump VElement<'bump>,
+ new_node: &'bump VNode<'bump>,
+ ) {
+ let root = old.dom_id.get();
// If the element type is completely different, the element needs to be re-rendered completely
// This is an optimization React makes due to how users structure their code
@@ -417,21 +447,24 @@ impl<'bump> DiffMachine<'bump> {
self.stack.push_nodes_created(0);
self.stack.push(DiffInstruction::Mount {
and: MountType::ReplaceByElementId {
- el: old.dom_id.get().unwrap(),
+ el: old.dom_id.get(),
},
});
- self.create_element_node(new);
+ self.create_element_node(new, new_node);
return;
}
- new.dom_id.set(Some(root));
+ new.dom_id.set(root);
// Don't push the root if we don't have to
let mut has_comitted = false;
let mut please_commit = |edits: &mut Vec| {
if !has_comitted {
has_comitted = true;
- edits.push(PushRoot { id: root.as_u64() });
+ log::info!("planning on committing... {:#?}, {:#?}", old, new);
+ edits.push(PushRoot {
+ id: root.unwrap().as_u64(),
+ });
}
};
@@ -443,13 +476,17 @@ impl<'bump> DiffMachine<'bump> {
// TODO: take a more efficient path than this
if old.attributes.len() == new.attributes.len() {
for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) {
+ log::info!("checking attribute");
if old_attr.value != new_attr.value {
please_commit(&mut self.mutations.edits);
self.mutations.set_attribute(new_attr);
+ } else if new_attr.is_volatile {
+ log::debug!("setting due to volatile atttributes");
+ please_commit(&mut self.mutations.edits);
+ self.mutations.set_attribute(new_attr);
}
}
} else {
- // TODO: provide some sort of report on how "good" the diffing was
please_commit(&mut self.mutations.edits);
for attribute in old.attributes {
self.mutations.remove_attribute(attribute);
@@ -467,7 +504,6 @@ impl<'bump> DiffMachine<'bump> {
// We also need to make sure that all listeners are properly attached to the parent scope (fix_listener)
//
// TODO: take a more efficient path than this
-
if let Some(cur_scope_id) = self.stack.current_scope() {
let scope = self.vdom.get_scope(cur_scope_id).unwrap();
@@ -487,7 +523,7 @@ impl<'bump> DiffMachine<'bump> {
self.mutations.remove_event_listener(listener.event);
}
for listener in new.listeners {
- listener.mounted_node.set(Some(root));
+ listener.mounted_node.set(root);
self.mutations.new_event_listener(listener, cur_scope_id);
self.attach_listener_to_scope(listener, scope);
}
@@ -503,6 +539,9 @@ impl<'bump> DiffMachine<'bump> {
fn diff_component_nodes(
&mut self,
+ old_node: &'bump VNode<'bump>,
+ new_node: &'bump VNode<'bump>,
+
old: &'bump VComponent<'bump>,
new: &'bump VComponent<'bump>,
) {
@@ -519,20 +558,16 @@ impl<'bump> DiffMachine<'bump> {
// make sure the component's caller function is up to date
let scope = self.vdom.get_scope_mut(scope_addr).unwrap();
- scope.update_scope_dependencies(new.caller.clone(), ScopeChildren(new.children));
+ scope.update_scope_dependencies(new.caller, ScopeChildren(new.children));
// React doesn't automatically memoize, but we do.
- let compare = old.comparator.unwrap();
+ let props_are_the_same = old.comparator.unwrap();
- match compare(new) {
- true => {
- // the props are the same...
- }
- false => {
- // the props are different...
- if scope.run_scope(self.vdom) {
- self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
- }
+ if self.cfg.force_diff || !props_are_the_same(new) {
+ let succeeded = scope.run_scope(self.vdom);
+
+ if succeeded {
+ self.diff_node(scope.frames.wip_head(), scope.frames.fin_head());
}
}
@@ -540,25 +575,8 @@ impl<'bump> DiffMachine<'bump> {
self.seen_scopes.insert(scope_addr);
} else {
- todo!();
-
- // let mut old_iter = RealChildIterator::new(old_node, &self.vdom);
- // let first = old_iter
- // .next()
- // .expect("Components should generate a placeholder root");
-
- // // remove any leftovers
- // for to_remove in old_iter {
- // self.mutations.push_root(to_remove.direct_id());
- // self.mutations.remove();
- // }
-
- // // seems like we could combine this into a single instruction....
- // self.replace_node_with_node(first.direct_id(), old_node, new_node);
-
- // // Wipe the old one and plant the new one
- // let old_scope = old.ass_scope.get().unwrap();
- // self.destroy_scopes(old_scope);
+ self.stack
+ .create_node(new_node, MountType::Replace { old: old_node });
}
}
@@ -573,6 +591,11 @@ impl<'bump> DiffMachine<'bump> {
self.diff_children(old.children, new.children);
}
+ fn diff_suspended_nodes(&mut self, old: &'bump VSuspended, new: &'bump VSuspended) {
+ new.dom_id.set(old.dom_id.get());
+ self.attach_suspended_node_to_scope(new);
+ }
+
// =============================================
// Utilites for creating new diff instructions
// =============================================
@@ -608,7 +631,7 @@ impl<'bump> DiffMachine<'bump> {
old_anchor.dom_id.set(new_anchor.dom_id.get());
}
([VNode::Anchor(anchor)], _) => {
- let el = anchor.dom_id.get().unwrap();
+ let el = anchor.dom_id.get();
self.stack
.create_children(new, MountType::ReplaceByElementId { el });
}
@@ -647,24 +670,27 @@ impl<'bump> DiffMachine<'bump> {
// the change list stack is in the same state when this function returns.
fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) {
// Handled these cases in `diff_children` before calling this function.
- log::debug!("diffing non-keyed case");
debug_assert!(!new.is_empty());
debug_assert!(!old.is_empty());
for (new, old) in new.iter().zip(old.iter()).rev() {
- self.stack.push(DiffInstruction::DiffNode { new, old });
+ self.stack.push(DiffInstruction::Diff { new, old });
}
- if old.len() > new.len() {
- self.remove_nodes(&old[new.len()..]);
- } else if new.len() > old.len() {
- log::debug!("Calling create children on array differences");
- self.stack.create_children(
- &new[old.len()..],
- MountType::InsertAfter {
- other_node: old.last().unwrap(),
- },
- );
+ use std::cmp::Ordering;
+ match old.len().cmp(&new.len()) {
+ Ordering::Greater => self.remove_nodes(&old[new.len()..]),
+ Ordering::Less => {
+ self.stack.create_children(
+ &new[old.len()..],
+ MountType::InsertAfter {
+ other_node: old.last().unwrap(),
+ },
+ );
+ }
+ Ordering::Equal => {
+ // nothing - they're the same size
+ }
}
}
@@ -748,7 +774,7 @@ impl<'bump> DiffMachine<'bump> {
if old.key() != new.key() {
break;
}
- self.stack.push(DiffInstruction::DiffNode { old, new });
+ self.stack.push(DiffInstruction::Diff { old, new });
left_offset += 1;
}
@@ -887,8 +913,8 @@ impl<'bump> DiffMachine<'bump> {
stack.create_node(new_node, MountType::Absorb);
} else {
// this funciton should never take LIS indicies
- stack.push(DiffInstruction::PrepareMoveNode { node: new_node });
- stack.push(DiffInstruction::DiffNode {
+ stack.push(DiffInstruction::PrepareMove { node: new_node });
+ stack.push(DiffInstruction::Diff {
new: new_node,
old: &old[old_index],
});
@@ -913,7 +939,7 @@ impl<'bump> DiffMachine<'bump> {
// for each spacing, generate a mount instruction
let mut lis_iter = lis_sequence.iter().rev();
let mut last = *lis_iter.next().unwrap();
- while let Some(&next) = lis_iter.next() {
+ for next in lis_iter {
if last - next > 1 {
self.stack.push_nodes_created(0);
self.stack.push(DiffInstruction::Mount {
@@ -925,7 +951,7 @@ impl<'bump> DiffMachine<'bump> {
apply(idx + next + 1, new_node, &mut self.stack);
}
}
- last = next;
+ last = *next;
}
// add mount instruction for the first items not covered by the lis
@@ -943,7 +969,7 @@ impl<'bump> DiffMachine<'bump> {
}
for idx in lis_sequence.iter().rev() {
- self.stack.push(DiffInstruction::DiffNode {
+ self.stack.push(DiffInstruction::Diff {
new: &new[*idx],
old: &old[new_index_to_old_index[*idx]],
});
@@ -970,7 +996,7 @@ impl<'bump> DiffMachine<'bump> {
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
- search_node = Some(scope.root());
+ search_node = Some(scope.root_node());
}
}
}
@@ -988,7 +1014,7 @@ impl<'bump> DiffMachine<'bump> {
VNode::Component(el) => {
let scope_id = el.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
- search_node = Some(scope.root());
+ search_node = Some(scope.root_node());
}
VNode::Text(t) => break t.dom_id.get(),
VNode::Element(t) => break t.dom_id.get(),
@@ -998,6 +1024,14 @@ impl<'bump> DiffMachine<'bump> {
}
}
+ fn replace_and_create_one_with_many(
+ &mut self,
+ old: &'bump VNode<'bump>,
+ new: &'bump [VNode<'bump>],
+ ) {
+ //
+ }
+
fn replace_and_create_many_with_one(
&mut self,
old: &'bump [VNode<'bump>],
@@ -1019,25 +1053,27 @@ impl<'bump> DiffMachine<'bump> {
for node in nodes {
match node {
VNode::Text(t) => {
- t.dom_id.get().map(|id| {
+ if let Some(id) = t.dom_id.get() {
self.mutations.remove(id.as_u64());
self.vdom.collect_garbage(id);
- });
+ }
}
VNode::Suspended(s) => {
- s.dom_id.get().map(|id| {
+ if let Some(id) = s.dom_id.get() {
self.mutations.remove(id.as_u64());
self.vdom.collect_garbage(id);
- });
+ }
}
VNode::Anchor(a) => {
- a.dom_id.get().map(|id| {
+ if let Some(id) = a.dom_id.get() {
self.mutations.remove(id.as_u64());
self.vdom.collect_garbage(id);
- });
+ }
}
VNode::Element(e) => {
- e.dom_id.get().map(|id| self.mutations.remove(id.as_u64()));
+ if let Some(id) = e.dom_id.get() {
+ self.mutations.remove(id.as_u64());
+ }
}
VNode::Fragment(f) => {
self.remove_nodes(f.children);
@@ -1046,7 +1082,7 @@ impl<'bump> DiffMachine<'bump> {
VNode::Component(c) => {
let scope_id = c.associated_scope.get().unwrap();
let scope = self.vdom.get_scope(scope_id).unwrap();
- let root = scope.root();
+ let root = scope.root_node();
self.remove_nodes(Some(root));
}
}
@@ -1070,22 +1106,6 @@ impl<'bump> DiffMachine<'bump> {
}
}
- fn create_garbage(&mut self, node: &'bump VNode<'bump>) {
- match self
- .stack
- .current_scope()
- .and_then(|id| self.vdom.get_scope(id))
- {
- Some(scope) => {
- let garbage: &'bump VNode<'static> = unsafe { std::mem::transmute(node) };
- scope.pending_garbage.borrow_mut().push(garbage);
- }
- None => {
- log::info!("No scope to collect garbage into")
- }
- }
- }
-
/// Adds a listener closure to a scope during diff.
fn attach_listener_to_scope<'a>(&mut self, listener: &'a Listener<'a>, scope: &Scope) {
let mut queue = scope.listeners.borrow_mut();
@@ -1093,35 +1113,18 @@ impl<'bump> DiffMachine<'bump> {
queue.push(long_listener as *const _)
}
- /// Destroy a scope and all of its descendents.
- ///
- /// Calling this will run the destuctors on all hooks in the tree.
- /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered.
- fn destroy_scopes(&mut self, old_scope: ScopeId) {
- let mut nodes_to_delete = vec![old_scope];
- let mut scopes_to_explore = vec![old_scope];
-
- // explore the scope tree breadth first
- while let Some(scope_id) = scopes_to_explore.pop() {
- // If we're planning on deleting this node, then we don't need to both rendering it
- self.seen_scopes.insert(scope_id);
- let scope = self.vdom.get_scope(scope_id).unwrap();
- for child in scope.descendents.borrow().iter() {
- // Add this node to be explored
- scopes_to_explore.push(child.clone());
-
- // Also add it for deletion
- nodes_to_delete.push(child.clone());
- }
- }
-
- // Delete all scopes that we found as part of this subtree
- for node in nodes_to_delete {
- log::debug!("Removing scope {:#?}", node);
- let _scope = self.vdom.try_remove(node).unwrap();
- // do anything we need to do to delete the scope
- // I think we need to run the destructors on the hooks
- // TODO
+ fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) {
+ if let Some(scope) = self
+ .stack
+ .current_scope()
+ .and_then(|id| self.vdom.get_scope_mut(id))
+ {
+ // safety: this lifetime is managed by the logic on scope
+ let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) };
+ scope
+ .suspended_nodes
+ .borrow_mut()
+ .insert(suspended.task_id, extended as *const _);
}
}
}
diff --git a/packages/core/src/diff_stack.rs b/packages/core/src/diff_stack.rs
index 5c4f39646..aa9716cb6 100644
--- a/packages/core/src/diff_stack.rs
+++ b/packages/core/src/diff_stack.rs
@@ -3,23 +3,18 @@ use smallvec::{smallvec, SmallVec};
/// The stack instructions we use to diff and create new nodes.
#[derive(Debug)]
-pub enum DiffInstruction<'a> {
- DiffNode {
+pub(crate) enum DiffInstruction<'a> {
+ Diff {
old: &'a VNode<'a>,
new: &'a VNode<'a>,
},
- DiffChildren {
- old: &'a [VNode<'a>],
- new: &'a [VNode<'a>],
- },
-
Create {
node: &'a VNode<'a>,
},
/// pushes the node elements onto the stack for use in mount
- PrepareMoveNode {
+ PrepareMove {
node: &'a VNode<'a>,
},
@@ -35,7 +30,7 @@ pub enum MountType<'a> {
Absorb,
Append,
Replace { old: &'a VNode<'a> },
- ReplaceByElementId { el: ElementId },
+ ReplaceByElementId { el: Option },
InsertAfter { other_node: &'a VNode<'a> },
InsertBefore { other_node: &'a VNode<'a> },
}
@@ -44,6 +39,7 @@ pub(crate) struct DiffStack<'bump> {
instructions: Vec>,
nodes_created_stack: SmallVec<[usize; 10]>,
pub scope_stack: SmallVec<[ScopeId; 5]>,
+ pub element_id_stack: SmallVec<[ElementId; 5]>,
}
impl<'bump> DiffStack<'bump> {
@@ -52,6 +48,7 @@ impl<'bump> DiffStack<'bump> {
instructions: Vec::with_capacity(1000),
nodes_created_stack: smallvec![],
scope_stack: smallvec![],
+ element_id_stack: smallvec![],
}
}
@@ -63,8 +60,8 @@ impl<'bump> DiffStack<'bump> {
self.instructions.pop()
}
- pub fn pop_scope(&mut self) -> Option {
- self.scope_stack.pop()
+ pub fn pop_off_scope(&mut self) {
+ self.scope_stack.pop();
}
pub fn push(&mut self, instruction: DiffInstruction<'bump>) {
@@ -75,7 +72,7 @@ impl<'bump> DiffStack<'bump> {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and });
- for child in children.into_iter().rev() {
+ for child in children.iter().rev() {
self.instructions
.push(DiffInstruction::Create { node: child });
}
@@ -85,6 +82,10 @@ impl<'bump> DiffStack<'bump> {
self.nodes_created_stack.push(count);
}
+ pub fn _push_element_id(&mut self, id: ElementId) {
+ self.element_id_stack.push(id);
+ }
+
pub fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) {
self.nodes_created_stack.push(0);
self.instructions.push(DiffInstruction::Mount { and });
@@ -100,7 +101,7 @@ impl<'bump> DiffStack<'bump> {
}
pub fn current_scope(&self) -> Option {
- self.scope_stack.last().map(|f| f.clone())
+ self.scope_stack.last().copied()
}
pub fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) {
diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs
index 119afbf35..e11127e43 100644
--- a/packages/core/src/events.rs
+++ b/packages/core/src/events.rs
@@ -3,16 +3,18 @@
//!
//! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might
//! be heavy or need to interact through FFI, so the events themselves are designed to be lazy.
-use crate::innerlude::{ElementId, ScopeId};
-
-use bumpalo::boxed::Box as BumpBox;
-use std::{any::Any, cell::RefCell, fmt::Debug, ops::Deref, rc::Rc};
-
use crate::{
- innerlude::NodeFactory,
- innerlude::{Attribute, Listener, VNode},
+ innerlude::Listener,
+ innerlude::{ElementId, NodeFactory, ScopeId},
+};
+use bumpalo::boxed::Box as BumpBox;
+use std::{
+ any::Any,
+ cell::{Cell, RefCell},
+ fmt::Debug,
+ ops::Deref,
+ rc::Rc,
};
-use std::cell::Cell;
#[derive(Debug)]
pub struct UserEvent {
@@ -74,6 +76,120 @@ impl std::fmt::Debug for SyntheticEvent {
}
}
+/// Priority of Event Triggers.
+///
+/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
+/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
+/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
+///
+/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
+///
+/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
+/// we keep it simple, and just use a 3-tier priority system.
+///
+/// - NoPriority = 0
+/// - LowPriority = 1
+/// - NormalPriority = 2
+/// - UserBlocking = 3
+/// - HighPriority = 4
+/// - ImmediatePriority = 5
+///
+/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
+/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
+/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+pub enum EventPriority {
+ /// Work that must be completed during the EventHandler phase.
+ ///
+ /// Currently this is reserved for controlled inputs.
+ Immediate = 3,
+
+ /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
+ ///
+ /// This is typically reserved for things like user interaction.
+ ///
+ /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
+ High = 2,
+
+ /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
+ /// than "High Priority" events and will take presedence over low priority events.
+ ///
+ /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
+ ///
+ /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
+ Medium = 1,
+
+ /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
+ /// advanced to the front of the work queue until completed.
+ ///
+ /// The primary user of Low Priority work is the asynchronous work system (suspense).
+ ///
+ /// This is considered "idle" work or "background" work.
+ Low = 0,
+}
+
+pub(crate) fn event_meta(event: &UserEvent) -> (bool, EventPriority) {
+ use EventPriority::*;
+
+ match event.name {
+ // clipboard
+ "copy" | "cut" | "paste" => (true, Medium),
+
+ // Composition
+ "compositionend" | "compositionstart" | "compositionupdate" => (true, Low),
+
+ // Keyboard
+ "keydown" | "keypress" | "keyup" => (true, High),
+
+ // Focus
+ "focus" | "blur" => (true, Low),
+
+ // Form
+ "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
+
+ // Mouse
+ "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
+ | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
+ | "mouseleave" | "mouseout" | "mouseover" | "mouseup" => (true, High),
+
+ "mousemove" => (false, Medium),
+
+ // Pointer
+ "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
+ | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
+ (true, Medium)
+ }
+
+ // Selection
+ "select" | "touchcancel" | "touchend" => (true, Medium),
+
+ // Touch
+ "touchmove" | "touchstart" => (true, Medium),
+
+ // Wheel
+ "scroll" | "wheel" => (false, Medium),
+
+ // Media
+ "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
+ | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
+ | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
+ | "timeupdate" | "volumechange" | "waiting" => (true, Medium),
+
+ // Animation
+ "animationstart" | "animationend" | "animationiteration" => (true, Medium),
+
+ // Transition
+ "transitionend" => (true, Medium),
+
+ // Toggle
+ "toggle" => (true, Medium),
+
+ _ => (true, Low),
+ }
+}
+
+pub use on::KeyCode;
+
pub mod on {
//! This module defines the synthetic events that all Dioxus apps enable. No matter the platform, every dioxus renderer
//! will implement the same events and same behavior (bubbling, cancelation, etc).
@@ -125,8 +241,13 @@ pub mod on {
let callback: BumpBox = unsafe { BumpBox::from_raw(cb) };
+
+ // ie oncopy
let event_name = stringify!($name);
+
+ // ie copy
let shortname: &'static str = &event_name[2..];
+
Listener {
event: shortname,
mounted_node: Cell::new(None),
@@ -143,8 +264,10 @@ pub mod on {
ClipboardEventInner(ClipboardEvent): [
/// Called when "copy"
oncopy
+
/// oncut
oncut
+
/// onpaste
onpaste
];
@@ -152,8 +275,10 @@ pub mod on {
CompositionEventInner(CompositionEvent): [
/// oncompositionend
oncompositionend
+
/// oncompositionstart
oncompositionstart
+
/// oncompositionupdate
oncompositionupdate
];
@@ -161,8 +286,10 @@ pub mod on {
KeyboardEventInner(KeyboardEvent): [
/// onkeydown
onkeydown
+
/// onkeypress
onkeypress
+
/// onkeyup
onkeyup
];
@@ -170,6 +297,7 @@ pub mod on {
FocusEventInner(FocusEvent): [
/// onfocus
onfocus
+
/// onblur
onblur
];
@@ -178,12 +306,16 @@ pub mod on {
FormEventInner(FormEvent): [
/// onchange
onchange
- /// oninput
+
+ /// oninput handler
oninput
+
/// oninvalid
oninvalid
+
/// onreset
onreset
+
/// onsubmit
onsubmit
];
@@ -254,36 +386,50 @@ pub mod on {
/// ## Reference
/// - https://www.w3schools.com/tags/ev_onclick.asp
/// - https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event
- ///
onclick
+
/// oncontextmenu
oncontextmenu
+
/// ondoubleclick
ondoubleclick
+
/// ondrag
ondrag
+
/// ondragend
ondragend
+
/// ondragenter
ondragenter
+
/// ondragexit
ondragexit
+
/// ondragleave
ondragleave
+
/// ondragover
ondragover
+
/// ondragstart
ondragstart
+
/// ondrop
ondrop
+
/// onmousedown
onmousedown
+
/// onmouseenter
onmouseenter
+
/// onmouseleave
onmouseleave
+
/// onmousemove
onmousemove
+
/// onmouseout
onmouseout
@@ -294,6 +440,7 @@ pub mod on {
///
/// Triggered when the users's mouse hovers over an element.
onmouseover
+
/// onmouseup
onmouseup
];
@@ -301,22 +448,31 @@ pub mod on {
PointerEventInner(PointerEvent): [
/// pointerdown
onpointerdown
+
/// pointermove
onpointermove
+
/// pointerup
onpointerup
+
/// pointercancel
onpointercancel
+
/// gotpointercapture
ongotpointercapture
+
/// lostpointercapture
onlostpointercapture
+
/// pointerenter
onpointerenter
+
/// pointerleave
onpointerleave
+
/// pointerover
onpointerover
+
/// pointerout
onpointerout
];
@@ -329,10 +485,13 @@ pub mod on {
TouchEventInner(TouchEvent): [
/// ontouchcancel
ontouchcancel
+
/// ontouchend
ontouchend
+
/// ontouchmove
ontouchmove
+
/// ontouchstart
ontouchstart
];
@@ -345,48 +504,70 @@ pub mod on {
MediaEventInner(MediaEvent): [
///abort
onabort
+
///canplay
oncanplay
+
///canplaythrough
oncanplaythrough
+
///durationchange
ondurationchange
+
///emptied
onemptied
+
///encrypted
onencrypted
+
///ended
onended
+
///error
onerror
+
///loadeddata
onloadeddata
+
///loadedmetadata
onloadedmetadata
+
///loadstart
onloadstart
+
///pause
onpause
+
///play
onplay
+
///playing
onplaying
+
///progress
onprogress
+
///ratechange
onratechange
+
///seeked
onseeked
+
///seeking
onseeking
+
///stalled
onstalled
+
///suspend
onsuspend
+
///timeupdate
ontimeupdate
+
///volumechange
onvolumechange
+
///waiting
onwaiting
];
@@ -394,8 +575,10 @@ pub mod on {
AnimationEventInner(AnimationEvent): [
/// onanimationstart
onanimationstart
+
/// onanimationend
onanimationend
+
/// onanimationiteration
onanimationiteration
];
@@ -827,7 +1010,7 @@ pub mod on {
}
// get the raw code
- fn raw_code(&self) -> u32 {
+ pub fn raw_code(&self) -> u32 {
*self as u32
}
}
diff --git a/packages/core/src/hooklist.rs b/packages/core/src/hooklist.rs
index ebfe5723a..2f68be3b8 100644
--- a/packages/core/src/hooklist.rs
+++ b/packages/core/src/hooklist.rs
@@ -3,45 +3,28 @@ use std::{
cell::{Cell, RefCell, UnsafeCell},
};
+/// An abstraction over internally stored data using a hook-based memory layout.
+///
+/// Hooks are allocated using Boxes and then our stored references are given out.
+///
+/// It's unsafe to "reset" the hooklist, but it is safe to add hooks into it.
+///
+/// Todo: this could use its very own bump arena, but that might be a tad overkill
+#[derive(Default)]
pub(crate) struct HookList {
- vals: RefCell>>>,
+ vals: RefCell>, Box)>>,
idx: Cell,
}
-impl Default for HookList {
- fn default() -> Self {
- Self {
- vals: Default::default(),
- idx: Cell::new(0),
- }
- }
-}
-
-struct InnerHook {
- cell: UnsafeCell,
-}
-
-impl InnerHook {
- fn new(new: T) -> Self {
- Self {
- cell: UnsafeCell::new(new),
- }
- }
-}
-
impl HookList {
pub(crate) fn next(&self) -> Option<&mut T> {
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
self.idx.set(self.idx.get() + 1);
- let raw_box = unsafe { &mut *inn.cell.get() };
+ let raw_box = unsafe { &mut *inn.0.get() };
raw_box.downcast_mut::()
})
}
- pub(crate) fn push(&self, new: T) {
- self.vals.borrow_mut().push(InnerHook::new(Box::new(new)))
- }
-
/// This resets the internal iterator count
/// It's okay that we've given out each hook, but now we have the opportunity to give it out again
/// Therefore, resetting is cosudered unsafe
@@ -52,18 +35,31 @@ impl HookList {
self.idx.set(0);
}
- #[inline]
+ pub(crate) fn push_hook(&self, new: T, cleanup: Box) {
+ self.vals
+ .borrow_mut()
+ .push((UnsafeCell::new(Box::new(new)), cleanup))
+ }
+
pub(crate) fn len(&self) -> usize {
self.vals.borrow().len()
}
- #[inline]
pub(crate) fn cur_idx(&self) -> usize {
self.idx.get()
}
- #[inline]
pub(crate) fn at_end(&self) -> bool {
self.cur_idx() >= self.len()
}
}
+
+// When the scope is dropped, we want to call the cleanup function for each of the hooks
+impl Drop for HookList {
+ fn drop(&mut self) {
+ self.vals
+ .borrow_mut()
+ .drain(..)
+ .for_each(|(mut state, cleanup)| cleanup(state.get_mut()));
+ }
+}
diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs
index 47e7fcda9..f90f42d59 100644
--- a/packages/core/src/hooks.rs
+++ b/packages/core/src/hooks.rs
@@ -11,83 +11,27 @@
use crate::innerlude::*;
use futures_util::FutureExt;
-use std::{
- any::{Any, TypeId},
- cell::RefCell,
- future::Future,
- rc::Rc,
-};
-
-/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
-///
-/// This is a hook, so it may not be called conditionally!
-///
-/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
-/// so don't put it in a conditional.
-///
-/// When the component is dropped, so is the context. Be aware of this behavior when consuming
-/// the context via Rc/Weak.
-///
-///
-///
-pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc
-where
- T: 'static,
- F: FnOnce() -> T,
-{
- let ty = TypeId::of::();
- let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty);
-
- let is_initialized = cx.use_hook(
- |_| false,
- |s| {
- let i = s.clone();
- *s = true;
- i
- },
- |_| {},
- );
-
- match (is_initialized, contains_key) {
- // Do nothing, already initialized and already exists
- (true, true) => {}
-
- // Needs to be initialized
- (false, false) => cx.add_shared_state(init()),
-
- _ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
- };
-
- use_consume_state::(cx)
-}
-
-/// There are hooks going on here!
-pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc {
- use_try_consume_state::(cx).unwrap()
-}
-
-/// Uses a context, storing the cached value around
-///
-/// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a
-/// context was added later. This allows using another hook as a fallback
-///
-pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc> {
- struct UseContextHook(Option>);
-
- cx.use_hook(
- move |_| UseContextHook(cx.consume_shared_state::()),
- move |hook| hook.0.as_ref(),
- |_| {},
- )
-}
+use std::{any::Any, cell::RefCell, future::Future, ops::Deref, rc::Rc};
/// Awaits the given task, forcing the component to re-render when the value is ready.
///
+/// Returns the handle to the task and the value (if it is ready, else None).
///
+/// ```
+/// static Example: FC<()> = |cx, props| {
+/// let (task, value) = use_task(|| async {
+/// timer::sleep(Duration::from_secs(1)).await;
+/// "Hello World"
+/// });
///
-///
-pub fn use_task<'src, Out, Fut, Init, P>(
- cx: Context<'src, P>,
+/// match contents {
+/// Some(contents) => rsx!(cx, div { "{title}" }),
+/// None => rsx!(cx, div { "Loading..." }),
+/// }
+/// };
+/// ```
+pub fn use_task<'src, Out, Fut, Init>(
+ cx: Context<'src>,
task_initializer: Init,
) -> (&'src TaskHandle, &'src Option)
where
@@ -110,7 +54,7 @@ where
let slot = task_dump.clone();
- let updater = cx.prepare_update();
+ let updater = cx.schedule_update_any();
let originator = cx.scope.our_arena_idx;
let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
@@ -145,8 +89,8 @@ where
/// # Example
///
///
-pub fn use_suspense<'src, Out, Fut, Cb, P>(
- cx: Context<'src, P>,
+pub fn use_suspense<'src, Out, Fut, Cb>(
+ cx: Context<'src>,
task_initializer: impl FnOnce() -> Fut,
user_callback: Cb,
) -> DomTree<'src>
@@ -166,11 +110,10 @@ where
- if it doesn't, then we render a suspended node along with with the callback and task id
*/
cx.use_hook(
- move |hook_idx| {
+ move |_| {
let value = Rc::new(RefCell::new(None));
let slot = value.clone();
-
- let originator = cx.scope.our_arena_idx.clone();
+ let originator = cx.scope.our_arena_idx;
let handle = cx.submit_task(Box::pin(task_initializer().then(
move |output| async move {
@@ -185,10 +128,7 @@ where
Some(value) => {
let out = value.downcast_ref::().unwrap();
let sus = SuspendedContext {
- inner: Context {
- props: &(),
- scope: cx.scope,
- },
+ inner: Context { scope: cx.scope },
};
user_callback(sus, out)
}
@@ -232,10 +172,8 @@ pub(crate) struct SuspenseHook {
pub value: Rc>>>,
}
-type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>;
-
pub struct SuspendedContext<'a> {
- pub(crate) inner: Context<'a, ()>,
+ pub(crate) inner: Context<'a>,
}
impl<'src> SuspendedContext<'src> {
@@ -243,25 +181,21 @@ impl<'src> SuspendedContext<'src> {
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
- let scope_ref = self.inner.scope;
let bump = &self.inner.scope.frames.wip_frame().bump;
-
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
}
#[derive(Clone, Copy)]
-pub struct NodeRef<'src, T: 'static>(&'src RefCell);
+pub struct NodeRef<'src, T: 'static>(&'src RefCell>);
-pub fn use_node_ref(cx: Context) -> NodeRef {
- cx.use_hook(
- |f| {},
- |f| {
- //
- todo!()
- },
- |f| {
- //
- },
- )
+impl<'a, T> Deref for NodeRef<'a, T> {
+ type Target = RefCell>;
+ fn deref(&self) -> &Self::Target {
+ self.0
+ }
+}
+
+pub fn use_node_ref(cx: Context) -> NodeRef {
+ cx.use_hook(|_| RefCell::new(None), |f| NodeRef { 0: f }, |_| {})
}
diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs
index b588215b3..33aa21ede 100644
--- a/packages/core/src/lib.rs
+++ b/packages/core/src/lib.rs
@@ -4,7 +4,7 @@
/*
Navigating this crate:
- virtual_dom: the primary entrypoint for the crate
-- scheduler: the core interior logic called by virtual_dom
+- scheduler: the core interior logic called by the [`VirtualDom`]
- nodes: the definition of VNodes, listeners, etc.
- diff: the stackmachine-based diffing algorithm
- hooks: foundational hooks that require crate-private APIs
@@ -24,8 +24,10 @@ pub mod hooklist;
pub mod hooks;
pub mod mutations;
pub mod nodes;
+pub mod resources;
pub mod scheduler;
pub mod scope;
+pub mod tasks;
pub mod test_dom;
pub mod util;
pub mod virtual_dom;
@@ -43,22 +45,24 @@ pub(crate) mod innerlude {
pub use crate::hooks::*;
pub use crate::mutations::*;
pub use crate::nodes::*;
+ pub(crate) use crate::resources::*;
pub use crate::scheduler::*;
pub use crate::scope::*;
+ pub use crate::tasks::*;
pub use crate::test_dom::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
pub type DomTree<'a> = Option>;
- pub type FC = fn(Context
) -> DomTree;
+ pub type FC
= for<'a> fn(Context<'a>, &'a P) -> DomTree<'a>;
pub use dioxus_core_macro::{format_args_f, html, rsx};
}
pub use crate::innerlude::{
- format_args_f, html, rsx, Context, DiffInstruction, DioxusElement, DomEdit, DomTree, ElementId,
- EventPriority, LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId,
- SuspendedContext, SyntheticEvent, TestDom, UserEvent, VNode, VirtualDom, FC,
+ format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
+ LazyNodes, MountType, Mutations, NodeFactory, Properties, ScopeId, SuspendedContext,
+ SyntheticEvent, TaskHandle, TestDom, UserEvent, VNode, VirtualDom, FC,
};
pub mod prelude {
diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs
index 58f259c72..5346042a1 100644
--- a/packages/core/src/nodes.rs
+++ b/packages/core/src/nodes.rs
@@ -12,7 +12,6 @@ use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
marker::PhantomData,
- rc::Rc,
};
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
@@ -26,6 +25,7 @@ pub enum VNode<'src> {
///
/// # Example
///
+ /// ```
/// let node = cx.render(rsx!{ "hello" }).unwrap();
///
/// if let VNode::Text(vtext) = node {
@@ -188,6 +188,8 @@ pub struct VElement<'a> {
pub dom_id: Cell>,
+ pub parent_id: Cell >,
+
pub listeners: &'a [Listener<'a>],
pub attributes: &'a [Attribute<'a>],
@@ -195,6 +197,21 @@ pub struct VElement<'a> {
pub children: &'a [VNode<'a>],
}
+impl Debug for VElement<'_> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("VElement")
+ .field("tag_name", &self.tag_name)
+ .field("namespace", &self.namespace)
+ .field("key", &self.key)
+ .field("dom_id", &self.dom_id)
+ .field("parent_id", &self.parent_id)
+ .field("listeners", &self.listeners.len())
+ .field("attributes", &self.attributes)
+ .field("children", &self.children)
+ .finish()
+ }
+}
+
/// A trait for any generic Dioxus Element.
///
/// This trait provides the ability to use custom elements in the `rsx!` macro.
@@ -244,13 +261,16 @@ pub struct Attribute<'a> {
/// An event listener.
/// IE onclick, onkeydown, etc
pub struct Listener<'bump> {
+ /// The ID of the node that this listener is mounted to
+ /// Used to generate the event listener's ID on the DOM
pub mounted_node: Cell >,
/// The type of event to listen for.
///
- /// IE "onclick" - whatever the renderer needs to attach the listener by name.
+ /// IE "click" - whatever the renderer needs to attach the listener by name.
pub event: &'static str,
+ /// The actual callback that the user specified
pub(crate) callback: RefCell >>,
}
@@ -266,7 +286,7 @@ pub struct VComponent<'src> {
// Function pointer to the FC that was used to generate this component
pub user_fc: *const (),
- pub(crate) caller: Rc DomTree>,
+ pub(crate) caller: &'src dyn for<'b> Fn(&'b Scope) -> DomTree<'b>,
pub(crate) children: &'src [VNode<'src>],
@@ -283,8 +303,7 @@ pub struct VComponent<'src> {
pub struct VSuspended<'a> {
pub task_id: u64,
pub dom_id: Cell>,
- pub(crate) callback:
- RefCell ) -> DomTree<'a>>>>,
+ pub callback: RefCell ) -> DomTree<'a>>>>,
}
/// This struct provides an ergonomic API to quickly build VNodes.
@@ -303,7 +322,7 @@ impl<'a> NodeFactory<'a> {
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
- &self.bump
+ self.bump
}
pub fn render_directly(&self, lazy_nodes: LazyNodes<'a, F>) -> DomTree<'a>
@@ -413,6 +432,7 @@ impl<'a> NodeFactory<'a> {
attributes,
children,
dom_id: empty_cell(),
+ parent_id: empty_cell(),
}))
}
@@ -461,12 +481,12 @@ impl<'a> NodeFactory<'a> {
// - This comparator is only called on a corresponding set of bumpframes
let props_memoized = unsafe {
let real_other: &P = &*(other.raw_props as *const _ as *const P);
- props.memoize(&real_other)
+ props.memoize(real_other)
};
// It's only okay to memoize if there are no children and the props can be memoized
// Implementing memoize is unsafe and done automatically with the props trait
- match (props_memoized, children.len() == 0) {
+ match (props_memoized, children.is_empty()) {
(true, true) => true,
_ => false,
}
@@ -488,6 +508,8 @@ impl<'a> NodeFactory<'a> {
std::mem::drop(b);
has_dropped = true;
+ } else {
+ panic!("Drop props called twice - this is an internal failure of Dioxus");
}
}
});
@@ -497,15 +519,20 @@ impl<'a> NodeFactory<'a> {
RefCell::new(Some(drop_props))
};
- let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
+ let is_static = children.is_empty() && P::IS_STATIC && key.is_none();
let key = key.map(|f| self.raw_text(f).0);
- let caller = NodeFactory::create_component_caller(component, raw_props);
+ let caller: &'a mut dyn for<'b> Fn(&'b Scope) -> DomTree<'b> =
+ bump.alloc(move |scope: &Scope| -> DomTree {
+ let props: &'_ P = unsafe { &*(raw_props as *const P) };
+ let res = component(Context { scope }, props);
+ unsafe { std::mem::transmute(res) }
+ });
- let can_memoize = children.len() == 0 && P::IS_STATIC;
+ let can_memoize = children.is_empty() && P::IS_STATIC;
- VNode::Component(bump.alloc_with(|| VComponent {
+ VNode::Component(bump.alloc(VComponent {
user_fc,
comparator,
raw_props,
@@ -519,24 +546,6 @@ impl<'a> NodeFactory<'a> {
}))
}
- pub(crate) fn create_component_caller<'g, P: 'g>(
- component: FC,
- raw_props: *const (),
- ) -> Rc Fn(&'r Scope) -> DomTree<'r>> {
- type Captured<'a> = Rc Fn(&'r Scope) -> DomTree<'r> + 'a>;
- let caller: Captured = Rc::new(move |scp: &Scope| -> DomTree {
- // cast back into the right lifetime
- let safe_props: &'_ P = unsafe { &*(raw_props as *const P) };
- let cx: Context = Context {
- props: safe_props,
- scope: scp,
- };
- let res = component(cx);
- unsafe { std::mem::transmute(res) }
- });
- unsafe { std::mem::transmute::<_, Captured<'static>>(caller) }
- }
-
pub fn fragment_from_iter(self, node_iter: impl IntoVNodeList<'a>) -> VNode<'a> {
let children = node_iter.into_vnode_list(self);
@@ -602,7 +611,7 @@ where
nodes.push(node.into_vnode(cx));
}
- if nodes.len() == 0 {
+ if nodes.is_empty() {
nodes.push(VNode::Anchor(VAnchor {
dom_id: empty_cell(),
}));
@@ -722,14 +731,14 @@ where
// Conveniently, we also support "null" (nothing) passed in
impl IntoVNode<'_> for () {
- fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> {
+ fn into_vnode(self, cx: NodeFactory) -> VNode {
cx.fragment_from_iter(None as Option)
}
}
// Conveniently, we also support "None"
impl IntoVNode<'_> for Option<()> {
- fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> {
+ fn into_vnode(self, cx: NodeFactory) -> VNode {
cx.fragment_from_iter(None as Option)
}
}
@@ -744,12 +753,12 @@ impl<'a> IntoVNode<'a> for Option> {
}
impl IntoVNode<'_> for &'static str {
- fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> {
+ fn into_vnode(self, cx: NodeFactory) -> VNode {
cx.static_text(self)
}
}
impl IntoVNode<'_> for Arguments<'_> {
- fn into_vnode<'a>(self, cx: NodeFactory<'a>) -> VNode<'a> {
+ fn into_vnode(self, cx: NodeFactory) -> VNode {
cx.text(self)
}
}
@@ -763,13 +772,12 @@ impl Debug for NodeFactory<'_> {
impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self {
- VNode::Element(el) => {
- //
- s.debug_struct("VElement")
- .field("name", &el.tag_name)
- .field("key", &el.key)
- .finish()
- }
+ VNode::Element(el) => s
+ .debug_struct("VElement")
+ .field("name", &el.tag_name)
+ .field("key", &el.key)
+ .finish(),
+
VNode::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNode::Anchor(_) => write!(s, "VAnchor"),
diff --git a/packages/core/src/resources.rs b/packages/core/src/resources.rs
new file mode 100644
index 000000000..f0361b5f7
--- /dev/null
+++ b/packages/core/src/resources.rs
@@ -0,0 +1,89 @@
+use crate::innerlude::*;
+use slab::Slab;
+
+use std::{cell::UnsafeCell, rc::Rc};
+#[derive(Clone)]
+pub(crate) struct ResourcePool {
+ /*
+ This *has* to be an UnsafeCell.
+
+ Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to
+ its bumpframe conents immutably.
+
+ However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly
+ simple to audit.
+
+ Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!)
+ */
+ pub components: Rc>>,
+
+ /*
+ Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy
+ that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client.
+
+ This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse.
+
+ In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes...
+ */
+ pub raw_elements: Rc>>>,
+
+ pub channel: EventChannel,
+}
+
+impl ResourcePool {
+ /// this is unsafe because the caller needs to track which other scopes it's already using
+ pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
+ let inner = unsafe { &*self.components.get() };
+ inner.get(idx.0)
+ }
+
+ /// this is unsafe because the caller needs to track which other scopes it's already using
+ pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
+ let inner = unsafe { &mut *self.components.get() };
+ inner.get_mut(idx.0)
+ }
+
+ // return a bumpframe with a lifetime attached to the arena borrow
+ // this is useful for merging lifetimes
+ pub fn with_scope_vnode<'b>(
+ &self,
+ _id: ScopeId,
+ _f: impl FnOnce(&mut Scope) -> &VNode<'b>,
+ ) -> Option<&VNode<'b>> {
+ todo!()
+ }
+
+ pub fn try_remove(&self, id: ScopeId) -> Option {
+ let inner = unsafe { &mut *self.components.get() };
+ Some(inner.remove(id.0))
+ // .try_remove(id.0)
+ // .ok_or_else(|| Error::FatalInternal("Scope not found"))
+ }
+
+ pub fn reserve_node<'a>(&self, node: &'a VNode<'a>) -> ElementId {
+ let els = unsafe { &mut *self.raw_elements.get() };
+ let entry = els.vacant_entry();
+ let key = entry.key();
+ let id = ElementId(key);
+ let node = node as *const _;
+ let node = unsafe { std::mem::transmute(node) };
+ entry.insert(node);
+ id
+ }
+
+ /// return the id, freeing the space of the original node
+ pub fn collect_garbage(&self, id: ElementId) {
+ todo!("garabge collection currently WIP")
+ // self.raw_elements.remove(id.0);
+ }
+
+ pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
+ let g = unsafe { &mut *self.components.get() };
+ let entry = g.vacant_entry();
+ let id = ScopeId(entry.key());
+ entry.insert(f(id));
+ id
+ }
+
+ pub fn borrow_bumpframe(&self) {}
+}
diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs
index d173a81f5..dcb6acb33 100644
--- a/packages/core/src/scheduler.rs
+++ b/packages/core/src/scheduler.rs
@@ -14,7 +14,7 @@ Some essential reading:
Dioxus is a framework for "user experience" - not just "user interfaces." Part of the "experience" is keeping the UI
snappy and "jank free" even under heavy work loads. Dioxus already has the "speed" part figured out - but there's no
-point if being "fast" if you can't also be "responsive."
+point in being "fast" if you can't also be "responsive."
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
@@ -71,33 +71,41 @@ For the rest, we defer to the rIC period and work down each queue from high to l
use crate::heuristics::*;
use crate::innerlude::*;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
-use futures_util::stream::FuturesUnordered;
-use futures_util::{future::FusedFuture, pin_mut, Future, FutureExt, StreamExt};
-use fxhash::{FxHashMap, FxHashSet};
+use futures_util::{pin_mut, stream::FuturesUnordered, Future, FutureExt, StreamExt};
+use fxhash::FxHashMap;
+use fxhash::FxHashSet;
use indexmap::IndexSet;
use slab::Slab;
-use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
- cell::{Cell, RefCell, RefMut, UnsafeCell},
- collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque},
- fmt::Display,
- pin::Pin,
+ cell::{Cell, UnsafeCell},
+ collections::{HashSet, VecDeque},
rc::Rc,
};
#[derive(Clone)]
-pub struct EventChannel {
+pub(crate) struct EventChannel {
pub task_counter: Rc>,
pub sender: UnboundedSender,
pub schedule_any_immediate: Rc,
pub submit_task: Rc TaskHandle>,
- pub get_shared_context: Rc Option>>,
+ pub get_shared_context: GetSharedContext,
}
+pub type GetSharedContext = Rc Option>>;
+
pub enum SchedulerMsg {
- Immediate(ScopeId),
+ // events from the host
UiEvent(UserEvent),
+
+ // setstate
+ Immediate(ScopeId),
+
+ // tasks
+ Task(TaskMsg),
+}
+
+pub enum TaskMsg {
SubmitTask(FiberTask, u64),
ToggleTask(u64),
PauseTask(u64),
@@ -109,7 +117,7 @@ pub enum SchedulerMsg {
///
/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components.
///
-/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
+/// In Dioxus, the scheduler provides 4 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs
/// out.
///
/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between
@@ -137,22 +145,25 @@ pub(crate) struct Scheduler {
// In-flight futures
pub async_tasks: FuturesUnordered,
- // scheduler stuff
- pub current_priority: EventPriority,
-
+ // // scheduler stuff
+ // pub current_priority: EventPriority,
pub ui_events: VecDeque,
pub pending_immediates: VecDeque,
pub pending_tasks: VecDeque,
+ pub batched_events: VecDeque,
+
pub garbage_scopes: HashSet,
- pub lanes: [PriorityLane; 4],
+ pub dirty_scopes: IndexSet,
+ pub saved_state: Option>,
+ pub in_progress: bool,
}
impl Scheduler {
- pub fn new() -> Self {
+ pub(crate) fn new() -> Self {
/*
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
Perhaps this should be configurable from some external config?
@@ -173,12 +184,13 @@ impl Scheduler {
Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap())
},
submit_task: {
- let sender = sender.clone();
Rc::new(move |fiber_task| {
let task_id = task_counter.get();
task_counter.set(task_id + 1);
sender
- .unbounded_send(SchedulerMsg::SubmitTask(fiber_task, task_id))
+ .unbounded_send(SchedulerMsg::Task(TaskMsg::SubmitTask(
+ fiber_task, task_id,
+ )))
.unwrap();
TaskHandle {
our_id: task_id,
@@ -204,155 +216,217 @@ impl Scheduler {
};
let pool = ResourcePool {
- components: components.clone(),
+ components,
raw_elements,
channel,
};
+ let async_tasks = FuturesUnordered::new();
+
+ // push a task that would never resolve - prevents us from immediately aborting the scheduler
+ async_tasks.push(Box::pin(async {
+ std::future::pending::<()>().await;
+ ScopeId(0)
+ }) as FiberTask);
+
+ let saved_state = SavedDiffWork {
+ mutations: Mutations::new(),
+ stack: DiffStack::new(),
+ seen_scopes: Default::default(),
+ };
+
Self {
pool,
+
receiver,
- async_tasks: FuturesUnordered::new(),
+ async_tasks,
pending_garbage: FxHashSet::default(),
heuristics,
- // a storage for our receiver to dump into
ui_events: VecDeque::new(),
pending_immediates: VecDeque::new(),
pending_tasks: VecDeque::new(),
+ batched_events: VecDeque::new(),
+
garbage_scopes: HashSet::new(),
- current_priority: EventPriority::Low,
-
- // a dedicated fiber for each priority
- lanes: [
- PriorityLane::new(),
- PriorityLane::new(),
- PriorityLane::new(),
- PriorityLane::new(),
- ],
+ dirty_scopes: Default::default(),
+ saved_state: Some(saved_state),
+ in_progress: false,
}
}
- pub fn manually_poll_events(&mut self) {
- while let Ok(Some(msg)) = self.receiver.try_next() {
- self.handle_channel_msg(msg);
- }
- }
+ // returns true if the event is discrete
+ pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
+ let (discrete, priority) = event_meta(&event);
- // Converts UI events into dirty scopes with various priorities
- pub fn consume_pending_events(&mut self) {
- while let Some(trigger) = self.ui_events.pop_back() {
- if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
- if let Some(element) = trigger.mounted_dom_id {
- let priority = match &trigger.event {
- SyntheticEvent::ClipboardEvent(_) => {}
- SyntheticEvent::CompositionEvent(_) => {}
- SyntheticEvent::KeyboardEvent(_) => {}
- SyntheticEvent::FocusEvent(_) => {}
- SyntheticEvent::FormEvent(_) => {}
- SyntheticEvent::SelectionEvent(_) => {}
- SyntheticEvent::TouchEvent(_) => {}
- SyntheticEvent::WheelEvent(_) => {}
- SyntheticEvent::MediaEvent(_) => {}
- SyntheticEvent::AnimationEvent(_) => {}
- SyntheticEvent::TransitionEvent(_) => {}
- SyntheticEvent::ToggleEvent(_) => {}
- SyntheticEvent::MouseEvent(_) => {}
- SyntheticEvent::PointerEvent(_) => {}
- SyntheticEvent::GenericEvent(_) => {}
- };
+ if let Some(scope) = self.pool.get_scope_mut(event.scope) {
+ if let Some(element) = event.mounted_dom_id {
+ // TODO: bubble properly here
+ scope.call_listener(event, element);
- scope.call_listener(trigger.event, element);
- // let receiver = self.immediate_receiver.clone();
- // let mut receiver = receiver.borrow_mut();
-
- // // Drain the immediates into the dirty scopes, setting the appropiate priorities
- // while let Ok(Some(dirty_scope)) = receiver.try_next() {
+ while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
+ //
// self.add_dirty_scope(dirty_scope, trigger.priority)
- // }
}
}
}
+
+ // use EventPriority::*;
+
+ // match priority {
+ // Immediate => todo!(),
+ // High => todo!(),
+ // Medium => todo!(),
+ // Low => todo!(),
+ // }
+
+ discrete
+ }
+
+ fn prepare_work(&mut self) {
+ // while let Some(trigger) = self.ui_events.pop_back() {
+ // if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {}
+ // }
}
// nothing to do, no events on channels, no work
pub fn has_any_work(&self) -> bool {
- let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
- pending_lanes || self.has_pending_events()
- }
-
- pub fn has_pending_events(&self) -> bool {
- self.ui_events.len() > 0
- }
-
- fn shift_priorities(&mut self) {
- self.current_priority = match (
- self.lanes[0].has_work(),
- self.lanes[1].has_work(),
- self.lanes[2].has_work(),
- self.lanes[3].has_work(),
- ) {
- (true, _, _, _) => EventPriority::Immediate,
- (false, true, _, _) => EventPriority::High,
- (false, false, true, _) => EventPriority::Medium,
- (false, false, false, _) => EventPriority::Low,
- };
+ !(self.dirty_scopes.is_empty() && self.ui_events.is_empty())
}
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
fn balance_lanes(&mut self) {}
- fn load_current_lane(&mut self) -> &mut PriorityLane {
- match self.current_priority {
- EventPriority::Immediate => todo!(),
- EventPriority::High => todo!(),
- EventPriority::Medium => todo!(),
- EventPriority::Low => todo!(),
- }
- }
-
fn save_work(&mut self, lane: SavedDiffWork) {
let saved: SavedDiffWork<'static> = unsafe { std::mem::transmute(lane) };
- self.load_current_lane().saved_state = Some(saved);
+ self.saved_state = Some(saved);
}
- fn load_work(&mut self) -> SavedDiffWork<'static> {
- match self.current_priority {
- EventPriority::Immediate => todo!(),
- EventPriority::High => todo!(),
- EventPriority::Medium => todo!(),
- EventPriority::Low => todo!(),
+ unsafe fn load_work(&mut self) -> SavedDiffWork<'static> {
+ self.saved_state.take().unwrap().extend()
+ }
+
+ pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
+ match msg {
+ //
+ SchedulerMsg::Task(msg) => todo!(),
+
+ SchedulerMsg::Immediate(_) => todo!(),
+
+ SchedulerMsg::UiEvent(event) => {
+ //
+
+ let (discrete, priority) = event_meta(&event);
+
+ if let Some(scope) = self.pool.get_scope_mut(event.scope) {
+ if let Some(element) = event.mounted_dom_id {
+ // TODO: bubble properly here
+ scope.call_listener(event, element);
+
+ while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
+ //
+ // self.add_dirty_scope(dirty_scope, trigger.priority)
+ }
+ }
+ }
+
+ discrete;
+ }
}
}
- /// Work the scheduler down, not polling any ongoing tasks.
+ /// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
///
- /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
- pub fn work_sync<'a>(&'a mut self) -> Vec> {
- let mut committed_mutations = Vec::new();
+ /// Returns true if the lane is finished before the deadline could be met.
+ pub fn work_on_current_lane(
+ &mut self,
+ deadline_reached: impl FnMut() -> bool,
+ mutations: &mut Vec,
+ ) -> bool {
+ // Work through the current subtree, and commit the results when it finishes
+ // When the deadline expires, give back the work
+ let saved_state = unsafe { self.load_work() };
- self.manually_poll_events();
+ // We have to split away some parts of ourself - current lane is borrowed mutably
+ let shared = self.pool.clone();
+ let mut machine = unsafe { saved_state.promote(&shared) };
- if !self.has_any_work() {
- self.clean_up_garbage();
- return committed_mutations;
+ let mut ran_scopes = FxHashSet::default();
+
+ if machine.stack.is_empty() {
+ let shared = self.pool.clone();
+
+ self.dirty_scopes.sort_by(|a, b| {
+ let h1 = shared.get_scope(*a).unwrap().height;
+ let h2 = shared.get_scope(*b).unwrap().height;
+ h1.cmp(&h2)
+ });
+
+ if let Some(scopeid) = self.dirty_scopes.pop() {
+ log::info!("handlng dirty scope {:#?}", scopeid);
+ if !ran_scopes.contains(&scopeid) {
+ ran_scopes.insert(scopeid);
+
+ let mut component = self.pool.get_scope_mut(scopeid).unwrap();
+ if component.run_scope(&self.pool) {
+ let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
+ // let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
+ machine.stack.scope_stack.push(scopeid);
+ machine.stack.push(DiffInstruction::Diff { new, old });
+ }
+ }
+ }
}
- self.consume_pending_events();
+ let work_completed = machine.work(deadline_reached);
- while self.has_any_work() {
- self.shift_priorities();
- self.work_on_current_lane(&mut || false, &mut committed_mutations);
+ log::debug!("Working finished? {:?}", work_completed);
+
+ // log::debug!("raw edits {:?}", machine.mutations.edits);
+
+ let mut machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
+ // let mut saved = machine.save();
+
+ if work_completed {
+ for node in machine.seen_scopes.drain() {
+ // self.dirty_scopes.clear();
+ // self.ui_events.clear();
+ self.dirty_scopes.remove(&node);
+ // self.dirty_scopes.remove(&node);
+ }
+
+ let mut new_mutations = Mutations::new();
+
+ for edit in machine.mutations.edits.drain(..) {
+ new_mutations.edits.push(edit);
+ }
+
+ // for edit in saved.edits.drain(..) {
+ // new_mutations.edits.push(edit);
+ // }
+
+ // std::mem::swap(&mut new_mutations, &mut saved.mutations);
+
+ mutations.push(new_mutations);
+
+ // log::debug!("saved edits {:?}", mutations);
+
+ let mut saved = machine.save();
+ self.save_work(saved);
+ true
+
+ // self.save_work(saved);
+ // false
+ } else {
+ false
}
-
- committed_mutations
}
/// The primary workhorse of the VirtualDOM.
@@ -360,9 +434,9 @@ impl Scheduler {
/// Uses some fairly complex logic to schedule what work should be produced.
///
/// Returns a list of successful mutations.
- pub async fn work_with_deadline<'a>(
+ pub fn work_with_deadline<'a>(
&'a mut self,
- mut deadline_reached: Pin>>,
+ mut deadline: impl FnMut() -> bool,
) -> Vec> {
/*
Strategy:
@@ -378,6 +452,14 @@ impl Scheduler {
- If there are no pending discrete events, then check for continuous events. These can be completely batched
+ - we batch completely until we run into a discrete event
+ - all continuous events are batched together
+ - so D C C C C C would be two separate events - D and C. IE onclick and onscroll
+ - D C C C C C C D C C C D would be D C D C D in 5 distinct phases.
+
+ - !listener bubbling is not currently implemented properly and will need to be implemented somehow in the future
+ - we need to keep track of element parents to be able to traverse properly
+
Open questions:
- what if we get two clicks from the component during the same slice?
@@ -387,427 +469,107 @@ impl Scheduler {
*/
let mut committed_mutations = Vec::>::new();
- loop {
- // Internalize any pending work since the last time we ran
- self.manually_poll_events();
+ while self.has_any_work() {
+ // switch our priority, pop off any work
+ while let Some(event) = self.ui_events.pop_front() {
+ if let Some(scope) = self.pool.get_scope_mut(event.scope) {
+ if let Some(element) = event.mounted_dom_id {
+ log::info!("Calling listener {:?}, {:?}", event.scope, element);
- // Wait for any new events if we have nothing to do
- if !self.has_any_work() {
- self.clean_up_garbage();
- let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await;
+ // TODO: bubble properly here
+ scope.call_listener(event, element);
- if deadline_expired {
- return committed_mutations;
+ while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
+ match dirty_scope {
+ SchedulerMsg::Immediate(im) => {
+ self.dirty_scopes.insert(im);
+ }
+ SchedulerMsg::UiEvent(e) => self.ui_events.push_back(e),
+ SchedulerMsg::Task(_) => todo!(),
+ }
+ }
+ }
}
}
- // Create work from the pending event queue
- self.consume_pending_events();
+ let work_complete = self.work_on_current_lane(&mut deadline, &mut committed_mutations);
- // shift to the correct lane
- self.shift_priorities();
-
- let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some();
-
- let finished_before_deadline =
- self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
-
- if !finished_before_deadline {
- break;
+ if !work_complete {
+ return committed_mutations;
}
}
committed_mutations
}
- /// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
+ /// Work the scheduler down, not polling any ongoing tasks.
///
- /// Returns true if the lane is finished before the deadline could be met.
- pub fn work_on_current_lane(
- &mut self,
- deadline_reached: &mut impl FnMut() -> bool,
- mutations: &mut Vec,
- ) -> bool {
- // Work through the current subtree, and commit the results when it finishes
- // When the deadline expires, give back the work
- let saved_state = self.load_work();
+ /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
+ pub fn work_sync<'a>(&'a mut self) -> Vec> {
+ let mut committed_mutations = Vec::new();
- // We have to split away some parts of ourself - current lane is borrowed mutably
+ while let Ok(Some(msg)) = self.receiver.try_next() {
+ self.handle_channel_msg(msg);
+ }
+
+ if !self.has_any_work() {
+ return committed_mutations;
+ }
+
+ while self.has_any_work() {
+ self.prepare_work();
+ self.work_on_current_lane(|| false, &mut committed_mutations);
+ }
+
+ committed_mutations
+ }
+
+ /// Restart the entire VirtualDOM from scratch, wiping away any old state and components.
+ ///
+ /// Typically used to kickstart the VirtualDOM after initialization.
+ pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
let mut shared = self.pool.clone();
- let mut machine = unsafe { saved_state.promote(&mut shared) };
+ let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
- if machine.stack.is_empty() {
- let shared = self.pool.clone();
+ // TODO: drain any in-flight work
+ let cur_component = self
+ .pool
+ .get_scope_mut(base_scope)
+ .expect("The base scope should never be moved");
- self.current_lane().dirty_scopes.sort_by(|a, b| {
- let h1 = shared.get_scope(*a).unwrap().height;
- let h2 = shared.get_scope(*b).unwrap().height;
- h1.cmp(&h2)
- });
+ // We run the component. If it succeeds, then we can diff it and add the changes to the dom.
+ if cur_component.run_scope(&self.pool) {
+ diff_machine
+ .stack
+ .create_node(cur_component.frames.fin_head(), MountType::Append);
- if let Some(scope) = self.current_lane().dirty_scopes.pop() {
- let component = self.pool.get_scope(scope).unwrap();
- let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
- machine.stack.push(DiffInstruction::DiffNode { new, old });
- }
- }
+ diff_machine.stack.scope_stack.push(base_scope);
- let deadline_expired = machine.work(deadline_reached);
-
- let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
- let mut saved = machine.save();
-
- if deadline_expired {
- self.save_work(saved);
- false
+ diff_machine.work(|| false);
} else {
- for node in saved.seen_scopes.drain() {
- self.current_lane().dirty_scopes.remove(&node);
- }
-
- let mut new_mutations = Mutations::new();
- std::mem::swap(&mut new_mutations, &mut saved.mutations);
-
- mutations.push(new_mutations);
- self.save_work(saved);
- true
- }
- }
-
- // waits for a trigger, canceling early if the deadline is reached
- // returns true if the deadline was reached
- // does not return the trigger, but caches it in the scheduler
- pub async fn wait_for_any_trigger(
- &mut self,
- deadline: &mut Pin>>,
- ) -> bool {
- use futures_util::future::{select, Either};
-
- let event_fut = async {
- match select(self.receiver.next(), self.async_tasks.next()).await {
- Either::Left((msg, _other)) => {
- self.handle_channel_msg(msg.unwrap());
- }
- Either::Right((task, _other)) => {
- // do nothing, async task will likely generate a set of scheduler messages
- }
- }
- };
-
- pin_mut!(event_fut);
-
- match select(event_fut, deadline).await {
- Either::Left((msg, _other)) => false,
- Either::Right((deadline, _)) => true,
- }
- }
-
- pub fn current_lane(&mut self) -> &mut PriorityLane {
- match self.current_priority {
- EventPriority::Immediate => &mut self.lanes[0],
- EventPriority::High => &mut self.lanes[1],
- EventPriority::Medium => &mut self.lanes[2],
- EventPriority::Low => &mut self.lanes[3],
- }
- }
-
- pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
- match msg {
- SchedulerMsg::Immediate(_) => todo!(),
- SchedulerMsg::UiEvent(_) => todo!(),
-
- //
- SchedulerMsg::SubmitTask(_, _) => todo!(),
- SchedulerMsg::ToggleTask(_) => todo!(),
- SchedulerMsg::PauseTask(_) => todo!(),
- SchedulerMsg::ResumeTask(_) => todo!(),
- SchedulerMsg::DropTask(_) => todo!(),
- }
- }
-
- fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
- todo!()
- // match priority {
- // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
- // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
- // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
- // };
- }
-
- fn collect_garbage(&mut self, id: ElementId) {
- //
- }
-
- pub fn clean_up_garbage(&mut self) {
- let mut scopes_to_kill = Vec::new();
- let mut garbage_list = Vec::new();
-
- for scope in self.garbage_scopes.drain() {
- let scope = self.pool.get_scope_mut(scope).unwrap();
- for node in scope.consume_garbage() {
- garbage_list.push(node);
- }
-
- while let Some(node) = garbage_list.pop() {
- match &node {
- VNode::Text(_) => {
- self.pool.collect_garbage(node.mounted_id());
- }
- VNode::Anchor(_) => {
- self.pool.collect_garbage(node.mounted_id());
- }
- VNode::Suspended(_) => {
- self.pool.collect_garbage(node.mounted_id());
- }
-
- VNode::Element(el) => {
- self.pool.collect_garbage(node.mounted_id());
- for child in el.children {
- garbage_list.push(child);
- }
- }
-
- VNode::Fragment(frag) => {
- for child in frag.children {
- garbage_list.push(child);
- }
- }
-
- VNode::Component(comp) => {
- // TODO: run the hook destructors and then even delete the scope
-
- let scope_id = comp.associated_scope.get().unwrap();
- let scope = self.pool.get_scope(scope_id).unwrap();
- let root = scope.root();
-
- garbage_list.push(root);
- scopes_to_kill.push(scope_id);
- }
- }
- }
+ // todo: should this be a hard error?
+ log::warn!(
+ "Component failed to run succesfully during rebuild.
+ This does not result in a failed rebuild, but indicates a logic failure within your app."
+ );
}
- for scope in scopes_to_kill.drain(..) {
- //
- // kill em
+ unsafe { std::mem::transmute(diff_machine.mutations) }
+ }
+
+ pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations {
+ let cur_component = self
+ .pool
+ .get_scope_mut(base_scope)
+ .expect("The base scope should never be moved");
+
+ if cur_component.run_scope(&self.pool) {
+ let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self.pool);
+ diff_machine.cfg.force_diff = true;
+ diff_machine.diff_scope(base_scope);
+ diff_machine.mutations
+ } else {
+ Mutations::new()
}
}
}
-
-pub(crate) struct PriorityLane {
- pub dirty_scopes: IndexSet,
- pub saved_state: Option>,
- pub in_progress: bool,
-}
-
-impl PriorityLane {
- pub fn new() -> Self {
- Self {
- saved_state: None,
- dirty_scopes: Default::default(),
- in_progress: false,
- }
- }
-
- fn has_work(&self) -> bool {
- todo!()
- }
-
- fn work(&mut self) {
- let scope = self.dirty_scopes.pop();
- }
-}
-
-pub struct TaskHandle {
- pub sender: UnboundedSender,
- pub our_id: u64,
-}
-
-impl TaskHandle {
- /// Toggles this coroutine off/on.
- ///
- /// This method is not synchronous - your task will not stop immediately.
- pub fn toggle(&self) {
- self.sender
- .unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
- .unwrap()
- }
-
- /// This method is not synchronous - your task will not stop immediately.
- pub fn resume(&self) {
- self.sender
- .unbounded_send(SchedulerMsg::ResumeTask(self.our_id))
- .unwrap()
- }
-
- /// This method is not synchronous - your task will not stop immediately.
- pub fn stop(&self) {
- self.sender
- .unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
- .unwrap()
- }
-
- /// This method is not synchronous - your task will not stop immediately.
- pub fn restart(&self) {
- self.sender
- .unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
- .unwrap()
- }
-}
-
-#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
-pub struct ScopeId(pub usize);
-
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
-pub struct ElementId(pub usize);
-impl Display for ElementId {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.0)
- }
-}
-
-impl ElementId {
- pub fn as_u64(self) -> u64 {
- self.0 as u64
- }
-}
-
-/// Priority of Event Triggers.
-///
-/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus
-/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers
-/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
-///
-/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
-///
-/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
-/// we keep it simple, and just use a 3-tier priority system.
-///
-/// - NoPriority = 0
-/// - LowPriority = 1
-/// - NormalPriority = 2
-/// - UserBlocking = 3
-/// - HighPriority = 4
-/// - ImmediatePriority = 5
-///
-/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
-/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
-/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
-pub enum EventPriority {
- /// Work that must be completed during the EventHandler phase.
- ///
- /// Currently this is reserved for controlled inputs.
- Immediate = 3,
-
- /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
- ///
- /// This is typically reserved for things like user interaction.
- ///
- /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
- High = 2,
-
- /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
- /// than "High Priority" events and will take presedence over low priority events.
- ///
- /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
- ///
- /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
- Medium = 1,
-
- /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be
- /// advanced to the front of the work queue until completed.
- ///
- /// The primary user of Low Priority work is the asynchronous work system (suspense).
- ///
- /// This is considered "idle" work or "background" work.
- Low = 0,
-}
-
-#[derive(Clone)]
-pub(crate) struct ResourcePool {
- /*
- This *has* to be an UnsafeCell.
-
- Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to
- its bumpframe conents immutably.
-
- However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly
- simple to audit.
-
- Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!)
- */
- pub components: Rc>>,
-
- /*
- Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy
- that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client.
-
- This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse.
-
- In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes...
- */
- pub raw_elements: Rc>>,
-
- pub channel: EventChannel,
-}
-
-impl ResourcePool {
- /// this is unsafe because the caller needs to track which other scopes it's already using
- pub fn get_scope(&self, idx: ScopeId) -> Option<&Scope> {
- let inner = unsafe { &*self.components.get() };
- inner.get(idx.0)
- }
-
- /// this is unsafe because the caller needs to track which other scopes it's already using
- pub fn get_scope_mut(&self, idx: ScopeId) -> Option<&mut Scope> {
- let inner = unsafe { &mut *self.components.get() };
- inner.get_mut(idx.0)
- }
-
- pub fn with_scope<'b, O: 'static>(
- &'b self,
- _id: ScopeId,
- _f: impl FnOnce(&'b mut Scope) -> O,
- ) -> Option {
- todo!()
- }
-
- // return a bumpframe with a lifetime attached to the arena borrow
- // this is useful for merging lifetimes
- pub fn with_scope_vnode<'b>(
- &self,
- _id: ScopeId,
- _f: impl FnOnce(&mut Scope) -> &VNode<'b>,
- ) -> Option<&VNode<'b>> {
- todo!()
- }
-
- pub fn try_remove(&self, id: ScopeId) -> Option {
- let inner = unsafe { &mut *self.components.get() };
- Some(inner.remove(id.0))
- // .try_remove(id.0)
- // .ok_or_else(|| Error::FatalInternal("Scope not found"))
- }
-
- pub fn reserve_node(&self) -> ElementId {
- let els = unsafe { &mut *self.raw_elements.get() };
- ElementId(els.insert(()))
- }
-
- /// return the id, freeing the space of the original node
- pub fn collect_garbage(&self, id: ElementId) {
- todo!("garabge collection currently WIP")
- // self.raw_elements.remove(id.0);
- }
-
- pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId {
- let g = unsafe { &mut *self.components.get() };
- let entry = g.vacant_entry();
- let id = ScopeId(entry.key());
- entry.insert(f(id));
- id
- }
-
- pub fn borrow_bumpframe(&self) {}
-}
diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs
index 0f5cae2c7..19006042e 100644
--- a/packages/core/src/scope.rs
+++ b/packages/core/src/scope.rs
@@ -1,5 +1,5 @@
use crate::innerlude::*;
-use fxhash::FxHashSet;
+use fxhash::FxHashMap;
use std::{
any::{Any, TypeId},
cell::RefCell,
@@ -23,37 +23,116 @@ pub struct Scope {
pub(crate) parent_idx: Option,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
- pub(crate) descendents: RefCell>,
// Nodes
- // an internal, highly efficient storage of vnodes
- // lots of safety condsiderations
pub(crate) frames: ActiveFrame,
- pub(crate) caller: Rc,
+ pub(crate) caller: *const dyn for<'b> Fn(&'b Scope) -> DomTree<'b>,
pub(crate) child_nodes: ScopeChildren<'static>,
- pub(crate) pending_garbage: RefCell>>,
- // Listeners
+ /*
+ we care about:
+ - listeners (and how to call them when an event is triggered)
+ - borrowed props (and how to drop them when the parent is dropped)
+ - suspended nodes (and how to call their callback when their associated tasks are complete)
+ */
pub(crate) listeners: RefCell>>,
pub(crate) borrowed_props: RefCell>>,
- pub(crate) suspended_nodes: RefCell>>,
+ pub(crate) suspended_nodes: RefCell>>,
// State
pub(crate) hooks: HookList,
pub(crate) shared_contexts: RefCell>>,
- pub(crate) memoized_updater: Rc,
+ // whenever set_state is called, we fire off a message to the scheduler
+ // this closure _is_ the method called by schedule_update that marks this component as dirty
+ pub(crate) memoized_updater: Rc,
pub(crate) shared: EventChannel,
}
-// The type of closure that wraps calling components
-pub type WrappedCaller = dyn for<'b> Fn(&'b Scope) -> DomTree<'b>;
+/// Public interface for Scopes.
+impl Scope {
+ /// Get the root VNode for this Scope.
+ ///
+ /// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment.
+ ///
+ /// # Example
+ /// ```rust
+ /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
+ /// dom.rebuild();
+ ///
+ /// let base = dom.base_scope();
+ ///
+ /// if let VNode::VElement(node) = base.root_node() {
+ /// assert_eq!(node.tag_name, "div");
+ /// }
+ /// ```
+ pub fn root_node(&self) -> &VNode {
+ self.frames.fin_head()
+ }
+ /// Get the height of this Scope - IE the number of scopes above it.
+ ///
+ /// A Scope with a height of `0` is the root scope - there are no other scopes above it.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
+ /// dom.rebuild();
+ ///
+ /// let base = dom.base_scope();
+ ///
+ /// assert_eq!(base.height(), 0);
+ /// ```
+ pub fn height(&self) -> u32 {
+ self.height
+ }
+
+ /// Get the Parent of this Scope within this Dioxus VirtualDOM.
+ ///
+ /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted.
+ ///
+ /// The base component will not have a parent, and will return `None`.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
+ /// dom.rebuild();
+ ///
+ /// let base = dom.base_scope();
+ ///
+ /// assert_eq!(base.parent(), None);
+ /// ```
+ pub fn parent(&self) -> Option {
+ self.parent_idx
+ }
+
+ /// Get the ID of this Scope within this Dioxus VirtualDOM.
+ ///
+ /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
+ /// dom.rebuild();
+ /// let base = dom.base_scope();
+ ///
+ /// assert_eq!(base.scope_id(), 0);
+ /// ```
+ pub fn scope_id(&self) -> ScopeId {
+ self.our_arena_idx
+ }
+}
+
+// The type of closure that wraps calling components
/// The type of task that gets sent to the task scheduler
/// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes
pub type FiberTask = Pin>>;
+/// Private interface for Scopes.
impl Scope {
// we are being created in the scope of an existing component (where the creator_node lifetime comes into play)
// we are going to break this lifetime by force in order to save it on ourselves.
@@ -62,78 +141,57 @@ impl Scope {
//
// Scopes cannot be made anywhere else except for this file
// Therefore, their lifetimes are connected exclusively to the virtual dom
- pub(crate) fn new<'creator_node>(
- caller: Rc,
- arena_idx: ScopeId,
- parent: Option,
+ pub(crate) fn new(
+ caller: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b>,
+ our_arena_idx: ScopeId,
+ parent_idx: Option,
height: u32,
child_nodes: ScopeChildren,
shared: EventChannel,
) -> Self {
let child_nodes = unsafe { child_nodes.extend_lifetime() };
- let up = shared.schedule_any_immediate.clone();
- let memoized_updater = Rc::new(move || up(arena_idx));
+ let schedule_any_update = shared.schedule_any_immediate.clone();
+
+ let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx));
+
+ let caller = caller as *const _;
+
+ // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph
+ let caller = unsafe { std::mem::transmute(caller) };
Self {
memoized_updater,
shared,
child_nodes,
caller,
- parent_idx: parent,
- our_arena_idx: arena_idx,
+ parent_idx,
+ our_arena_idx,
height,
+
frames: ActiveFrame::new(),
hooks: Default::default(),
suspended_nodes: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
borrowed_props: Default::default(),
- descendents: Default::default(),
- pending_garbage: Default::default(),
}
}
pub(crate) fn update_scope_dependencies<'creator_node>(
&mut self,
- caller: Rc,
+ caller: &'creator_node dyn for<'b> Fn(&'b Scope) -> DomTree<'b>,
child_nodes: ScopeChildren,
) {
- self.caller = caller;
+ let caller = caller as *const _;
+ self.caller = unsafe { std::mem::transmute(caller) };
+
let child_nodes = unsafe { child_nodes.extend_lifetime() };
self.child_nodes = child_nodes;
}
- /// Returns true if the scope completed successfully
- ///
- pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
- // Cycle to the next frame and then reset it
- // This breaks any latent references, invalidating every pointer referencing into it.
- // Remove all the outdated listeners
-
- self.ensure_drop_safety(pool);
-
- // Safety:
- // - We dropped the listeners, so no more &mut T can be used while these are held
- // - All children nodes that rely on &mut T are replaced with a new reference
- unsafe { self.hooks.reset() };
-
- // Safety:
- // - We've dropped all references to the wip bump frame
- unsafe { self.frames.reset_wip_frame() };
-
- // Cast the caller ptr from static to one with our own reference
- let render: &WrappedCaller = self.caller.as_ref();
-
- match render(self) {
- None => false,
- Some(new_head) => {
- // the user's component succeeded. We can safely cycle to the next frame
- self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
- self.frames.cycle_frame();
- true
- }
- }
+ pub(crate) fn child_nodes(&self) -> ScopeChildren {
+ unsafe { self.child_nodes.shorten_lifetime() }
}
/// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from
@@ -145,32 +203,30 @@ impl Scope {
///
/// Refrences to hook data can only be stored in listeners and component props. During diffing, we make sure to log
/// all listeners and borrowed props so we can clear them here.
+ ///
+ /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will
+ /// be dropped.
pub(crate) fn ensure_drop_safety(&mut self, pool: &ResourcePool) {
- // make sure all garabge is collected before trying to proceed with anything else
- debug_assert!(
- self.pending_garbage.borrow().is_empty(),
- "clean up your garabge please"
- );
-
- // todo!("arch changes");
-
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Referrence)
// right now, we don't drop
- // let vdom = &self.vdom;
self.borrowed_props
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
.for_each(|comp| {
// First drop the component's undropped references
- let scope_id = comp.associated_scope.get().unwrap();
- let scope = pool.get_scope_mut(scope_id).unwrap();
- scope.ensure_drop_safety(pool);
+ let scope_id = comp
+ .associated_scope
+ .get()
+ .expect("VComponents should be associated with a valid Scope");
- // Now, drop our own reference
- let mut dropper = comp.drop_props.borrow_mut().take().unwrap();
- dropper();
+ if let Some(scope) = pool.get_scope_mut(scope_id) {
+ scope.ensure_drop_safety(pool);
+
+ let mut drop_props = comp.drop_props.borrow_mut().take().unwrap();
+ drop_props();
+ }
});
// Now that all the references are gone, we can safely drop our own references in our listeners.
@@ -178,25 +234,20 @@ impl Scope {
.get_mut()
.drain(..)
.map(|li| unsafe { &*li })
- .for_each(|listener| {
- listener.callback.borrow_mut().take();
- });
+ .for_each(|listener| drop(listener.callback.borrow_mut().take()));
}
- // A safe wrapper around calling listeners
- //
- //
- pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) {
+ /// A safe wrapper around calling listeners
+ pub(crate) fn call_listener(&mut self, event: UserEvent, element: ElementId) {
let listners = self.listeners.borrow_mut();
let raw_listener = listners.iter().find(|lis| {
let search = unsafe { &***lis };
- let search_id = search.mounted_node.get();
-
- // this assumes the node might not be mounted - should we assume that though?
- match search_id.map(|f| f == element) {
- Some(same) => same,
- None => false,
+ if search.event == event.name {
+ let search_id = search.mounted_node.get();
+ search_id.map(|f| f == element).unwrap_or(false)
+ } else {
+ false
}
});
@@ -204,51 +255,179 @@ impl Scope {
let listener = unsafe { &**raw_listener };
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
- (cb)(event);
+ (cb)(event.event);
}
} else {
log::warn!("An event was triggered but there was no listener to handle it");
}
}
- pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren {
- unsafe { self.child_nodes.shorten_lifetime() }
- }
+ /*
+ General strategy here is to load up the appropriate suspended task and then run it.
+ Suspended nodes cannot be called repeatedly.
+ */
+ pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
+ let mut nodes = self.suspended_nodes.borrow_mut();
- pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) {
- let g = self.suspended_nodes.borrow_mut();
-
- if let Some(suspended) = g.get(&task) {
- let sus: &'a VSuspended<'static> = unsafe { &**suspended };
+ if let Some(suspended) = nodes.remove(&task_id) {
+ let sus: &'a VSuspended<'static> = unsafe { &*suspended };
let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
- let bump = self.frames.wip_frame();
- let mut cb = sus.callback.borrow_mut();
- let mut _cb = cb.take().unwrap();
let cx: SuspendedContext<'a> = SuspendedContext {
- inner: Context {
- props: &(),
- scope: &self,
- },
+ inner: Context { scope: self },
};
- let n: DomTree<'a> = (_cb)(cx);
+
+ let mut cb = sus.callback.borrow_mut().take().unwrap();
+
+ let new_node: DomTree<'a> = (cb)(cx);
}
}
- pub(crate) fn consume_garbage(&self) -> Vec<&VNode> {
- self.pending_garbage
- .borrow_mut()
- .drain(..)
- .map(|node| {
- // safety: scopes cannot cycle without their garbage being collected. these nodes are safe
- let node: &VNode<'static> = unsafe { &*node };
- let node: &VNode = unsafe { std::mem::transmute(node) };
- node
- })
- .collect::>()
- }
+ /// Render this component.
+ ///
+ /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated).
+ pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool {
+ // Cycle to the next frame and then reset it
+ // This breaks any latent references, invalidating every pointer referencing into it.
+ // Remove all the outdated listeners
+ self.ensure_drop_safety(pool);
- pub fn root(&self) -> &VNode {
- self.frames.fin_head()
+ // Safety:
+ // - We dropped the listeners, so no more &mut T can be used while these are held
+ // - All children nodes that rely on &mut T are replaced with a new reference
+ unsafe { self.hooks.reset() };
+
+ // Safety:
+ // - We've dropped all references to the wip bump frame
+ unsafe { self.frames.reset_wip_frame() };
+
+ // just forget about our suspended nodes while we're at it
+ self.suspended_nodes.get_mut().clear();
+
+ // guarantee that we haven't screwed up - there should be no latent references anywhere
+ debug_assert!(self.listeners.borrow().is_empty());
+ debug_assert!(self.suspended_nodes.borrow().is_empty());
+ debug_assert!(self.borrowed_props.borrow().is_empty());
+
+ // Cast the caller ptr from static to one with our own reference
+ let render: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b> = unsafe { &*self.caller };
+
+ // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
+ if let Some(new_head) = render(self) {
+ // the user's component succeeded. We can safely cycle to the next frame
+ self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) };
+ self.frames.cycle_frame();
+
+ true
+ } else {
+ false
+ }
+ }
+}
+
+/// render the scope to a string using the rsx! syntax
+pub(crate) struct ScopeRenderer<'a> {
+ pub skip_components: bool,
+ pub show_fragments: bool,
+ pub _scope: &'a Scope,
+ pub _pre_render: bool,
+ pub _newline: bool,
+ pub _indent: bool,
+ pub _max_depth: usize,
+}
+
+// this is more or less a debug tool, but it'll render the entire tree to the terminal
+impl<'a> ScopeRenderer<'a> {
+ pub fn render(
+ &self,
+ vdom: &VirtualDom,
+ node: &VNode,
+ f: &mut std::fmt::Formatter,
+ il: u16,
+ ) -> std::fmt::Result {
+ const INDENT: &str = " ";
+ let write_indent = |_f: &mut std::fmt::Formatter, le| {
+ for _ in 0..le {
+ write!(_f, "{}", INDENT).unwrap();
+ }
+ };
+
+ match &node {
+ VNode::Text(text) => {
+ write_indent(f, il);
+ writeln!(f, "\"{}\"", text.text)?
+ }
+ VNode::Anchor(_anchor) => {
+ write_indent(f, il);
+ writeln!(f, "Anchor {{}}")?;
+ }
+ VNode::Element(el) => {
+ write_indent(f, il);
+ write!(f, "{} {{\n", el.tag_name)?;
+ // write!(f, "element: {}", el.tag_name)?;
+ let mut attr_iter = el.attributes.iter().peekable();
+
+ while let Some(attr) = attr_iter.next() {
+ match attr.namespace {
+ None => {
+ //
+ write_indent(f, il + 1);
+ writeln!(f, "{}: \"{}\"", attr.name, attr.value)?
+ }
+
+ Some(ns) => {
+ // write the opening tag
+ write_indent(f, il + 1);
+ write!(f, " {}:\"", ns)?;
+ let mut cur_ns_el = attr;
+ 'ns_parse: loop {
+ write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?;
+ match attr_iter.peek() {
+ Some(next_attr) if next_attr.namespace == Some(ns) => {
+ cur_ns_el = attr_iter.next().unwrap();
+ }
+ _ => break 'ns_parse,
+ }
+ }
+ // write the closing tag
+ write!(f, "\"")?;
+ }
+ }
+ }
+
+ for child in el.children {
+ self.render(vdom, child, f, il + 1)?;
+ }
+ write_indent(f, il);
+
+ writeln!(f, "}}")?;
+ }
+ VNode::Fragment(frag) => {
+ if self.show_fragments {
+ write_indent(f, il);
+ writeln!(f, "Fragment {{")?;
+ for child in frag.children {
+ self.render(vdom, child, f, il + 1)?;
+ }
+ write_indent(f, il);
+ writeln!(f, "}}")?;
+ } else {
+ for child in frag.children {
+ self.render(vdom, child, f, il)?;
+ }
+ }
+ }
+ VNode::Component(vcomp) => {
+ let idx = vcomp.associated_scope.get().unwrap();
+ if !self.skip_components {
+ let new_node = vdom.get_scope(idx).unwrap().root_node();
+ self.render(vdom, new_node, f, il)?;
+ }
+ }
+ VNode::Suspended { .. } => {
+ // we can't do anything with suspended nodes
+ }
+ }
+ Ok(())
}
}
diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs
new file mode 100644
index 000000000..95540d153
--- /dev/null
+++ b/packages/core/src/tasks.rs
@@ -0,0 +1,39 @@
+use crate::innerlude::*;
+use futures_channel::mpsc::UnboundedSender;
+
+pub struct TaskHandle {
+ pub(crate) sender: UnboundedSender,
+ pub(crate) our_id: u64,
+}
+
+impl TaskHandle {
+ /// Toggles this coroutine off/on.
+ ///
+ /// This method is not synchronous - your task will not stop immediately.
+ pub fn toggle(&self) {
+ self.sender
+ .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
+ .unwrap()
+ }
+
+ /// This method is not synchronous - your task will not stop immediately.
+ pub fn resume(&self) {
+ self.sender
+ .unbounded_send(SchedulerMsg::Task(TaskMsg::ResumeTask(self.our_id)))
+ .unwrap()
+ }
+
+ /// This method is not synchronous - your task will not stop immediately.
+ pub fn stop(&self) {
+ self.sender
+ .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
+ .unwrap()
+ }
+
+ /// This method is not synchronous - your task will not stop immediately.
+ pub fn restart(&self) {
+ self.sender
+ .unbounded_send(SchedulerMsg::Task(TaskMsg::ToggleTask(self.our_id)))
+ .unwrap()
+ }
+}
diff --git a/packages/core/src/test_dom.rs b/packages/core/src/test_dom.rs
index 39b9e5e74..60d2de9b8 100644
--- a/packages/core/src/test_dom.rs
+++ b/packages/core/src/test_dom.rs
@@ -12,25 +12,33 @@ pub struct TestDom {
impl TestDom {
pub fn new() -> TestDom {
let bump = Bump::new();
- let mut scheduler = Scheduler::new();
+ let scheduler = Scheduler::new();
TestDom { bump, scheduler }
}
- pub fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
+ pub fn new_factory(&self) -> NodeFactory {
NodeFactory::new(&self.bump)
}
- pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
+ pub fn render_direct<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
+ pub fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> &'a VNode<'a>
+ where
+ F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
+ {
+ self.bump
+ .alloc(lazy_nodes.into_vnode(NodeFactory::new(&self.bump)))
+ }
+
pub fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mutations = Mutations::new();
let mut machine = DiffMachine::new(mutations, &self.scheduler.pool);
- machine.stack.push(DiffInstruction::DiffNode { new, old });
+ machine.stack.push(DiffInstruction::Diff { new, old });
machine.mutations
}
@@ -38,7 +46,7 @@ impl TestDom {
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
- let old = self.bump.alloc(self.render(left));
+ let old = self.bump.alloc(self.render_direct(left));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
@@ -58,20 +66,18 @@ impl TestDom {
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
- let old = self.bump.alloc(self.render(left));
-
- let new = self.bump.alloc(self.render(right));
+ let (old, new) = (self.render(left), self.render(right));
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
machine.stack.create_node(old, MountType::Append);
- machine.work(&mut || false);
+ machine.work(|| false);
let create_edits = machine.mutations;
let mut machine = DiffMachine::new(Mutations::new(), &self.scheduler.pool);
- machine.stack.push(DiffInstruction::DiffNode { old, new });
+ machine.stack.push(DiffInstruction::Diff { old, new });
machine.work(&mut || false);
@@ -80,3 +86,15 @@ impl TestDom {
(create_edits, edits)
}
}
+
+impl Default for TestDom {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl VirtualDom {
+ pub fn simulate(&mut self) {
+ //
+ }
+}
diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs
index d2df10eab..63962d792 100644
--- a/packages/core/src/util.rs
+++ b/packages/core/src/util.rs
@@ -1,6 +1,5 @@
use std::cell::Cell;
-
-use crate::innerlude::*;
+use std::fmt::Display;
// create a cell with a "none" value
#[inline]
@@ -64,3 +63,28 @@ impl Future for YieldNow {
}
}
}
+
+/// A component's unique identifier.
+///
+/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
+/// unmounted, then the `ScopeId` will be reused for a new component.
+#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
+pub struct ScopeId(pub usize);
+
+/// An Element's unique identifier.
+///
+/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
+/// unmounted, then the `ElementId` will be reused for a new component.
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+pub struct ElementId(pub usize);
+impl Display for ElementId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl ElementId {
+ pub fn as_u64(self) -> u64 {
+ self.0 as u64
+ }
+}
diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs
index b43907ea2..1c732d862 100644
--- a/packages/core/src/virtual_dom.rs
+++ b/packages/core/src/virtual_dom.rs
@@ -1,4 +1,5 @@
//! # VirtualDOM Implementation for Rust
+//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
//!
//! In this file, multiple items are defined. This file is big, but should be documented well to
@@ -19,22 +20,39 @@
//! Additional functionality is defined in the respective files.
use crate::innerlude::*;
-use futures_util::{Future, FutureExt};
-use std::{
- any::{Any, TypeId},
- pin::Pin,
- rc::Rc,
-};
+use futures_util::Future;
+use std::any::Any;
/// An integrated virtual node system that progresses events and diffs UI trees.
+///
/// Differences are converted into patches which a renderer can use to draw the UI.
///
+/// If you are building an App with Dioxus, you probably won't want to reach for this directly, instead opting to defer
+/// to a particular crate's wrapper over the [`VirtualDom`] API.
///
+/// Example
+/// ```rust
+/// static App: FC<()> = |cx, props|{
+/// cx.render(rsx!{
+/// div {
+/// "Hello World"
+/// }
+/// })
+/// }
///
+/// async fn main() {
+/// let mut dom = VirtualDom::new(App);
+/// let mut inital_edits = dom.rebuild();
+/// initialize_screen(inital_edits);
///
-///
-///
-///
+/// loop {
+/// let next_frame = TimeoutFuture::new(Duration::from_millis(16));
+/// let edits = dom.run_with_deadline(next_frame).await;
+/// apply_edits(edits);
+/// render_frame();
+/// }
+/// }
+/// ```
pub struct VirtualDom {
scheduler: Scheduler,
@@ -42,7 +60,10 @@ pub struct VirtualDom {
root_fc: Box,
- root_props: Pin>,
+ root_props: Box,
+
+ // we need to keep the allocation around, but we don't necessarily use it
+ _root_caller: Box Fn(&'b Scope) -> DomTree<'b> + 'static>,
}
impl VirtualDom {
@@ -101,15 +122,23 @@ impl VirtualDom {
/// let mutations = dom.rebuild();
/// ```
pub fn new_with_props(root: FC, root_props: P) -> Self {
- let scheduler = Scheduler::new();
+ let root_fc = Box::new(root);
+
+ let root_props: Box = Box::new(root_props);
- let root_props: Pin> = Box::pin(root_props);
let props_ptr = root_props.downcast_ref::().unwrap() as *const P;
+ // Safety: this callback is only valid for the lifetime of the root props
+ let root_caller: Box DomTree> = Box::new(move |scope: &Scope| unsafe {
+ let props: &'_ P = &*(props_ptr as *const P);
+ std::mem::transmute(root(Context { scope }, props))
+ });
+
+ let scheduler = Scheduler::new();
+
let base_scope = scheduler.pool.insert_scope_with_key(|myidx| {
- let caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
Scope::new(
- caller,
+ root_caller.as_ref(),
myidx,
None,
0,
@@ -119,7 +148,8 @@ impl VirtualDom {
});
Self {
- root_fc: Box::new(root),
+ _root_caller: root_caller,
+ root_fc,
base_scope,
scheduler,
root_props,
@@ -151,26 +181,38 @@ impl VirtualDom {
/// struct AppProps {
/// route: &'static str
/// }
- /// static App: FC = |cx| cx.render(rsx!{ "route is {cx.route}" });
+ /// static App: FC = |cx, props|cx.render(rsx!{ "route is {cx.route}" });
///
/// let mut dom = VirtualDom::new_with_props(App, AppProps { route: "start" });
///
/// let mutations = dom.update_root_props(AppProps { route: "end" }).unwrap();
/// ```
- pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option> {
+ pub fn update_root_props(&mut self, root_props: P) -> Option
+ where
+ P: 'static,
+ {
let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap();
+
+ // Pre-emptively drop any downstream references of the old props
root_scope.ensure_drop_safety(&self.scheduler.pool);
- let mut root_props: Pin> = Box::pin(root_props);
+ let mut root_props: Box = Box::new(root_props);
if let Some(props_ptr) = root_props.downcast_ref::().map(|p| p as *const P) {
+ // Swap the old props and new props
std::mem::swap(&mut self.root_props, &mut root_props);
let root = *self.root_fc.downcast_ref::>().unwrap();
- let new_caller = NodeFactory::create_component_caller(root, props_ptr as *const _);
+ let root_caller: Box DomTree> =
+ Box::new(move |scope: &Scope| unsafe {
+ let props: &'_ P = &*(props_ptr as *const P);
+ std::mem::transmute(root(Context { scope }, props))
+ });
- root_scope.update_scope_dependencies(new_caller, ScopeChildren(&[]));
+ root_scope.update_scope_dependencies(&root_caller, ScopeChildren(&[]));
+
+ drop(root_props);
Some(self.rebuild())
} else {
@@ -178,81 +220,70 @@ impl VirtualDom {
}
}
- /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
+ /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch
///
- /// The diff machine expects the RealDom's stack to be the root of the application
+ /// The diff machine expects the RealDom's stack to be the root of the application.
///
- /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed
- /// through "run". We completely avoid the task scheduler infrastructure.
- pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> {
- let mut fut = self.rebuild_async().boxed_local();
-
- loop {
- if let Some(edits) = (&mut fut).now_or_never() {
- break edits;
- }
- }
- }
-
- /// Rebuild the dom from the ground up
+ /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
+ /// root component will be ran once and then diffed. All updates will flow out as mutations.
///
- /// This method is asynchronous to prevent the application from blocking while the dom is being rebuilt. Computing
- /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This
- /// method can be useful when needing to perform some crucial periodic tasks.
- pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> {
- let mut shared = self.scheduler.pool.clone();
- let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
-
- let cur_component = self
- .scheduler
- .pool
- .get_scope_mut(self.base_scope)
- .expect("The base scope should never be moved");
-
- // We run the component. If it succeeds, then we can diff it and add the changes to the dom.
- if cur_component.run_scope(&self.scheduler.pool) {
- diff_machine
- .stack
- .create_node(cur_component.frames.fin_head(), MountType::Append);
- diff_machine.stack.scope_stack.push(self.base_scope);
-
- // let completed = diff_machine.work();
- } else {
- // todo: should this be a hard error?
- log::warn!(
- "Component failed to run succesfully during rebuild.
- This does not result in a failed rebuild, but indicates a logic failure within your app."
- );
- }
-
- unsafe { std::mem::transmute(diff_machine.mutations) }
+ /// All state stored in components will be completely wiped away.
+ ///
+ /// # Example
+ /// ```
+ /// static App: FC<()> = |cx, props|cx.render(rsx!{ "hello world" });
+ /// let mut dom = VirtualDom::new();
+ /// let edits = dom.rebuild();
+ ///
+ /// apply_edits(edits);
+ /// ```
+ pub fn rebuild(&mut self) -> Mutations {
+ self.scheduler.rebuild(self.base_scope)
}
/// Compute a manual diff of the VirtualDOM between states.
///
/// This can be useful when state inside the DOM is remotely changed from the outside, but not propogated as an event.
- pub fn diff<'s>(&'s mut self) -> Mutations<'s> {
- let cur_component = self
- .scheduler
- .pool
- .get_scope_mut(self.base_scope)
- .expect("The base scope should never be moved");
-
- if cur_component.run_scope(&self.scheduler.pool) {
- let mut diff_machine: DiffMachine<'s> =
- DiffMachine::new(Mutations::new(), &mut self.scheduler.pool);
- diff_machine.diff_scope(self.base_scope);
- diff_machine.mutations
- } else {
- Mutations::new()
- }
+ ///
+ /// In this case, every component will be diffed, even if their props are memoized. This method is intended to be used
+ /// to force an update of the DOM when the state of the app is changed outside of the app.
+ ///
+ ///
+ /// # Example
+ /// ```rust
+ /// #[derive(PartialEq, Props)]
+ /// struct AppProps {
+ /// value: Shared<&'static str>,
+ /// }
+ ///
+ /// static App: FC = |cx, props|{
+ /// let val = cx.value.borrow();
+ /// cx.render(rsx! { div { "{val}" } })
+ /// };
+ ///
+ /// let value = Rc::new(RefCell::new("Hello"));
+ /// let mut dom = VirtualDom::new_with_props(
+ /// App,
+ /// AppProps {
+ /// value: value.clone(),
+ /// },
+ /// );
+ ///
+ /// let _ = dom.rebuild();
+ ///
+ /// *value.borrow_mut() = "goodbye";
+ ///
+ /// let edits = dom.diff();
+ /// ```
+ pub fn diff(&mut self) -> Mutations {
+ self.scheduler.hard_diff(self.base_scope)
}
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
///
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
- pub fn run_immediate<'s>(&'s mut self) -> Option>> {
+ pub fn run_immediate(&mut self) -> Option> {
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
} else {
@@ -287,7 +318,7 @@ impl VirtualDom {
/// # Example
///
/// ```no_run
- /// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} );
+ /// static App: FC<()> = |cx, props|rsx!(cx, div {"hello"} );
/// let mut dom = VirtualDom::new(App);
/// loop {
/// let deadline = TimeoutFuture::from_ms(16);
@@ -303,29 +334,76 @@ impl VirtualDom {
/// applied the edits.
///
/// Mutations are the only link between the RealDOM and the VirtualDOM.
- pub async fn run_with_deadline<'s>(
- &'s mut self,
- deadline: impl Future,
- ) -> Vec> {
- let mut deadline = Box::pin(deadline.fuse());
- self.scheduler.work_with_deadline(deadline).await
+ pub fn run_with_deadline(&mut self, deadline: impl FnMut() -> bool) -> Vec> {
+ self.scheduler.work_with_deadline(deadline)
}
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender {
self.scheduler.pool.channel.sender.clone()
}
- pub fn has_work(&self) -> bool {
- true
- }
+ /// Waits for the scheduler to have work
+ /// This lets us poll async tasks during idle periods without blocking the main thread.
+ pub async fn wait_for_work(&mut self) {
+ if self.scheduler.has_any_work() {
+ log::debug!("No need to wait for work, we already have some");
+ return;
+ }
- pub async fn wait_for_any_work(&mut self) {
- let mut timeout = Box::pin(futures_util::future::pending().fuse());
- self.scheduler.wait_for_any_trigger(&mut timeout).await;
+ log::debug!("No active work.... waiting for some...");
+ use futures_util::StreamExt;
+
+ // right now this won't poll events if there is ongoing work
+ // in the future we want to prioritize some events over ongoing work
+ // this is coming in the "priorities" PR
+
+ // Wait for any new events if we have nothing to do
+ // todo: poll the events once even if there is work to do to prevent starvation
+ futures_util::select! {
+ _ = self.scheduler.async_tasks.next() => {}
+ msg = self.scheduler.receiver.next() => {
+ match msg.unwrap() {
+ SchedulerMsg::Task(t) => todo!(),
+ SchedulerMsg::Immediate(im) => {
+ self.scheduler.dirty_scopes.insert(im);
+ }
+ SchedulerMsg::UiEvent(evt) => {
+ self.scheduler.ui_events.push_back(evt);
+ }
+ }
+ },
+ }
+
+ while let Ok(Some(msg)) = self.scheduler.receiver.try_next() {
+ match msg {
+ SchedulerMsg::Task(t) => todo!(),
+ SchedulerMsg::Immediate(im) => {
+ self.scheduler.dirty_scopes.insert(im);
+ }
+ SchedulerMsg::UiEvent(evt) => {
+ self.scheduler.ui_events.push_back(evt);
+ }
+ }
+ }
}
}
-// TODO!
-// These impls are actually wrong. The DOM needs to have a mutex implemented.
-unsafe impl Sync for VirtualDom {}
-unsafe impl Send for VirtualDom {}
+impl std::fmt::Display for VirtualDom {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let base = self.base_scope();
+ let root = base.root_node();
+
+ let renderer = ScopeRenderer {
+ show_fragments: false,
+ skip_components: false,
+
+ _scope: base,
+ _pre_render: false,
+ _newline: true,
+ _indent: true,
+ _max_depth: usize::MAX,
+ };
+
+ renderer.render(self, root, f, 0)
+ }
+}
diff --git a/packages/core/tests/README.md b/packages/core/tests/README.md
new file mode 100644
index 000000000..63293c741
--- /dev/null
+++ b/packages/core/tests/README.md
@@ -0,0 +1,53 @@
+# Testing of Dioxus core
+
+
+NodeFactory
+- [] rsx, html, NodeFactory generate the same structures
+
+Diffing
+- [x] create elements
+- [x] create text
+- [x] create fragments
+- [x] create empty fragments (placeholders)
+- [x] diff elements
+- [x] diff from element/text to fragment
+- [x] diff from element/text to empty fragment
+- [x] diff to element with children works too
+- [x] replace with works forward
+- [x] replace with works backward
+- [x] un-keyed diffing
+- [x] keyed diffing
+- [x] keyed diffing out of order
+- [x] keyed diffing with prefix/suffix
+- [x] suspended nodes work
+
+Lifecycle
+- [] Components mount properly
+- [] Components create new child components
+- [] Replaced components unmount old components and mount new
+- [] Post-render effects are called
+- []
+
+
+Shared Context
+- [] Shared context propagates downwards
+- [] unwrapping shared context if it doesn't exist works too
+
+Suspense
+- [] use_suspense generates suspended nodes
+
+
+Hooks
+- [] Drop order is maintained
+- [] Shared hook state is okay
+- [] use_hook works
+- [] use_ref works
+- [] use_noderef works
+- [] use_provide_state
+- [] use_consume_state
+
+
+VirtualDOM API
+- [] work
+- [] rebuild
+- [] change props
diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs
new file mode 100644
index 000000000..02fdd51c5
--- /dev/null
+++ b/packages/core/tests/borrowedstate.rs
@@ -0,0 +1,44 @@
+use dioxus::{nodes::VSuspended, prelude::*, DomEdit, TestDom};
+use dioxus_core as dioxus;
+use dioxus_html as dioxus_elements;
+
+static Parent: FC<()> = |cx, props| {
+ let value = cx.use_hook(|_| String::new(), |f| &*f, |_| {});
+
+ cx.render(rsx! {
+ div {
+ Child { name: value }
+ Child { name: value }
+ Child { name: value }
+ Child { name: value }
+ }
+ })
+};
+
+#[derive(Props)]
+struct ChildProps<'a> {
+ name: &'a String,
+}
+
+fn Child<'a>(cx: Context<'a>, props: &'a ChildProps) -> DomTree<'a> {
+ cx.render(rsx! {
+ div {
+ h1 { "it's nested" }
+ Child2 { name: props.name }
+ }
+ })
+}
+
+#[derive(Props)]
+struct Grandchild<'a> {
+ name: &'a String,
+}
+
+fn Child2<'a>(cx: Context<'a>, props: &Grandchild) -> DomTree<'a> {
+ cx.render(rsx! {
+ div { "Hello {props.name}!" }
+ })
+}
+
+#[test]
+fn test_borrowed_state() {}
diff --git a/packages/core/tests/channels.rs b/packages/core/tests/channels.rs
deleted file mode 100644
index 2eb64e5b1..000000000
--- a/packages/core/tests/channels.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use futures_channel::mpsc::unbounded;
-
-#[async_std::test]
-async fn channels() {
- let (sender, mut receiver) = unbounded::();
-
- // drop(sender);
-
- match receiver.try_next() {
- Ok(a) => {
- dbg!(a);
- }
- Err(no) => {
- dbg!(no);
- }
- }
-
- sender.unbounded_send(1).unwrap();
-}
diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_dom.rs
similarity index 75%
rename from packages/core/tests/create_iterative.rs
rename to packages/core/tests/create_dom.rs
index d95b3b0dd..b1b6639b8 100644
--- a/packages/core/tests/create_iterative.rs
+++ b/packages/core/tests/create_dom.rs
@@ -1,17 +1,24 @@
-//! tests to prove that the iterative implementation works
+//! Prove that the dom works normally through virtualdom methods.
+//!
+//! This methods all use "rebuild" which completely bypasses the scheduler.
+//! Hard rebuilds don't consume any events from the event queue.
-use anyhow::{Context, Result};
-use dioxus::{prelude::*, DomEdit, Mutations};
-mod test_logging;
+use dioxus::{prelude::*, DomEdit};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
+
+mod test_logging;
use DomEdit::*;
-const LOGGING_ENABLED: bool = false;
+fn new_dom |