瀏覽代碼

chore: more refactoring, docs

Jonathan Kelley 2 年之前
父節點
當前提交
c44bd11fe5

+ 0 - 13
packages/core/src/any_props.rs

@@ -1,7 +1,5 @@
 use std::marker::PhantomData;
 
-use std::future::Future;
-
 use crate::{
     factory::{ComponentReturn, RenderReturn},
     innerlude::Scoped,
@@ -25,17 +23,6 @@ where
     pub _marker: PhantomData<A>,
 }
 
-impl<'a> VComponentProps<'a, (), ()> {
-    pub fn new_empty(render_fn: fn(Scope) -> Element) -> Self {
-        Self {
-            render_fn,
-            memo: <() as PartialEq>::eq,
-            props: (),
-            _marker: PhantomData,
-        }
-    }
-}
-
 impl<'a, P, A, F: ComponentReturn<'a, A>> VComponentProps<'a, P, A, F> {
     pub(crate) fn new(
         render_fn: fn(Scope<'a, P>) -> F,

+ 9 - 0
packages/core/src/arena.rs

@@ -8,6 +8,15 @@ pub struct ElementPath {
     pub element: usize,
 }
 
+impl ElementPath {
+    pub fn null() -> Self {
+        Self {
+            template: std::ptr::null_mut(),
+            element: 0,
+        }
+    }
+}
+
 impl VirtualDom {
     pub fn next_element(&mut self, template: &VNode) -> ElementId {
         let entry = self.elements.vacant_entry();

+ 4 - 0
packages/core/src/bump_frame.rs

@@ -21,4 +21,8 @@ impl BumpFrame {
         self.bump.reset();
         self.node.set(std::ptr::null_mut());
     }
+
+    pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> {
+        unsafe { std::mem::transmute(&*self.node.get()) }
+    }
 }

+ 0 - 11
packages/core/src/component.rs

@@ -1,11 +0,0 @@
-// pub trait IntoComponentType<T> {
-//     fn into_component_type(self) -> ComponentType;
-// }
-
-use std::marker::PhantomData;
-
-use std::future::Future;
-
-use crate::{scopes::Scope, Element};
-
-pub type Component<'a, T = ()> = fn(Scope<'a, T>) -> Element<'a>;

+ 5 - 2
packages/core/src/create.rs

@@ -130,7 +130,10 @@ impl VirtualDom {
     ) {
         match *node {
             // Todo: create the children's template
-            TemplateNode::Dynamic(_) => mutations.push(CreatePlaceholder { id: ElementId(0) }),
+            TemplateNode::Dynamic(_) => {
+                let id = self.next_element(template);
+                mutations.push(CreatePlaceholder { id })
+            }
             TemplateNode::Text(value) => mutations.push(CreateText { value }),
             TemplateNode::DynamicText { .. } => mutations.push(CreateText {
                 value: "placeholder",
@@ -220,7 +223,7 @@ impl VirtualDom {
 
                                 let split_off = unsafe { std::mem::transmute(split_off) };
 
-                                boundary_mut.mutations.mutations = split_off;
+                                boundary_mut.mutations.edits = split_off;
                                 boundary_mut
                                     .waiting_on
                                     .extend(self.collected_leaves.drain(..));

+ 57 - 1
packages/core/src/diff.rs

@@ -1,12 +1,12 @@
 use std::any::Any;
 
+use crate::factory::RenderReturn;
 use crate::innerlude::Mutations;
 use crate::virtual_dom::VirtualDom;
 use crate::{Attribute, AttributeValue, TemplateNode};
 
 use crate::any_props::VComponentProps;
 
-use crate::component::Component;
 use crate::mutations::Mutation;
 use crate::nodes::{DynamicNode, Template, TemplateId};
 use crate::scopes::Scope;
@@ -41,6 +41,62 @@ impl Ord for DirtyScope {
 impl<'b> VirtualDom {
     pub fn diff_scope(&mut self, mutations: &mut Mutations<'b>, scope: ScopeId) {
         let scope_state = &mut self.scopes[scope.0];
+
+        let cur_arena = scope_state.current_frame();
+        let prev_arena = scope_state.previous_frame();
+
+        // relax the borrow checker
+        let cur_arena: &BumpFrame = unsafe { std::mem::transmute(cur_arena) };
+        let prev_arena: &BumpFrame = unsafe { std::mem::transmute(prev_arena) };
+
+        // Make sure the nodes arent null (they've been set properly)
+        assert_ne!(
+            cur_arena.node.get(),
+            std::ptr::null_mut(),
+            "Call rebuild before diffing"
+        );
+        assert_ne!(
+            prev_arena.node.get(),
+            std::ptr::null_mut(),
+            "Call rebuild before diffing"
+        );
+
+        self.scope_stack.push(scope);
+        let left = unsafe { prev_arena.load_node() };
+        let right = unsafe { cur_arena.load_node() };
+        self.diff_maybe_node(mutations, left, right);
+        self.scope_stack.pop();
+    }
+
+    fn diff_maybe_node(
+        &mut self,
+        m: &mut Mutations<'b>,
+        left: &'b RenderReturn<'b>,
+        right: &'b RenderReturn<'b>,
+    ) {
+        use RenderReturn::{Async, Sync};
+        match (left, right) {
+            // diff
+            (Sync(Some(l)), Sync(Some(r))) => self.diff_node(m, l, r),
+
+            // remove old with placeholder
+            (Sync(Some(l)), Sync(None)) | (Sync(Some(l)), Async(_)) => {
+                //
+                let id = self.next_element(l); // todo!
+                m.push(Mutation::CreatePlaceholder { id });
+                self.drop_template(m, l, true);
+            }
+
+            // remove placeholder with nodes
+            (Sync(None), Sync(Some(_))) => {}
+            (Async(_), Sync(Some(v))) => {}
+
+            // nothing...
+            (Async(_), Async(_))
+            | (Sync(None), Sync(None))
+            | (Sync(None), Async(_))
+            | (Async(_), Sync(None)) => {}
+        }
     }
 
     pub fn diff_node(

+ 1 - 45
packages/core/src/events.rs

@@ -98,53 +98,9 @@ pub enum EventPriority {
     Low = 0,
 }
 
-impl VirtualDom {
-    /// Returns None if no element could be found
-    pub fn handle_event<T: 'static>(&mut self, event: &UiEvent<T>) -> Option<()> {
-        let path = self.elements.get(event.element.0)?;
-
-        let location = unsafe { &mut *path.template }
-            .dynamic_attrs
-            .iter()
-            .position(|attr| attr.mounted_element.get() == event.element)?;
-
-        let mut index = Some((path.template, location));
-
-        let mut listeners = Vec::<&Attribute>::new();
-
-        while let Some((raw_parent, dyn_index)) = index {
-            let parent = unsafe { &mut *raw_parent };
-            let path = parent.template.node_paths[dyn_index];
-
-            listeners.extend(
-                parent
-                    .dynamic_attrs
-                    .iter()
-                    .enumerate()
-                    .filter_map(|(idx, attr)| {
-                        match is_path_ascendant(parent.template.node_paths[idx], path) {
-                            true if attr.name == event.name => Some(attr),
-                            _ => None,
-                        }
-                    }),
-            );
-
-            index = parent.parent;
-        }
-
-        for listener in listeners {
-            if let AttributeValue::Listener(listener) = &listener.value {
-                (listener.borrow_mut())(&event.event)
-            }
-        }
-
-        Some(())
-    }
-}
-
 // ensures a strict descendant relationship
 // returns false if the paths are equal
-fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
+pub fn is_path_ascendant(small: &[u8], big: &[u8]) -> bool {
     if small.len() >= big.len() {
         return false;
     }

+ 7 - 10
packages/core/src/factory.rs

@@ -1,4 +1,4 @@
-use std::{cell::Cell, fmt::Arguments, pin::Pin};
+use std::{cell::Cell, fmt::Arguments};
 
 use bumpalo::boxed::Box as BumpBox;
 use bumpalo::Bump;
@@ -123,13 +123,6 @@ where
     }
 }
 
-#[test]
-fn takes_it() {
-    fn demo(cx: Scope) -> Element {
-        todo!()
-    }
-}
-
 pub enum RenderReturn<'a> {
     Sync(Element<'a>),
     Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
@@ -144,8 +137,6 @@ impl<'a> RenderReturn<'a> {
     }
 }
 
-pub type FiberLeaf<'a> = Pin<BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>>;
-
 pub trait IntoVnode<'a, A = ()> {
     fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a>;
 }
@@ -162,6 +153,12 @@ impl<'a, 'b> IntoVnode<'a> for VNode<'a> {
     }
 }
 
+impl<'a, 'b> IntoVnode<'a> for Option<VNode<'a>> {
+    fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> {
+        self.expect("please allow optional nodes in")
+    }
+}
+
 impl<'a, 'b> IntoVnode<'a> for &'a VNode<'a> {
     fn into_dynamic_node(self, _cx: &'a ScopeState) -> VNode<'a> {
         VNode {

+ 13 - 7
packages/core/src/garbage.rs

@@ -1,16 +1,22 @@
-use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode};
+use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations};
 
-impl VirtualDom {
+impl<'b> VirtualDom {
     pub fn drop_scope(&mut self, id: ScopeId) {
-        let scope = self.scopes.get(id.0).unwrap();
+        // let scope = self.scopes.get(id.0).unwrap();
 
-        let root = scope.root_node();
-        let root = unsafe { std::mem::transmute(root) };
+        // let root = scope.root_node();
+        // let root = unsafe { std::mem::transmute(root) };
 
-        self.drop_template(root);
+        // self.drop_template(root, false);
+        todo!()
     }
 
-    pub fn drop_template<'a>(&'a mut self, template: &'a VNode<'a>) {
+    pub fn drop_template(
+        &mut self,
+        mutations: &mut Mutations,
+        template: &'b VNode<'b>,
+        gen_roots: bool,
+    ) {
         for node in template.dynamic_nodes.iter() {
             match node {
                 DynamicNode::Text { id, .. } => {}

+ 6 - 25
packages/core/src/lib.rs

@@ -1,7 +1,6 @@
 mod any_props;
 mod arena;
 mod bump_frame;
-mod component;
 mod create;
 mod diff;
 mod events;
@@ -77,7 +76,10 @@ pub use crate::innerlude::{
     ElementId,
     ElementPath,
     EventPriority,
+    Fragment,
     LazyNodes,
+    Mutation,
+    Mutations,
     NodeFactory,
     Properties,
     Scope,
@@ -100,9 +102,9 @@ pub use crate::innerlude::{
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 pub mod prelude {
     pub use crate::innerlude::{
-        fc_to_builder, Attribute, DynamicNode, Element, EventPriority, LazyNodes, NodeFactory,
-        Properties, Scope, ScopeId, ScopeState, Scoped, SuspenseBoundary, SuspenseContext, TaskId,
-        Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom,
+        fc_to_builder, Element, EventPriority, Fragment, LazyNodes, NodeFactory, Properties, Scope,
+        ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent,
+        VNode, VirtualDom,
     };
 }
 
@@ -135,24 +137,3 @@ macro_rules! to_owned {
         let mut $es = $es.to_owned();
     )*}
 }
-
-/// A helper macro for values into callbacks for async environements.
-///
-///
-macro_rules! callback {
-    () => {};
-}
-
-/// Convert a hook into a hook with an implicit dependency list by analyzing the closure.
-///
-/// ```
-/// // Convert hooks with annoying dependencies into...
-///
-/// let val = use_effect(cx, (val,) |(val,)| println!("thing {val}"))
-///
-/// // a simple closure
-/// let val = use_effect!(cx, |val| async { println!("thing {val}")) });
-/// ```
-macro_rules! make_dep_fn {
-    () => {};
-}

+ 5 - 5
packages/core/src/mutations.rs

@@ -4,14 +4,14 @@ use crate::arena::ElementId;
 pub struct Mutations<'a> {
     pub subtree: usize,
     pub template_mutations: Vec<Mutation<'a>>,
-    pub mutations: Vec<Mutation<'a>>,
+    pub edits: Vec<Mutation<'a>>,
 }
 
 impl<'a> Mutations<'a> {
     pub fn new(subtree: usize) -> Self {
         Self {
             subtree,
-            mutations: Vec::new(),
+            edits: Vec::new(),
             template_mutations: Vec::new(),
         }
     }
@@ -21,13 +21,13 @@ impl<'a> std::ops::Deref for Mutations<'a> {
     type Target = Vec<Mutation<'a>>;
 
     fn deref(&self) -> &Self::Target {
-        &self.mutations
+        &self.edits
     }
 }
 
 impl std::ops::DerefMut for Mutations<'_> {
     fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.mutations
+        &mut self.edits
     }
 }
 
@@ -35,7 +35,7 @@ impl std::ops::DerefMut for Mutations<'_> {
 each subtree has its own numbering scheme
 */
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq)]
 pub enum Mutation<'a> {
     SetAttribute {
         name: &'a str,

+ 31 - 30
packages/core/src/properties.rs

@@ -64,36 +64,37 @@ impl<'a> Properties for FragmentProps<'a> {
     }
 }
 
-// /// Create inline fragments using Component syntax.
-// ///
-// /// ## Details
-// ///
-// /// Fragments capture a series of children without rendering extra nodes.
-// ///
-// /// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
-// /// a key is needed to identify each item.
-// ///
-// /// ## Example
-// ///
-// /// ```rust, ignore
-// /// rsx!{
-// ///     Fragment { key: "abc" }
-// /// }
-// /// ```
-// ///
-// /// ## Usage
-// ///
-// /// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
-// /// Try to avoid highly nested fragments if you can. Unlike React, 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: Scope<'a, FragmentProps<'a>>) -> Element {
-//     let i = cx.props.0.as_ref().map(|f| f.decouple());
-//     cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
-// }
+/// Create inline fragments using Component syntax.
+///
+/// ## Details
+///
+/// Fragments capture a series of children without rendering extra nodes.
+///
+/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and
+/// a key is needed to identify each item.
+///
+/// ## Example
+///
+/// ```rust, ignore
+/// rsx!{
+///     Fragment { key: "abc" }
+/// }
+/// ```
+///
+/// ## Usage
+///
+/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase.
+/// Try to avoid highly nested fragments if you can. Unlike React, 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: Scope<'a, FragmentProps<'a>>) -> Element {
+    // let i = cx.props.0.as_ref().map(|f| f.decouple());
+    // cx.render(LazyNodes::new(|f| f.fragment_from_iter(i)))
+    todo!("Fragment")
+}
 
 /// 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

+ 1 - 7
packages/core/src/scheduler/mod.rs

@@ -14,16 +14,10 @@ pub use waker::RcWake;
 ///
 /// These messages control how the scheduler will process updates to the UI.
 #[derive(Debug)]
-pub enum SchedulerMsg {
-    /// Events from the Renderer
-    Event,
-
+pub(crate) enum SchedulerMsg {
     /// Immediate updates from Components that mark them as dirty
     Immediate(ScopeId),
 
-    /// Mark all components as dirty and update them
-    DirtyAll,
-
     /// A task has woken and needs to be progressed
     TaskNotified(TaskId),
 

+ 1 - 2
packages/core/src/scheduler/suspense.rs

@@ -29,12 +29,11 @@ impl SuspenseBoundary {
     }
 }
 
-pub struct SuspenseLeaf {
+pub(crate) struct SuspenseLeaf {
     pub id: SuspenseId,
     pub scope_id: ScopeId,
     pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
     pub notified: Cell<bool>,
-
     pub task: *mut dyn Future<Output = Element<'static>>,
 }
 

+ 0 - 5
packages/core/src/scheduler/task.rs

@@ -57,11 +57,6 @@ impl Scheduler {
     pub fn remove(&self, id: TaskId) {
         //
     }
-
-    // Aborts the future
-    pub fn abort(&self, id: TaskId) {
-        //
-    }
 }
 
 impl RcWake for LocalTask {

+ 78 - 96
packages/core/src/scheduler/wait.rs

@@ -1,110 +1,92 @@
-use futures_util::{FutureExt, StreamExt};
+use futures_util::FutureExt;
 use std::task::{Context, Poll};
 
 use crate::{
-    diff::DirtyScope,
     factory::RenderReturn,
     innerlude::{Mutation, Mutations, SuspenseContext},
-    VNode, VirtualDom,
+    ScopeId, TaskId, VNode, VirtualDom,
 };
 
-use super::{waker::RcWake, SchedulerMsg, SuspenseLeaf};
+use super::{waker::RcWake, SuspenseId, SuspenseLeaf};
 
 impl VirtualDom {
-    /// Wait for futures internal to the virtualdom
+    /// Handle notifications by tasks inside the scheduler
     ///
-    /// This is cancel safe, so if the future is dropped, you can push events into the virtualdom
-    pub async fn wait_for_work(&mut self) {
-        // todo: make sure the scheduler queue is completely drained
-        loop {
-            match self.rx.next().await.unwrap() {
-                SchedulerMsg::Event => break,
-
-                SchedulerMsg::Immediate(id) => {
-                    let height = self.scopes[id.0].height;
-                    self.dirty_scopes.insert(DirtyScope { height, id });
-                    break;
-                }
-
-                SchedulerMsg::DirtyAll => todo!(),
-
-                SchedulerMsg::TaskNotified(id) => {
-                    let mut tasks = self.scheduler.tasks.borrow_mut();
-                    let task = &tasks[id.0];
-
-                    // If the task completes...
-                    if task.progress() {
-                        // Remove it from the scope so we dont try to double drop it when the scope dropes
-                        self.scopes[task.scope.0].spawned_tasks.remove(&id);
-
-                        // Remove it from the scheduler
-                        tasks.remove(id.0);
-                    }
-                }
-
-                SchedulerMsg::SuspenseNotified(id) => {
-                    println!("suspense notified");
-
-                    let leaf = self
-                        .scheduler
-                        .leaves
-                        .borrow_mut()
-                        .get(id.0)
-                        .unwrap()
-                        .clone();
-
-                    let scope_id = leaf.scope_id;
-
-                    // todo: cache the waker
-                    let waker = leaf.waker();
-                    let mut cx = Context::from_waker(&waker);
-
-                    // Safety: the future is always pinned to the bump arena
-                    let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
-                    let as_pinned_mut = &mut pinned;
-
-                    // the component finished rendering and gave us nodes
-                    // we should attach them to that component and then render its children
-                    // continue rendering the tree until we hit yet another suspended component
-                    if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
-                        let boundary = &self.scopes[leaf.scope_id.0]
-                            .consume_context::<SuspenseContext>()
-                            .unwrap();
-
-                        println!("ready pool");
-
-                        let mut fiber = boundary.borrow_mut();
-
-                        println!(
-                            "Existing mutations {:?}, scope {:?}",
-                            fiber.mutations, fiber.id
-                        );
-
-                        let scope = &mut self.scopes[scope_id.0];
-                        let arena = scope.current_arena();
-
-                        let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
-                        arena.node.set(ret);
-
-                        if let RenderReturn::Sync(Some(template)) = ret {
-                            let mutations = &mut fiber.mutations;
-                            let template: &VNode = unsafe { std::mem::transmute(template) };
-                            let mutations: &mut Mutations =
-                                unsafe { std::mem::transmute(mutations) };
-
-                            self.scope_stack.push(scope_id);
-                            self.create(mutations, template);
-                            self.scope_stack.pop();
-
-                            println!("{:#?}", mutations);
-                        } else {
-                            println!("nodes arent right");
-                        }
-                    } else {
-                        println!("not ready");
-                    }
-                }
+    /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
+    /// queue
+    pub fn handle_task_wakeup(&mut self, id: TaskId) {
+        let mut tasks = self.scheduler.tasks.borrow_mut();
+        let task = &tasks[id.0];
+
+        // If the task completes...
+        if task.progress() {
+            // Remove it from the scope so we dont try to double drop it when the scope dropes
+            self.scopes[task.scope.0].spawned_tasks.remove(&id);
+
+            // Remove it from the scheduler
+            tasks.remove(id.0);
+        }
+    }
+
+    pub fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
+        println!("suspense notified");
+
+        let leaf = self
+            .scheduler
+            .leaves
+            .borrow_mut()
+            .get(id.0)
+            .unwrap()
+            .clone();
+
+        let scope_id = leaf.scope_id;
+
+        // todo: cache the waker
+        let waker = leaf.waker();
+        let mut cx = Context::from_waker(&waker);
+
+        // Safety: the future is always pinned to the bump arena
+        let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
+        let as_pinned_mut = &mut pinned;
+
+        // the component finished rendering and gave us nodes
+        // we should attach them to that component and then render its children
+        // continue rendering the tree until we hit yet another suspended component
+        if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
+            let boundary = &self.scopes[leaf.scope_id.0]
+                .consume_context::<SuspenseContext>()
+                .unwrap();
+
+            println!("ready pool");
+
+            let mut fiber = boundary.borrow_mut();
+
+            println!(
+                "Existing mutations {:?}, scope {:?}",
+                fiber.mutations, fiber.id
+            );
+
+            let scope = &mut self.scopes[scope_id.0];
+            let arena = scope.current_frame();
+
+            let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
+            arena.node.set(ret);
+
+            if let RenderReturn::Sync(Some(template)) = ret {
+                let mutations = &mut fiber.mutations;
+                let template: &VNode = unsafe { std::mem::transmute(template) };
+                let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
+
+                self.scope_stack.push(scope_id);
+                self.create(mutations, template);
+                self.scope_stack.pop();
+
+                println!("{:#?}", mutations);
+            } else {
+                println!("nodes arent right");
             }
+        } else {
+            println!("not ready");
         }
     }
 }

+ 10 - 9
packages/core/src/scopes.rs

@@ -7,7 +7,6 @@ use std::{
 };
 
 use bumpalo::Bump;
-use futures_channel::mpsc::UnboundedSender;
 use std::future::Future;
 
 use crate::{
@@ -69,20 +68,27 @@ pub struct ScopeState {
 }
 
 impl ScopeState {
-    pub fn current_arena(&self) -> &BumpFrame {
+    pub fn current_frame(&self) -> &BumpFrame {
         match self.render_cnt % 2 {
             0 => &self.node_arena_1,
             1 => &self.node_arena_2,
             _ => unreachable!(),
         }
     }
+    pub fn previous_frame(&self) -> &BumpFrame {
+        match self.render_cnt % 2 {
+            1 => &self.node_arena_1,
+            0 => &self.node_arena_2,
+            _ => unreachable!(),
+        }
+    }
 
     pub fn bump(&self) -> &Bump {
-        &self.current_arena().bump
+        &self.current_frame().bump
     }
 
     pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
-        let r = unsafe { &*self.current_arena().node.get() };
+        let r = unsafe { &*self.current_frame().node.get() };
         unsafe { std::mem::transmute(r) }
     }
 
@@ -142,11 +148,6 @@ impl ScopeState {
         self.id
     }
 
-    /// Get a handle to the raw update scheduler channel
-    pub fn scheduler_channel(&self) -> UnboundedSender<SchedulerMsg> {
-        self.tasks.sender.clone()
-    }
-
     /// Create a subscription that schedules a future render for the reference component
     ///
     /// ## Notice: you should prefer using [`schedule_update_any`] and [`scope_id`]

+ 248 - 67
packages/core/src/virtual_dom.rs

@@ -1,17 +1,17 @@
-use crate::any_props::VComponentProps;
-use crate::arena::ElementPath;
-use crate::diff::DirtyScope;
-use crate::factory::RenderReturn;
-use crate::innerlude::{Mutations, Scheduler, SchedulerMsg};
-use crate::mutations::Mutation;
-use crate::nodes::{Template, TemplateId};
 use crate::{
+    any_props::VComponentProps,
     arena::ElementId,
+    arena::ElementPath,
+    diff::DirtyScope,
+    factory::RenderReturn,
+    innerlude::{is_path_ascendant, Mutations, Scheduler, SchedulerMsg},
+    mutations::Mutation,
+    nodes::{Template, TemplateId},
+    scheduler::{SuspenseBoundary, SuspenseId},
     scopes::{ScopeId, ScopeState},
+    Attribute, AttributeValue, Element, Scope, SuspenseContext, UiEvent,
 };
-use crate::{scheduler, Element, Scope};
-use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
-use scheduler::{SuspenseBoundary, SuspenseId};
+use futures_util::{pin_mut, FutureExt, StreamExt};
 use slab::Slab;
 use std::collections::{BTreeSet, HashMap};
 use std::future::Future;
@@ -56,56 +56,88 @@ use std::rc::Rc;
 /// let edits = vdom.rebuild();
 /// ```
 ///
-/// To inject UserEvents into the VirtualDom, call [`VirtualDom::get_scheduler_channel`] to get access to the scheduler.
+/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
 ///
 /// ```rust, ignore
-/// let channel = vdom.get_scheduler_channel();
-/// channel.send_unbounded(SchedulerMsg::UserEvent(UserEvent {
-///     // ...
-/// }))
+/// vdom.handle_event(event);
 /// ```
 ///
-/// While waiting for UserEvents to occur, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
+/// While no events are ready, call [`VirtualDom::wait_for_work`] to poll any futures inside the VirtualDom.
 ///
 /// ```rust, ignore
 /// vdom.wait_for_work().await;
 /// ```
 ///
-/// Once work is ready, call [`VirtualDom::work_with_deadline`] to compute the differences between the previous and
+/// Once work is ready, call [`VirtualDom::render_with_deadline`] to compute the differences between the previous and
 /// current UI trees. This will return a [`Mutations`] object that contains Edits, Effects, and NodeRefs that need to be
 /// handled by the renderer.
 ///
 /// ```rust, ignore
-/// let mutations = vdom.work_with_deadline(|| false);
-/// for edit in mutations {
-///     apply(edit);
+/// let mutations = vdom.work_with_deadline(tokio::time::sleep(Duration::from_millis(100)));
+///
+/// for edit in mutations.edits {
+///     real_dom.apply(edit);
 /// }
 /// ```
 ///
+/// To not wait for suspense while diffing the VirtualDom, call [`VirtualDom::render_immediate`] or pass an immediately
+/// ready future to [`VirtualDom::render_with_deadline`].
+///
+///
 /// ## Building an event loop around Dioxus:
 ///
 /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
-///
-/// ```rust, ignore
-/// fn App(cx: Scope) -> Element {
+/// ```rust
+/// fn app(cx: Scope) -> Element {
 ///     cx.render(rsx!{
 ///         div { "Hello World" }
 ///     })
 /// }
 ///
-/// async fn main() {
-///     let mut dom = VirtualDom::new(App);
+/// let dom = VirtualDom::new(app);
 ///
-///     let mut inital_edits = dom.rebuild();
-///     apply_edits(inital_edits);
+/// real_dom.apply(dom.rebuild());
 ///
-///     loop {
-///         dom.wait_for_work().await;
-///         let frame_timeout = TimeoutFuture::new(Duration::from_millis(16));
-///         let deadline = || (&mut frame_timeout).now_or_never();
-///         let edits = dom.run_with_deadline(deadline).await;
-///         apply_edits(edits);
+/// loop {
+///     select! {
+///         _ = dom.wait_for_work() => {}
+///         evt = real_dom.wait_for_event() => dom.handle_event(evt),
 ///     }
+///
+///     real_dom.apply(dom.render_immediate());
+/// }
+/// ```
+///
+/// ## Waiting for suspense
+///
+/// Because Dioxus supports suspense, you can use it for server-side rendering, static site generation, and other usecases
+/// where waiting on portions of the UI to finish rendering is important. To wait for suspense, use the
+/// [`VirtualDom::render_with_deadline`] method:
+///
+/// ```rust
+/// let dom = VirtualDom::new(app);
+///
+/// let deadline = tokio::time::sleep(Duration::from_millis(100));
+/// let edits = dom.render_with_deadline(deadline).await;
+/// ```
+///
+/// ## Use with streaming
+///
+/// If not all rendering is done by the deadline, it might be worthwhile to stream the rest later. To do this, we
+/// suggest rendering with a deadline, and then looping between [`VirtualDom::wait_for_work`] and render_immediate until
+/// no suspended work is left.
+///
+/// ```
+/// let dom = VirtualDom::new(app);
+///
+/// let deadline = tokio::time::sleep(Duration::from_millis(20));
+/// let edits = dom.render_with_deadline(deadline).await;
+///
+/// real_dom.apply(edits);
+///
+/// while dom.has_suspended_work() {
+///    dom.wait_for_work().await;
+///    real_dom.apply(dom.render_immediate());
 /// }
 /// ```
 pub struct VirtualDom {
@@ -186,27 +218,7 @@ impl VirtualDom {
     where
         P: 'static,
     {
-        let channel = futures_channel::mpsc::unbounded();
-        Self::new_with_props_and_scheduler(root, root_props, channel)
-    }
-
-    /// Launch the VirtualDom, but provide your own channel for receiving and sending messages into the scheduler
-    ///
-    /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the
-    /// VirtualDom to be created just to retrieve its channel receiver.
-    ///
-    /// ```rust, ignore
-    /// let channel = futures_channel::mpsc::unbounded();
-    /// let dom = VirtualDom::new_with_scheduler(Example, (), channel);
-    /// ```
-    pub fn new_with_props_and_scheduler<P: 'static>(
-        root: fn(Scope<P>) -> Element,
-        root_props: P,
-        (tx, rx): (
-            UnboundedSender<SchedulerMsg>,
-            UnboundedReceiver<SchedulerMsg>,
-        ),
-    ) -> Self {
+        let (tx, rx) = futures_channel::mpsc::unbounded();
         let mut dom = Self {
             rx,
             scheduler: Scheduler::new(tx),
@@ -220,18 +232,138 @@ impl VirtualDom {
             finished_fibers: Vec::new(),
         };
 
-        dom.new_scope(Box::into_raw(Box::new(VComponentProps::new(
+        let root = dom.new_scope(Box::into_raw(Box::new(VComponentProps::new(
             root,
             |_, _| unreachable!(),
             root_props,
-        ))))
+        ))));
+
         // The root component is always a suspense boundary for any async children
         // This could be unexpected, so we might rethink this behavior
-        .provide_context(SuspenseBoundary::new(ScopeId(0)));
+        root.provide_context(SuspenseBoundary::new(ScopeId(0)));
+
+        // the root element is always given element 0
+        dom.elements.insert(ElementPath::null());
 
         dom
     }
 
+    pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
+        self.scopes.get(id.0)
+    }
+
+    pub fn base_scope(&self) -> &ScopeState {
+        self.scopes.get(0).unwrap()
+    }
+
+    fn mark_dirty_scope(&mut self, id: ScopeId) {
+        let height = self.scopes[id.0].height;
+        self.dirty_scopes.insert(DirtyScope { height, id });
+    }
+
+    fn is_scope_suspended(&self, id: ScopeId) -> bool {
+        !self.scopes[id.0]
+            .consume_context::<SuspenseContext>()
+            .unwrap()
+            .borrow()
+            .waiting_on
+            .is_empty()
+    }
+
+    /// Returns true if there is any suspended work left to be done.
+    pub fn has_suspended_work(&self) -> bool {
+        !self.scheduler.leaves.borrow().is_empty()
+    }
+
+    /// Returns None if no element could be found
+    pub fn handle_event<T: 'static>(&mut self, event: &UiEvent<T>) -> Option<()> {
+        let path = self.elements.get(event.element.0)?;
+
+        let location = unsafe { &mut *path.template }
+            .dynamic_attrs
+            .iter()
+            .position(|attr| attr.mounted_element.get() == event.element)?;
+
+        let mut index = Some((path.template, location));
+
+        let mut listeners = Vec::<&Attribute>::new();
+
+        while let Some((raw_parent, dyn_index)) = index {
+            let parent = unsafe { &mut *raw_parent };
+            let path = parent.template.node_paths[dyn_index];
+
+            listeners.extend(
+                parent
+                    .dynamic_attrs
+                    .iter()
+                    .enumerate()
+                    .filter_map(|(idx, attr)| {
+                        match is_path_ascendant(parent.template.node_paths[idx], path) {
+                            true if attr.name == event.name => Some(attr),
+                            _ => None,
+                        }
+                    }),
+            );
+
+            index = parent.parent;
+        }
+
+        for listener in listeners {
+            if let AttributeValue::Listener(listener) = &listener.value {
+                (listener.borrow_mut())(&event.event)
+            }
+        }
+
+        Some(())
+    }
+
+    /// Wait for the scheduler to have any work.
+    ///
+    /// This method polls the internal future queue, waiting for suspense nodes, tasks, or other work. This completes when
+    /// any work is ready. If multiple scopes are marked dirty from a task or a suspense tree is finished, this method
+    /// will exit.
+    ///
+    /// This method is cancel-safe, so you're fine to discard the future in a select block.
+    ///
+    /// This lets us poll async tasks during idle periods without blocking the main thread.
+    ///
+    /// # Example
+    ///
+    /// ```rust, ignore
+    /// let dom = VirtualDom::new(App);
+    /// let sender = dom.get_scheduler_channel();
+    /// ```
+    pub async fn wait_for_work(&mut self) {
+        let mut some_msg = None;
+
+        loop {
+            match some_msg.take() {
+                // If a bunch of messages are ready in a sequence, try to pop them off synchronously
+                Some(msg) => match msg {
+                    SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id),
+                    SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
+                    SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
+                },
+
+                // If they're not ready, then we should wait for them to be ready
+                None => {
+                    match self.rx.try_next() {
+                        Ok(Some(val)) => some_msg = Some(val),
+                        Ok(None) => return,
+                        Err(_) => {
+                            // If we have any dirty scopes, or finished fiber trees then we should exit
+                            if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() {
+                                return;
+                            }
+
+                            some_msg = self.rx.next().await
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     /// 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.
@@ -268,22 +400,71 @@ impl VirtualDom {
         mutations
     }
 
+    /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
+    /// suspended subtrees.
+    pub fn render_immediate(&mut self) -> Mutations {
+        // Build a waker that won't wake up since our deadline is already expired when it's polled
+        let waker = futures_util::task::noop_waker();
+        let mut cx = std::task::Context::from_waker(&waker);
+
+        // Now run render with deadline but dont even try to poll any async tasks
+        let fut = self.render_with_deadline(std::future::ready(()));
+        pin_mut!(fut);
+        match fut.poll_unpin(&mut cx) {
+            std::task::Poll::Ready(mutations) => mutations,
+            std::task::Poll::Pending => panic!("render_immediate should never return pending"),
+        }
+    }
+
     /// Render what you can given the timeline and then move on
     ///
     /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
-    pub async fn render_with_deadline(
-        &mut self,
+    ///
+    /// If no suspense trees are present
+    pub async fn render_with_deadline<'a>(
+        &'a mut self,
         deadline: impl Future<Output = ()>,
-    ) -> Vec<Mutation> {
-        todo!()
-    }
+    ) -> Mutations<'a> {
+        use futures_util::future::{select, Either};
 
-    pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
-        self.scopes.get(id.0)
-    }
+        let mut mutations = Mutations::new(0);
+        pin_mut!(deadline);
 
-    pub fn base_scope(&self) -> &ScopeState {
-        self.scopes.get(0).unwrap()
+        loop {
+            // first, unload any complete suspense trees
+            for finished_fiber in self.finished_fibers.drain(..) {
+                let scope = &mut self.scopes[finished_fiber.0];
+                let context = scope.has_context::<SuspenseContext>().unwrap();
+                let mut context = context.borrow_mut();
+                mutations.extend(context.mutations.drain(..));
+            }
+
+            // Next, diff any dirty scopes
+            // We choose not to poll the deadline since we complete pretty quickly anyways
+            if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
+                self.dirty_scopes.remove(&dirty);
+
+                // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update
+                if !self.is_scope_suspended(dirty.id) {
+                    self.run_scope(dirty.id);
+                    self.diff_scope(&mut mutations, dirty.id);
+                }
+            }
+
+            // Wait for suspense, or a deadline
+            if self.dirty_scopes.is_empty() {
+                if self.scheduler.leaves.borrow().is_empty() {
+                    return mutations;
+                }
+
+                let (work, deadline) = (self.wait_for_work(), &mut deadline);
+                pin_mut!(work);
+
+                if let Either::Left((_, _)) = select(deadline, work).await {
+                    return mutations;
+                }
+            }
+        }
     }
 }
 

+ 0 - 25
packages/core/tests/simple_syntax.rs

@@ -1,25 +0,0 @@
-use dioxus_core::prelude::*;
-
-fn app(cx: Scope) -> Element {
-    todo!();
-    // render! {
-    //      Suspend {
-    //          delay: Duration::from_millis(100),
-    //          fallback: rsx! { "Loading..." },
-    //          ChildAsync {}
-    //          ChildAsync {}
-    //          ChildAsync {}
-    //      }
-    // }
-}
-
-async fn ChildAsync(cx: Scope<'_>) -> Element {
-    todo!()
-}
-
-#[test]
-fn it_works() {
-    let mut dom = VirtualDom::new(app);
-
-    let mut mutations = dom.rebuild();
-}

+ 19 - 16
packages/core/tests/task.rs

@@ -6,26 +6,29 @@ use dioxus_core::*;
 async fn it_works() {
     let mut dom = VirtualDom::new(app);
 
-    let mutations = dom.rebuild();
+    let _ = dom.rebuild();
 
-    println!("mutations: {:?}", mutations);
-
-    dom.wait_for_work().await;
+    tokio::select! {
+        _ = dom.wait_for_work() => {}
+        _ = tokio::time::sleep(Duration::from_millis(1000)) => {}
+    };
 }
 
 fn app(cx: Scope) -> Element {
-    cx.spawn(async {
-        for x in 0..10 {
-            tokio::time::sleep(Duration::from_millis(500)).await;
-            println!("Hello, world! {x}");
-        }
-    });
-
-    cx.spawn(async {
-        for x in 0..10 {
-            tokio::time::sleep(Duration::from_millis(250)).await;
-            println!("Hello, world does! {x}");
-        }
+    cx.use_hook(|| {
+        cx.spawn(async {
+            for x in 0..10 {
+                tokio::time::sleep(Duration::from_millis(50)).await;
+                println!("Hello, world! {x}");
+            }
+        });
+
+        cx.spawn(async {
+            for x in 0..10 {
+                tokio::time::sleep(Duration::from_millis(25)).await;
+                println!("Hello, world from second thread! {x}");
+            }
+        });
     });
 
     None

+ 0 - 3
packages/dioxus/Cargo.toml

@@ -35,9 +35,6 @@ env_logger = "0.9.0"
 tokio = { version = "1.21.2", features = ["full"] }
 # dioxus-edit-stream = { path = "../edit-stream" }
 
-[[bench]]
-name = "create"
-harness = false
 
 [[bench]]
 name = "jsframework"

+ 0 - 1
packages/dioxus/benches/create.rs

@@ -1 +0,0 @@
-fn main() {}

+ 21 - 15
packages/dioxus/benches/jsframework.rs

@@ -5,9 +5,14 @@
 //! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
 //! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
 //!
-//! On my MBP 2019:
-//! - Dioxus takes 3ms to create 1_000 rows
-//! - Dioxus takes 30ms to create 10_000 rows
+//!
+//! Pre-templates (Mac M1):
+//! - 3ms to create 1_000 rows
+//! - 30ms to create 10_000 rows
+//!
+//! Post-templates
+//! - 580us to create 1_000 rows
+//! - 6.2ms to create 10_000 rows
 //!
 //! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
 //! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
@@ -20,25 +25,26 @@ criterion_group!(mbenches, create_rows);
 criterion_main!(mbenches);
 
 fn create_rows(c: &mut Criterion) {
-    static App: Component = |cx| {
+    fn app(cx: Scope) -> Element {
         let mut rng = SmallRng::from_entropy();
 
-        render!(table {
-            tbody {
-                (0..10_000_usize).map(|f| {
-                    let label = Label::new(&mut rng);
-                    rsx!(Row {
-                        row_id: f,
-                        label: label
+        render!(
+            table {
+                tbody {
+                    (0..10_000_usize).map(|f| {
+                        let label = Label::new(&mut rng);
+                        rsx!( Row { row_id: f, label: label } )
                     })
-                })
+                }
             }
-        })
-    };
+        )
+    }
 
     c.bench_function("create rows", |b| {
+        let mut dom = VirtualDom::new(app);
+        dom.rebuild();
+
         b.iter(|| {
-            let mut dom = VirtualDom::new(App);
             let g = dom.rebuild();
             assert!(g.edits.len() > 1);
         })

+ 101 - 0
packages/dioxus/examples/stress.rs

@@ -0,0 +1,101 @@
+use dioxus::prelude::*;
+use rand::prelude::*;
+
+fn main() {
+    let mut dom = VirtualDom::new(app);
+
+    dom.rebuild();
+
+    for _ in 0..1000 {
+        dom.rebuild();
+    }
+}
+
+fn app(cx: Scope) -> Element {
+    let mut rng = SmallRng::from_entropy();
+
+    render! (
+        table {
+            tbody {
+                (0..10_000_usize).map(|f| {
+                    let label = Label::new(&mut rng);
+                    rsx!( Row { row_id: f, label: label } )
+                })
+            }
+        }
+    )
+}
+
+#[derive(PartialEq, Props)]
+struct RowProps {
+    row_id: usize,
+    label: Label,
+}
+fn Row(cx: Scope<RowProps>) -> Element {
+    let [adj, col, noun] = cx.props.label.0;
+    render! {
+        tr {
+            td { class:"col-md-1", "{cx.props.row_id}" }
+            td { class:"col-md-1", onclick: move |_| { /* run onselect */ },
+                a { class: "lbl", "{adj}" "{col}" "{noun}" }
+            }
+            td { class: "col-md-1",
+                a { class: "remove", onclick: move |_| {/* remove */},
+                    span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
+                }
+            }
+            td { class: "col-md-6" }
+        }
+    }
+}
+
+#[derive(PartialEq)]
+struct Label([&'static str; 3]);
+
+impl Label {
+    fn new(rng: &mut SmallRng) -> Self {
+        Label([
+            ADJECTIVES.choose(rng).unwrap(),
+            COLOURS.choose(rng).unwrap(),
+            NOUNS.choose(rng).unwrap(),
+        ])
+    }
+}
+
+static ADJECTIVES: &[&str] = &[
+    "pretty",
+    "large",
+    "big",
+    "small",
+    "tall",
+    "short",
+    "long",
+    "handsome",
+    "plain",
+    "quaint",
+    "clean",
+    "elegant",
+    "easy",
+    "angry",
+    "crazy",
+    "helpful",
+    "mushy",
+    "odd",
+    "unsightly",
+    "adorable",
+    "important",
+    "inexpensive",
+    "cheap",
+    "expensive",
+    "fancy",
+];
+
+static COLOURS: &[&str] = &[
+    "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
+    "orange",
+];
+
+static NOUNS: &[&str] = &[
+    "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
+    "pizza", "mouse", "keyboard",
+];

+ 56 - 3
packages/dioxus/tests/rsx_syntax.rs

@@ -1,5 +1,3 @@
-use dioxus::prelude::*;
-
 fn basic_syntax_is_a_template(cx: Scope) -> Element {
     let asd = 123;
     let var = 123;
@@ -23,8 +21,63 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
     })
 }
 
+// imports come after the test since the rsx! macro is sensitive to its location in the file
+// (the byte index is used to differentiate sub templates)
+use dioxus::core::{ElementId, Mutation};
+use dioxus::prelude::*;
+
 #[test]
 fn dual_stream() {
     let mut dom = VirtualDom::new(basic_syntax_is_a_template);
-    dbg!(dom.rebuild());
+    let edits = dom.rebuild();
+
+    use Mutation::*;
+    assert_eq!(
+        edits.template_mutations,
+        vec![
+            CreateElement { name: "div", namespace: None, id: ElementId(1) },
+            SetAttribute { name: "class", value: "asd", id: ElementId(1) },
+            CreateElement { name: "div", namespace: None, id: ElementId(2) },
+            CreatePlaceholder { id: ElementId(3) },
+            AppendChildren { m: 1 },
+            CreateElement { name: "div", namespace: None, id: ElementId(4) },
+            CreateElement { name: "h1", namespace: None, id: ElementId(5) },
+            CreateText { value: "var" },
+            AppendChildren { m: 1 },
+            CreateElement { name: "p", namespace: None, id: ElementId(6) },
+            CreateText { value: "you're great!" },
+            AppendChildren { m: 1 },
+            CreateElement { name: "div", namespace: None, id: ElementId(7) },
+            SetAttribute { name: "background-color", value: "red", id: ElementId(7) },
+            CreateElement { name: "h1", namespace: None, id: ElementId(8) },
+            CreateText { value: "var" },
+            AppendChildren { m: 1 },
+            CreateElement { name: "div", namespace: None, id: ElementId(9) },
+            CreateElement { name: "b", namespace: None, id: ElementId(10) },
+            CreateText { value: "asd" },
+            AppendChildren { m: 1 },
+            CreateText { value: "not great" },
+            AppendChildren { m: 2 },
+            AppendChildren { m: 2 },
+            CreateElement { name: "p", namespace: None, id: ElementId(11) },
+            CreateText { value: "you're great!" },
+            AppendChildren { m: 1 },
+            AppendChildren { m: 4 },
+            AppendChildren { m: 2 },
+            SaveTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", m: 1 }
+        ]
+    );
+
+    assert_eq!(
+        edits.edits,
+        vec![
+            LoadTemplate { name: "packages/dioxus/tests/rsx_syntax.rs:5:15:122", index: 0 },
+            AssignId { path: &[], id: ElementId(12) },
+            SetAttribute { name: "class", value: "123", id: ElementId(12) },
+            SetAttribute { name: "onclick", value: "asd", id: ElementId(12) }, // ---- todo: listeners
+            HydrateText { path: &[0, 0], value: "123", id: ElementId(13) },
+            ReplacePlaceholder { m: 1, path: &[0, 0] },
+            AppendChildren { m: 1 }
+        ]
+    );
 }

+ 1 - 0
packages/dioxus/tests/suspense.rs

@@ -1,6 +1,7 @@
 use std::future::IntoFuture;
 
 use dioxus::prelude::*;
+use dioxus_core::SuspenseBoundary;
 
 #[inline_props]
 fn suspense_boundary<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {

+ 0 - 1
packages/dioxus/tests/task.rs

@@ -1 +0,0 @@
-fn main() {}