Ver código fonte

feat: bubbling

Jonathan Kelley 3 anos atrás
pai
commit
19df1bd

+ 14 - 3
examples/tasks.rs

@@ -2,6 +2,8 @@
 //!
 //! The example from the README.md.
 
+use std::time::Duration;
+
 use dioxus::prelude::*;
 fn main() {
     dioxus::desktop::launch(App, |c| c);
@@ -10,14 +12,23 @@ fn main() {
 static App: FC<()> = |cx, props| {
     let mut count = use_state(cx, || 0);
 
-    cx.push_task(async {
-        panic!("polled future");
-        //
+    cx.push_task(async move {
+        tokio::time::sleep(Duration::from_millis(100)).await;
+        println!("setting count");
+        count += 1;
+        // count.set(10);
+        // *count += 1;
+        // let c = count.get() + 1;
+        // count.set(c);
     });
 
     cx.render(rsx! {
         div {
             h1 { "High-Five counter: {count}" }
+            // button {
+            //     onclick: move |_| count +=1 ,
+            //     "Click me!"
+            // }
         }
     })
 };

+ 8 - 7
packages/core/src/diff.rs

@@ -1344,13 +1344,14 @@ impl<'bump> DiffState<'bump> {
             .current_scope()
             .and_then(|id| self.scopes.get_scope(&id))
         {
-            // safety: this lifetime is managed by the logic on scope
-            let extended = unsafe { std::mem::transmute(suspended) };
-            scope
-                .items
-                .borrow_mut()
-                .suspended_nodes
-                .insert(suspended.task_id, extended);
+            todo!()
+            // // safety: this lifetime is managed by the logic on scope
+            // let extended = unsafe { std::mem::transmute(suspended) };
+            // scope
+            //     .items
+            //     .borrow_mut()
+            //     .suspended_nodes
+            //     .insert(suspended.task_id, extended);
         }
     }
 }

+ 87 - 102
packages/core/src/mutations.rs

@@ -5,28 +5,111 @@
 use crate::innerlude::*;
 use std::{any::Any, fmt::Debug};
 
+/// ## Mutations
+///
+/// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
+/// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
+/// applied the edits.
+///
+/// Mutations are the only link between the RealDOM and the VirtualDOM.
 pub struct Mutations<'a> {
     pub edits: Vec<DomEdit<'a>>,
-    pub noderefs: Vec<NodeRefMutation<'a>>,
+    pub refs: Vec<NodeRefMutation<'a>>,
     pub effects: Vec<&'a dyn FnMut()>,
 }
+
 impl Debug for Mutations<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("Mutations")
             .field("edits", &self.edits)
-            .field("noderefs", &self.noderefs)
-            // .field("effects", &self.effects)
+            .field("noderefs", &self.refs)
             .finish()
     }
 }
 
+/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
+/// network or through FFI boundaries.
+#[derive(Debug, PartialEq)]
+#[cfg_attr(
+    feature = "serialize",
+    derive(serde::Serialize, serde::Deserialize),
+    serde(tag = "type")
+)]
+pub enum DomEdit<'bump> {
+    PushRoot {
+        root: u64,
+    },
+    PopRoot,
+
+    AppendChildren {
+        many: u32,
+    },
+
+    // "Root" refers to the item directly
+    // it's a waste of an instruction to push the root directly
+    ReplaceWith {
+        root: u64,
+        m: u32,
+    },
+    InsertAfter {
+        root: u64,
+        n: u32,
+    },
+    InsertBefore {
+        root: u64,
+        n: u32,
+    },
+    Remove {
+        root: u64,
+    },
+    CreateTextNode {
+        text: &'bump str,
+        root: u64,
+    },
+    CreateElement {
+        tag: &'bump str,
+        root: u64,
+    },
+    CreateElementNs {
+        tag: &'bump str,
+        root: u64,
+        ns: &'static str,
+    },
+    CreatePlaceholder {
+        root: u64,
+    },
+    NewEventListener {
+        event_name: &'static str,
+        scope: ScopeId,
+        root: u64,
+    },
+    RemoveEventListener {
+        root: u64,
+        event: &'static str,
+    },
+    SetText {
+        root: u64,
+        text: &'bump str,
+    },
+    SetAttribute {
+        root: u64,
+        field: &'static str,
+        value: &'bump str,
+        ns: Option<&'bump str>,
+    },
+    RemoveAttribute {
+        root: u64,
+        name: &'static str,
+    },
+}
+
 use DomEdit::*;
 
 impl<'a> Mutations<'a> {
     pub(crate) fn new() -> Self {
         Self {
             edits: Vec::new(),
-            noderefs: Vec::new(),
+            refs: Vec::new(),
             effects: Vec::new(),
         }
     }
@@ -156,101 +239,3 @@ impl<'a> NodeRefMutation<'a> {
             .and_then(|f| f.downcast_mut::<T>())
     }
 }
