From 19df1bda109aba03c40ff631263bcb7035004ca0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 15 Nov 2021 09:49:01 -0500 Subject: [PATCH] feat: bubbling --- examples/tasks.rs | 17 +- packages/core/src/diff.rs | 15 +- packages/core/src/mutations.rs | 189 +++++++++++----------- packages/core/src/nodes.rs | 11 +- packages/core/src/scope.rs | 233 +++++++++++++++------------- packages/core/src/scopearena.rs | 8 +- packages/core/src/virtual_dom.rs | 107 +++++++------ packages/core/tests/vdom_rebuild.rs | 31 +++- packages/desktop/src/events.rs | 4 +- packages/web/examples/async.rs | 29 ++++ packages/web/examples/suspense.rs | 40 +++++ packages/web/src/dom.rs | 6 +- 12 files changed, 398 insertions(+), 292 deletions(-) create mode 100644 packages/web/examples/async.rs create mode 100644 packages/web/examples/suspense.rs diff --git a/examples/tasks.rs b/examples/tasks.rs index a7daca839..54aa7ee5f 100644 --- a/examples/tasks.rs +++ b/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!" + // } } }) }; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 2ec0564db..8c6831ca3 100644 --- a/packages/core/src/diff.rs +++ b/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); } } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 1686b1c08..ffad7c967 100644 --- a/packages/core/src/mutations.rs +++ b/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>, - pub noderefs: Vec>, + pub refs: Vec>, 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::()) } } - -/// 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", - } - } -} diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c43657158..cedebddab 100644 --- a/packages/core/src/nodes.rs +++ b/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>>, } -pub struct VSuspended<'a> { - pub task_id: u64, +pub struct VSuspended { + pub task_id: usize, + pub scope: Cell>, pub dom_id: Cell>, - - #[allow(clippy::type_complexity)] - pub callback: RefCell Element + 'a>>>, + pub parent: Cell>, } /// A cached node is a "pointer" to a "rendered" node in a particular scope diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 02989f7ff..a2497ed80 100644 --- a/packages/core/src/scope.rs +++ b/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>, + pub(crate) suspended_nodes: FxHashMap, pub(crate) tasks: Vec>>, pub(crate) pending_effects: Vec>, } @@ -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 { + 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>) -> Option { - 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 + 'src) -> usize { - // allocate the future - let fut: &mut dyn Future = self.bump().alloc(fut); - - // wrap it in a type that will actually drop the contents - let boxed_fut: BumpBox> = 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. - /// - /// 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 { - if self.is_subtree_root.get() { - None - } else { - todo!() - // let cur = self.subtree().get(); - // self.shared.cur_subtree.set(cur + 1); - // Some(cur) - } + /// 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 } - /// Get the subtree ID that this scope belongs to. + /// 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 + 'src>( + &'src self, + mut fut: impl FnOnce() -> F + 'src, + ) -> usize { + // allocate the future + let fut = fut(); + let fut: &mut dyn Future = self.bump().alloc(fut); + + // wrap it in a type that will actually drop the contents + let boxed_fut: BumpBox> = 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. /// - /// # Example + /// ## 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() + /// ```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>) -> Option { + 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) + } + + pub fn suspend<'src, F: Future + 'src>( + &'src self, + mut fut: impl FnMut() -> F, + ) -> Option { + 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) } } diff --git a/packages/core/src/scopearena.rs b/packages/core/src/scopearena.rs index 315e32a5b..c83d1820a 100644 --- a/packages/core/src/scopearena.rs +++ b/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, + pub pending_futures: RefCell>, scope_counter: Cell, pub scopes: RefCell>, pub heuristics: RefCell>, @@ -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()); } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 452d31d7d..6dda54d18 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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 { 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`. + /// + /// ```rust, ignore + /// let mut vdom = VirtualDom::new(App); + /// + /// let timeout = TimeoutFuture::from_ms(16); + /// let deadline = || (&mut timeout).now_or_never(); + /// + /// 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. /// - /// 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. - /// - /// 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. - /// - /// 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 { 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, + pub element: Option, /// The event type IE "onclick" or "onmouseover" /// @@ -651,7 +666,7 @@ pub struct UserEvent { pub name: &'static str, /// Event Data - pub event: Arc, + pub data: Arc, } /// Priority of Event Triggers. diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index b7fef7643..dc7b76755 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/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] diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 5b146e409..ec5c6aebd 100644 --- a/packages/desktop/src/events.rs +++ b/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, } } diff --git a/packages/web/examples/async.rs b/packages/web/examples/async.rs new file mode 100644 index 000000000..09301ba27 --- /dev/null +++ b/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!" } + }) +}; diff --git a/packages/web/examples/suspense.rs b/packages/web/examples/suspense.rs new file mode 100644 index 000000000..e0c7b690c --- /dev/null +++ b/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::() + .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} + }) +}; diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index c4a09b2bc..630cd2b36 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -703,9 +703,9 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { 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, }) }