-
-/// A `DomEdit` represents a serialized form of the VirtualDom's trait-based API. This allows streaming edits across the
-/// network or through FFI boundaries.
-#[derive(Debug, PartialEq)]
-#[cfg_attr(
-    feature = "serialize",
-    derive(serde::Serialize, serde::Deserialize),
-    serde(tag = "type")
-)]
-pub enum DomEdit<'bump> {
-    PushRoot {
-        root: u64,
-    },
-    PopRoot,
-
-    AppendChildren {
-        many: u32,
-    },
-
-    // "Root" refers to the item directly
-    // it's a waste of an instruction to push the root directly
-    ReplaceWith {
-        root: u64,
-        m: u32,
-    },
-    InsertAfter {
-        root: u64,
-        n: u32,
-    },
-    InsertBefore {
-        root: u64,
-        n: u32,
-    },
-    Remove {
-        root: u64,
-    },
-    CreateTextNode {
-        text: &'bump str,
-        root: u64,
-    },
-    CreateElement {
-        tag: &'bump str,
-        root: u64,
-    },
-    CreateElementNs {
-        tag: &'bump str,
-        root: u64,
-        ns: &'static str,
-    },
-    CreatePlaceholder {
-        root: u64,
-    },
-    NewEventListener {
-        event_name: &'static str,
-        scope: ScopeId,
-        root: u64,
-    },
-    RemoveEventListener {
-        root: u64,
-        event: &'static str,
-    },
-    SetText {
-        root: u64,
-        text: &'bump str,
-    },
-    SetAttribute {
-        root: u64,
-        field: &'static str,
-        value: &'bump str,
-        ns: Option<&'bump str>,
-    },
-    RemoveAttribute {
-        root: u64,
-        name: &'static str,
-    },
-}
-impl DomEdit<'_> {
-    pub fn is(&self, id: &'static str) -> bool {
-        match self {
-            DomEdit::InsertAfter { .. } => id == "InsertAfter",
-            DomEdit::InsertBefore { .. } => id == "InsertBefore",
-            DomEdit::PushRoot { .. } => id == "PushRoot",
-            DomEdit::PopRoot => id == "PopRoot",
-            DomEdit::AppendChildren { .. } => id == "AppendChildren",
-            DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
-            DomEdit::Remove { .. } => id == "Remove",
-            DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
-            DomEdit::CreateElement { .. } => id == "CreateElement",
-            DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
-            DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
-            DomEdit::NewEventListener { .. } => id == "NewEventListener",
-            DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
-            DomEdit::SetText { .. } => id == "SetText",
-            DomEdit::SetAttribute { .. } => id == "SetAttribute",
-            DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
-        }
-    }
-}

+ 5 - 6
packages/core/src/nodes.rs

@@ -105,7 +105,7 @@ pub enum VNode<'src> {
     ///
     ///
     /// ```
-    Suspended(&'src VSuspended<'src>),
+    Suspended(&'src VSuspended),
 
     /// Anchors are a type of placeholder VNode used when fragments don't contain any children.
     ///
@@ -387,12 +387,11 @@ pub struct VComponent<'src> {
     pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
 }
 
-pub struct VSuspended<'a> {
-    pub task_id: u64,
+pub struct VSuspended {
+    pub task_id: usize,
+    pub scope: Cell<Option<ScopeId>>,
     pub dom_id: Cell<Option<ElementId>>,
-
-    #[allow(clippy::type_complexity)]
-    pub callback: RefCell<Option<BumpBox<'a, dyn FnMut() -> Element + 'a>>>,
+    pub parent: Cell<Option<ElementId>>,
 }
 
 /// A cached node is a "pointer" to a "rendered" node in a particular scope

+ 121 - 110
packages/core/src/scope.rs

@@ -72,7 +72,7 @@ pub struct Scope {
 pub struct SelfReferentialItems<'a> {
     pub(crate) listeners: Vec<&'a Listener<'a>>,
     pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
-    pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended<'a>>,
+    pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended>,
     pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
     pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
 }
@@ -107,6 +107,32 @@ impl Scope {
         self.subtree.get()
     }
 
+    /// Create a new subtree with this scope as the root of the subtree.
+    ///
+    /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
+    /// the mutations to the correct window/portal/subtree.
+    ///
+    /// This method
+    ///
+    /// # Example
+    ///
+    /// ```rust, ignore
+    /// fn App(cx: Context, props: &()) -> Element {
+    ///     todo!();
+    ///     rsx!(cx, div { "Subtree {id}"})
+    /// };
+    /// ```
+    pub fn create_subtree(&self) -> Option<u32> {
+        if self.is_subtree_root.get() {
+            None
+        } else {
+            todo!()
+            // let cur = self.subtree().get();
+            // self.shared.cur_subtree.set(cur + 1);
+            // Some(cur)
+        }
+    }
+
     /// 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.
@@ -207,75 +233,6 @@ impl Scope {
         &self.wip_frame().bump
     }
 
-    /// 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.
-    ///
-    /// ## Example
-    ///
-    /// ```ignore
-    /// fn Component(cx: Scope, props: &Props) -> Element {
-    ///     // Lazy assemble the VNode tree
-    ///     let lazy_nodes = rsx!("hello world");
-    ///
-    ///     // Actually build the tree and allocate it
-    ///     cx.render(lazy_tree)
-    /// }
-    ///```
-    pub fn render<'src>(&'src self, lazy_nodes: Option<LazyNodes<'src, '_>>) -> Option<NodeLink> {
-        let frame = self.wip_frame();
-        let bump = &frame.bump;
-        let factory = NodeFactory { bump };
-        let node = lazy_nodes.map(|f| f.call(factory))?;
-        let node = bump.alloc(node);
-
-        let node_ptr = node as *mut _;
-        let node_ptr = unsafe { std::mem::transmute(node_ptr) };
-
-        let link = NodeLink {
-            scope_id: Cell::new(Some(self.our_arena_idx)),
-            link_idx: Cell::new(0),
-            node: node_ptr,
-        };
-
-        Some(link)
-    }
-
-    /// Push an effect to be ran after the component has been successfully mounted to the dom
-    /// Returns the effect's position in the stack
-    pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
-        // this is some tricker to get around not being able to actually call fnonces
-        let mut slot = Some(effect);
-        let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
-
-        // wrap it in a type that will actually drop the contents
-        let boxed_fut = unsafe { BumpBox::from_raw(fut) };
-
-        // erase the 'src lifetime for self-referential storage
-        let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
-
-        let mut items = self.items.borrow_mut();
-        items.pending_effects.push(self_ref_fut);
-        items.pending_effects.len() - 1
-    }
-
-    /// Pushes the future onto the poll queue to be polled
-    /// The future is forcibly dropped if the component is not ready by the next render
-    pub fn push_task<'src>(&'src self, fut: impl Future<Output = ()> + 'src) -> usize {
-        // allocate the future
-        let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut);
-
-        // wrap it in a type that will actually drop the contents
-        let boxed_fut: BumpBox<dyn Future<Output = ()>> = unsafe { BumpBox::from_raw(fut) };
-
-        // erase the 'src lifetime for self-referential storage
-        let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
-
-        let mut items = self.items.borrow_mut();
-        items.tasks.push(self_ref_fut);
-        items.tasks.len() - 1
-    }
-
     /// This method enables the ability to expose state to children further down the VirtualDOM Tree.
     ///
     /// This is a "fundamental" operation and should only be called during initialization of a hook.
@@ -326,47 +283,102 @@ impl Scope {
         }
     }
 
-    /// Create a new subtree with this scope as the root of the subtree.
+    /// Push an effect to be ran after the component has been successfully mounted to the dom
+    /// Returns the effect's position in the stack
+    pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
+        // this is some tricker to get around not being able to actually call fnonces
+        let mut slot = Some(effect);
+        let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
+
+        // wrap it in a type that will actually drop the contents
+        let boxed_fut = unsafe { BumpBox::from_raw(fut) };
+
+        // erase the 'src lifetime for self-referential storage
+        let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
+
+        let mut items = self.items.borrow_mut();
+        items.pending_effects.push(self_ref_fut);
+        items.pending_effects.len() - 1
+    }
+
+    /// Pushes the future onto the poll queue to be polled
+    /// The future is forcibly dropped if the component is not ready by the next render
+    pub fn push_task<'src, F: Future<Output = ()> + 'src>(
+        &'src self,
+        mut fut: impl FnOnce() -> F + 'src,
+    ) -> usize {
+        // allocate the future
+        let fut = fut();
+        let fut: &mut dyn Future<Output = ()> = self.bump().alloc(fut);
+
+        // wrap it in a type that will actually drop the contents
+        let boxed_fut: BumpBox<dyn Future<Output = ()>> = unsafe { BumpBox::from_raw(fut) };
+
+        // erase the 'src lifetime for self-referential storage
+        let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
+
+        let mut items = self.items.borrow_mut();
+        items.tasks.push(self_ref_fut);
+        items.tasks.len() - 1
+    }
+
+    /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
     ///
-    /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
-    /// the mutations to the correct window/portal/subtree.
+    /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
     ///
-    /// This method
+    /// ## Example
     ///
-    /// # Example
+    /// ```ignore
+    /// fn Component(cx: Scope, props: &Props) -> Element {
+    ///     // Lazy assemble the VNode tree
+    ///     let lazy_nodes = rsx!("hello world");
     ///
-    /// ```rust, ignore
-    /// fn App(cx: Context, props: &()) -> Element {
-    ///     todo!();
-    ///     rsx!(cx, div { "Subtree {id}"})
-    /// };
-    /// ```
-    pub fn create_subtree(&self) -> Option<u32> {
-        if self.is_subtree_root.get() {
-            None
-        } else {
-            todo!()
-            // let cur = self.subtree().get();
-            // self.shared.cur_subtree.set(cur + 1);
-            // Some(cur)
-        }
+    ///     // Actually build the tree and allocate it
+    ///     cx.render(lazy_tree)
+    /// }
+    ///```
+    pub fn render<'src>(&'src self, lazy_nodes: Option<LazyNodes<'src, '_>>) -> Option<NodeLink> {
+        let frame = self.wip_frame();
+        let bump = &frame.bump;
+        let factory = NodeFactory { bump };
+        let node = lazy_nodes.map(|f| f.call(factory))?;
+        let node = bump.alloc(node);
+
+        let node_ptr = node as *mut _;
+        let node_ptr = unsafe { std::mem::transmute(node_ptr) };
+
+        let link = NodeLink {
+            scope_id: Cell::new(Some(self.our_arena_idx)),
+            link_idx: Cell::new(0),
+            node: node_ptr,
+        };
+
+        Some(link)
     }
 
-    /// Get the subtree ID that this scope belongs to.
-    ///
-    /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
-    /// the mutations to the correct window/portal/subtree.
-    ///
-    /// # Example
-    ///
-    /// ```rust, ignore
-    /// fn App(cx: Context, props: &()) -> Element {
-    ///     let id = cx.get_current_subtree();
-    ///     rsx!(cx, div { "Subtree {id}"})
-    /// };
-    /// ```
-    pub fn get_current_subtree(&self) -> u32 {
-        self.subtree()
+    pub fn suspend<'src, F: Future<Output = Element> + 'src>(
+        &'src self,
+        mut fut: impl FnMut() -> F,
+    ) -> Option<VNode> {
+        let channel = self.sender.clone();
+        let node_fut = fut();
+
+        let scope = self.scope_id();
+
+        // self.push_task(move || {
+        //
+        // async move {
+        //     //
+        //     let r = node_fut.await;
+        //     if let Some(node) = r {
+        //         channel
+        //             .unbounded_send(SchedulerMsg::Suspended { node, scope })
+        //             .unwrap();
+        //     }
+        // }
+        // });
+
+        todo!()
     }
 
     /// Store a value between renders
@@ -455,10 +467,9 @@ impl Scope {
         let mut nodes = &mut self.items.get_mut().suspended_nodes;
 
         if let Some(suspended) = nodes.remove(&task_id) {
-            let sus: &'a VSuspended<'static> = suspended;
-            let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) };
-            let mut boxed = sus.callback.borrow_mut().take().unwrap();
-            let new_node: Element = boxed();
+            let sus: &'a VSuspended = suspended;
+            // let mut boxed = sus.callback.borrow_mut().take().unwrap();
+            // let new_node: Element = boxed();
         }
     }
 
@@ -469,7 +480,7 @@ impl Scope {
         }
     }
 
-    pub fn root_node<'a>(&'a self) -> &'a VNode<'a> {
+    pub fn root_node(&self) -> &VNode {
         let node = *self.wip_frame().nodes.borrow().get(0).unwrap();
         unsafe { std::mem::transmute(&*node) }
     }

+ 4 - 4
packages/core/src/scopearena.rs

@@ -22,7 +22,7 @@ pub struct Heuristic {
 // has an internal heuristics engine to pre-allocate arenas to the right size
 pub(crate) struct ScopeArena {
     bump: Bump,
-    pub pending_futures: FxHashSet<ScopeId>,
+    pub pending_futures: RefCell<FxHashSet<ScopeId>>,
     scope_counter: Cell<usize>,
     pub scopes: RefCell<FxHashMap<ScopeId, *mut Scope>>,
     pub heuristics: RefCell<FxHashMap<FcSlot, Heuristic>>,
@@ -56,7 +56,7 @@ impl ScopeArena {
         Self {
             scope_counter: Cell::new(0),
             bump,
-            pending_futures: FxHashSet::default(),
+            pending_futures: RefCell::new(FxHashSet::default()),
             scopes: RefCell::new(FxHashMap::default()),
             heuristics: RefCell::new(FxHashMap::default()),
             free_scopes: RefCell::new(Vec::new()),
@@ -356,7 +356,7 @@ impl ScopeArena {
             debug_assert_eq!(scope.wip_frame().nodes.borrow().len(), 1);
 
             if !scope.items.borrow().tasks.is_empty() {
-                //
+                self.pending_futures.borrow_mut().insert(*id);
             }
 
             // make the "wip frame" contents the "finished frame"
@@ -381,7 +381,7 @@ impl ScopeArena {
                         if listener.event == event.name {
                             let mut cb = listener.callback.borrow_mut();
                             if let Some(cb) = cb.as_mut() {
-                                (cb)(event.event.clone());
+                                (cb)(event.data.clone());
                             }
                         }
                     }

+ 61 - 46
packages/core/src/virtual_dom.rs

@@ -263,7 +263,7 @@ impl VirtualDom {
         // todo: poll the events once even if there is work to do to prevent starvation
 
         // if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed
-        if self.scopes.pending_futures.is_empty() {
+        if self.scopes.pending_futures.borrow().is_empty() {
             self.pending_messages
                 .push_front(self.receiver.next().await.unwrap());
         } else {
@@ -275,7 +275,7 @@ impl VirtualDom {
                 type Output = ();
 
                 fn poll(
-                    mut self: Pin<&mut Self>,
+                    self: Pin<&mut Self>,
                     cx: &mut std::task::Context<'_>,
                 ) -> Poll<Self::Output> {
                     let mut all_pending = true;
@@ -284,7 +284,7 @@ impl VirtualDom {
                     let mut scopes_to_clear: SmallVec<[_; 10]> = smallvec::smallvec![];
 
                     // Poll every scope manually
-                    for fut in self.scopes.pending_futures.iter() {
+                    for fut in self.scopes.pending_futures.borrow().iter() {
                         let scope = self
                             .scopes
                             .get_scope(fut)
@@ -316,7 +316,7 @@ impl VirtualDom {
                     }
 
                     for scope in scopes_to_clear {
-                        self.scopes.pending_futures.remove(&scope);
+                        self.scopes.pending_futures.borrow_mut().remove(&scope);
                     }
 
                     // Resolve the future if any singular task is ready
@@ -347,28 +347,31 @@ impl VirtualDom {
 
     /// Run the virtualdom with a deadline.
     ///
-    /// This method will progress async tasks until the deadline is reached. If tasks are completed before the deadline,
-    /// and no tasks are pending, this method will return immediately. If tasks are still pending, then this method will
-    /// exhaust the deadline working on them.
+    /// This method will perform any outstanding diffing work and try to return as many mutations as possible before the
+    /// deadline is reached. This method accepts a closure that returns `true` if the deadline has been reached. To wrap
+    /// your future into a deadline, consider the `now_or_never` method from `future_utils`.
     ///
-    /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
-    /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
+    /// ```rust, ignore
+    /// let mut vdom = VirtualDom::new(App);
     ///
-    /// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline
-    /// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
-    /// deadline closure manually.
+    /// let timeout = TimeoutFuture::from_ms(16);
+    /// let deadline = || (&mut timeout).now_or_never();
     ///
-    /// The deadline is polled before starting to diff components. This strikes a balance between the overhead of checking
-    /// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
-    /// the screen will "jank" up. In debug, this will trigger an alert.
+    /// let mutations = vdom.work_with_deadline(deadline);
+    /// ```
+    ///
+    /// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
+    /// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
     ///
-    /// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
-    /// the provided deadline future resolves.
+    /// If the work is not finished by the deadline, Dioxus will store it for later and return when work_with_deadline
+    /// is called again. This means you can ensure some level of free time on the VirtualDom's thread during the work phase.
     ///
     /// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
     /// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
     /// entirely jank-free applications that perform a ton of work.
     ///
+    /// In general use, Dioxus is plenty fast enough to not need to worry about this.
+    ///
     /// # Example
     ///
     /// ```rust, ignore
@@ -387,15 +390,6 @@ impl VirtualDom {
     ///     apply_mutations(mutations);
     /// }
     /// ```
-    ///
-    /// ## Mutations
-    ///
-    /// This method returns "mutations" - IE the necessary changes to get the RealDOM to match the VirtualDOM. It also
-    /// includes a list of NodeRefs that need to be applied and effects that need to be triggered after the RealDOM has
-    /// applied the edits.
-    ///
-    /// Mutations are the only link between the RealDOM and the VirtualDOM.
-    ///
     pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> {
         let mut committed_mutations = vec![];
 
@@ -410,16 +404,18 @@ impl VirtualDom {
                     SchedulerMsg::Immediate(id) => {
                         self.dirty_scopes.insert(id);
                     }
+                    SchedulerMsg::Suspended { node, scope } => {
+                        todo!("should wire up suspense")
+                        // self.suspense_scopes.insert(id);
+                    }
                     SchedulerMsg::UiEvent(event) => {
-                        if let Some(element) = event.mounted_dom_id {
+                        if let Some(element) = event.element {
                             log::info!("Calling listener {:?}, {:?}", event.scope_id, element);
 
-                            // if let Some(scope) = self.scopes.get_scope(&event.scope_id) {
                             self.scopes.call_listener_with_bubbling(event, element);
                             while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
                                 self.pending_messages.push_front(dirty_scope);
                             }
-                            // }
                         } else {
                             log::debug!("User event without a targetted ElementId. Not currently supported.\nUnsure how to proceed. {:?}", event);
                         }
@@ -432,12 +428,9 @@ impl VirtualDom {
 
             let mut ran_scopes = FxHashSet::default();
 
-            // todo: the 2021 version of rust will let us not have to force the borrow
-            // let scopes = &self.scopes;
             // Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height
             self.dirty_scopes
                 .retain(|id| scopes.get_scope(id).is_some());
-
             self.dirty_scopes.sort_by(|a, b| {
                 let h1 = scopes.get_scope(a).unwrap().height;
                 let h2 = scopes.get_scope(b).unwrap().height;
@@ -445,13 +438,9 @@ impl VirtualDom {
             });
 
             if let Some(scopeid) = self.dirty_scopes.pop() {
-                log::info!("handling dirty scope {:?}", scopeid);
-
                 if !ran_scopes.contains(&scopeid) {
                     ran_scopes.insert(scopeid);
 
-                    log::debug!("about to run scope {:?}", scopeid);
-
                     if self.scopes.run_scope(&scopeid) {
                         let (old, new) = (
                             self.scopes.wip_head(&scopeid),
@@ -480,7 +469,6 @@ impl VirtualDom {
                 committed_mutations.push(mutations);
             } else {
                 // leave the work in an incomplete state
-                log::debug!("don't have a mechanism to pause work (yet)");
                 return committed_mutations;
             }
         }
@@ -530,7 +518,6 @@ impl VirtualDom {
     /// 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, ignore
     /// #[derive(PartialEq, Props)]
@@ -544,12 +531,7 @@ impl VirtualDom {
     /// };
     ///
     /// let value = Rc::new(RefCell::new("Hello"));
-    /// let mut dom = VirtualDom::new_with_props(
-    ///     App,
-    ///     AppProps {
-    ///         value: value.clone(),
-    ///     },
-    /// );
+    /// let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone(), });
     ///
     /// let _ = dom.rebuild();
     ///
@@ -633,8 +615,41 @@ pub enum SchedulerMsg {
 
     // setstate
     Immediate(ScopeId),
+
+    Suspended { scope: ScopeId, node: NodeLink },
 }
 
+/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel.
+///
+/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree
+/// where each listener is checked and fired if the event name matches.
+///
+/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in
+/// attempting to downcast the event data.
+///
+/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but
+/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel.
+///
+/// # Example
+/// ```rust
+/// fn App(cx: Context, props: &()) -> Element {
+///     rsx!(cx, div {
+///         onclick: move |_| println!("Clicked!")
+///     })
+/// }
+///
+/// let mut dom = VirtualDom::new(App);
+/// let mut scheduler = dom.get_scheduler_channel();
+/// scheduler.unbounded_send(SchedulerMsg::UiEvent(
+///     UserEvent {
+///         scope_id: None,
+///         priority: EventPriority::Medium,
+///         name: "click",
+///         element: Some(ElementId(0)),
+///         data: Arc::new(ClickEvent { .. })
+///     }
+/// )).unwrap();
+/// ```
 #[derive(Debug)]
 pub struct UserEvent {
     /// The originator of the event trigger
@@ -643,7 +658,7 @@ pub struct UserEvent {
     pub priority: EventPriority,
 
     /// The optional real node associated with the trigger
-    pub mounted_dom_id: Option<ElementId>,
+    pub element: Option<ElementId>,
 
     /// The event type IE "onclick" or "onmouseover"
     ///
@@ -651,7 +666,7 @@ pub struct UserEvent {
     pub name: &'static str,
 
     /// Event Data
-    pub event: Arc<dyn Any + Send + Sync>,
+    pub data: Arc<dyn Any + Send + Sync>,
 }
 
 /// Priority of Event Triggers.

+ 23 - 8
packages/core/tests/vdom_rebuild.rs

@@ -11,19 +11,18 @@
 //! Don't have a good way to validate, everything is done manually ATM
 
 use dioxus::prelude::*;
+use dioxus::DomEdit;
 use dioxus_core as dioxus;
 use dioxus_core_macro::*;
 use dioxus_html as dioxus_elements;
+use DomEdit::*;
 
 #[test]
 fn app_runs() {
-    static App: FC<()> = |cx, props| {
-        //
-        cx.render(rsx!( div{"hello"} ))
-    };
+    static App: FC<()> = |cx, props| rsx!(cx, div{"hello"} );
+
     let mut vdom = VirtualDom::new(App);
     let edits = vdom.rebuild();
-    dbg!(edits);
 }
 
 #[test]
@@ -65,9 +64,25 @@ fn conditional_rendering() {
     let mut vdom = VirtualDom::new(App);
 
     let mutations = vdom.rebuild();
-    dbg!(&mutations);
-    // the "false" fragment should generate an empty placeholder to re-visit
-    assert!(mutations.edits[mutations.edits.len() - 2].is("CreatePlaceholder"));
+    assert_eq!(
+        mutations.edits,
+        [
+            CreateElement { root: 1, tag: "h1" },
+            CreateTextNode {
+                root: 2,
+                text: "hello"
+            },
+            AppendChildren { many: 1 },
+            CreateElement {
+                root: 3,
+                tag: "span"
+            },
+            CreateTextNode { root: 4, text: "a" },
+            AppendChildren { many: 1 },
+            CreatePlaceholder { root: 5 },
+            AppendChildren { many: 3 },
+        ]
+    )
 }
 
 #[test]

+ 2 - 2
packages/desktop/src/events.rs

@@ -34,8 +34,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
         name,
         priority: EventPriority::Low,
         scope_id: None,
-        mounted_dom_id,
-        event,
+        element: mounted_dom_id,
+        data: event,
     }
 }
 

+ 29 - 0
packages/web/examples/async.rs

@@ -0,0 +1,29 @@
+//! Example: README.md showcase
+//!
+//! The example from the README.md.
+
+use dioxus::prelude::*;
+use dioxus_core as dioxus;
+use dioxus_core_macro::*;
+use dioxus_hooks::use_state;
+use dioxus_html as dioxus_elements;
+use dioxus_web;
+use gloo_timers::future::TimeoutFuture;
+
+fn main() {
+    dioxus_web::launch(App, |c| c);
+}
+
+static App: FC<()> = |cx, props| {
+    let mut count = use_state(cx, || 0);
+
+    cx.push_task(|| async move {
+        TimeoutFuture::new(100).await;
+        count += 1;
+    });
+
+    rsx!(cx, div {
+        h3 { "High-Five counter: {count}" }
+        button { onclick: move |_| count.set(0), "Reset!" }
+    })
+};

+ 40 - 0
packages/web/examples/suspense.rs

@@ -0,0 +1,40 @@
+#![allow(non_upper_case_globals)]
+
+//! Example: README.md showcase
+//!
+//! The example from the README.md.
+
+use dioxus::prelude::*;
+use dioxus_core as dioxus;
+use dioxus_core_macro::*;
+use dioxus_html as dioxus_elements;
+use dioxus_web;
+
+fn main() {
+    dioxus_web::launch(App, |c| c);
+}
+
+static App: FC<()> = |cx, _| {
+    let doggo = cx.suspend(|| async move {
+        #[derive(serde::Deserialize)]
+        struct Doggo {
+            message: String,
+        }
+
+        let src = reqwest::get("https://dog.ceo/api/breeds/image/random")
+            .await
+            .expect("Failed to fetch doggo")
+            .json::<Doggo>()
+            .await
+            .expect("Failed to parse doggo")
+            .message;
+
+        rsx!(cx, img { src: "{src}" })
+    });
+
+    rsx!(cx, div {
+        h1 {"One doggo coming right up"}
+        button { onclick: move |_| cx.needs_update(), "Get a new doggo" }
+        {doggo}
+    })
+};

+ 3 - 3
packages/web/src/dom.rs

@@ -703,9 +703,9 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
 
     Ok(UserEvent {
         name: event_name_from_typ(&typ),
-        event: virtual_event_from_websys_event(event.clone()),
-        mounted_dom_id: Some(ElementId(real_id as usize)),
-        scope_id: ScopeId(triggered_scope as usize),
+        data: virtual_event_from_websys_event(event.clone()),
+        element: Some(ElementId(real_id as usize)),
+        scope_id: Some(ScopeId(triggered_scope as usize)),
         priority: dioxus_core::EventPriority::Medium,
     })
 }