diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 73da2bd83..95f19657a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -277,8 +277,11 @@ impl<'bump> DiffMachine<'bump> { fn create_suspended_node(&mut self, suspended: &'bump VSuspended) { let real_id = self.vdom.reserve_node(); self.mutations.create_placeholder(real_id); + suspended.dom_id.set(Some(real_id)); self.stack.add_child_count(1); + + self.attach_suspended_node_to_scope(suspended); } fn create_anchor_node(&mut self, anchor: &'bump VAnchor) { @@ -394,7 +397,7 @@ impl<'bump> DiffMachine<'bump> { } (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), (Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()), - (Suspended(old), Suspended(new)) => new.dom_id.set(old.dom_id.get()), + (Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new), (Element(old), Element(new)) => self.diff_element_nodes(old, new), // Anything else is just a basic replace and create @@ -564,6 +567,11 @@ impl<'bump> DiffMachine<'bump> { self.diff_children(old.children, new.children); } + fn diff_suspended_nodes(&mut self, old: &'bump VSuspended, new: &'bump VSuspended) { + new.dom_id.set(old.dom_id.get()); + self.attach_suspended_node_to_scope(new); + } + // ============================================= // Utilites for creating new diff instructions // ============================================= @@ -961,7 +969,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - search_node = Some(scope.root()); + search_node = Some(scope.root_node()); } } } @@ -979,7 +987,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(el) => { let scope_id = el.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - search_node = Some(scope.root()); + search_node = Some(scope.root_node()); } VNode::Text(t) => break t.dom_id.get(), VNode::Element(t) => break t.dom_id.get(), @@ -1045,7 +1053,7 @@ impl<'bump> DiffMachine<'bump> { VNode::Component(c) => { let scope_id = c.associated_scope.get().unwrap(); let scope = self.vdom.get_scope(scope_id).unwrap(); - let root = scope.root(); + let root = scope.root_node(); self.remove_nodes(Some(root)); } } @@ -1075,6 +1083,21 @@ impl<'bump> DiffMachine<'bump> { let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) }; queue.push(long_listener as *const _) } + + fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) { + if let Some(scope) = self + .stack + .current_scope() + .and_then(|id| self.vdom.get_scope_mut(id)) + { + // safety: this lifetime is managed by the logic on scope + let extended: &VSuspended<'static> = unsafe { std::mem::transmute(suspended) }; + scope + .suspended_nodes + .borrow_mut() + .insert(suspended.task_id, extended as *const _); + } + } } fn compare_strs(a: &str, b: &str) -> bool { diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 16dfbc869..3f732af48 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -566,6 +566,53 @@ impl Scheduler { // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope), // }; } + + pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations { + // + let mut shared = self.pool.clone(); + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); + + // TODO: drain any in-flight work + let cur_component = self + .pool + .get_scope_mut(base_scope) + .expect("The base scope should never be moved"); + + // We run the component. If it succeeds, then we can diff it and add the changes to the dom. + if cur_component.run_scope(&self.pool) { + diff_machine + .stack + .create_node(cur_component.frames.fin_head(), MountType::Append); + + diff_machine.stack.scope_stack.push(base_scope); + + diff_machine.work(|| false); + } else { + // todo: should this be a hard error? + log::warn!( + "Component failed to run succesfully during rebuild. + This does not result in a failed rebuild, but indicates a logic failure within your app." + ); + } + + unsafe { std::mem::transmute(diff_machine.mutations) } + } + + pub fn hard_diff(&mut self, base_scope: ScopeId) -> Mutations { + let cur_component = self + .pool + .get_scope_mut(base_scope) + .expect("The base scope should never be moved"); + + if cur_component.run_scope(&self.pool) { + let mut diff_machine = DiffMachine::new(Mutations::new(), &mut self.pool); + diff_machine.cfg.force_diff = true; + diff_machine.diff_scope(base_scope); + diff_machine.mutations + } else { + Mutations::new() + } + } } pub(crate) struct PriorityLane { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 4d4a12077..93d82da10 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,5 @@ use crate::innerlude::*; -use fxhash::FxHashSet; +use fxhash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -24,35 +24,116 @@ pub struct Scope { pub(crate) parent_idx: Option, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, - pub(crate) descendents: RefCell>, // Nodes - // an internal, highly efficient storage of vnodes - // lots of safety condsiderations pub(crate) frames: ActiveFrame, pub(crate) caller: *const dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, pub(crate) child_nodes: ScopeChildren<'static>, - // Listeners + /* + we care about: + - listeners (and how to call them when an event is triggered) + - borrowed props (and how to drop them when the parent is dropped) + - suspended nodes (and how to call their callback when their associated tasks are complete) + */ pub(crate) listeners: RefCell>>, pub(crate) borrowed_props: RefCell>>, - pub(crate) suspended_nodes: RefCell>>, + pub(crate) suspended_nodes: RefCell>>, // State pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - pub(crate) memoized_updater: Rc, + // whenever set_state is called, we fire off a message to the scheduler + // this closure _is_ the method called by schedule_update that marks this component as dirty + pub(crate) memoized_updater: Rc, pub(crate) shared: EventChannel, } -// The type of closure that wraps calling components +/// Public interface for Scopes. +impl Scope { + /// Get the root VNode for this Scope. + /// + /// This VNode is the "entrypoint" VNode. If the component renders multiple nodes, then this VNode will be a fragment. + /// + /// # Example + /// ```rust + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// if let VNode::VElement(node) = base.root_node() { + /// assert_eq!(node.tag_name, "div"); + /// } + /// ``` + pub fn root_node(&self) -> &VNode { + self.frames.fin_head() + } + /// Get the height of this Scope - IE the number of scopes above it. + /// + /// A Scope with a height of `0` is the root scope - there are no other scopes above it. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.height(), 0); + /// ``` + pub fn height(&self) -> u32 { + self.height + } + + /// Get the Parent of this Scope within this Dioxus VirtualDOM. + /// + /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted. + /// + /// The base component will not have a parent, and will return `None`. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.parent(), None); + /// ``` + pub fn parent(&self) -> Option { + self.parent_idx + } + + /// Get the ID of this Scope within this Dioxus VirtualDOM. + /// + /// This ID is not unique across Dioxus VirtualDOMs or across time. IDs will be reused when components are unmounted. + /// + /// # Example + /// + /// ```rust + /// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} })); + /// dom.rebuild(); + /// let base = dom.base_scope(); + /// + /// assert_eq!(base.scope_id(), 0); + /// ``` + pub fn scope_id(&self) -> ScopeId { + self.our_arena_idx + } +} + +// The type of closure that wraps calling components /// The type of task that gets sent to the task scheduler /// Submitting a fiber task returns a handle to that task, which can be used to wake up suspended nodes pub type FiberTask = Pin>>; +/// Private interface for Scopes. impl Scope { // we are being created in the scope of an existing component (where the creator_node lifetime comes into play) // we are going to break this lifetime by force in order to save it on ourselves. @@ -61,20 +142,23 @@ impl Scope { // // Scopes cannot be made anywhere else except for this file // Therefore, their lifetimes are connected exclusively to the virtual dom - pub(crate) fn new<'creator_node>( - caller: &'creator_node dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, - arena_idx: ScopeId, - parent: Option, + pub(crate) fn new( + caller: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b>, + our_arena_idx: ScopeId, + parent_idx: Option, height: u32, child_nodes: ScopeChildren, shared: EventChannel, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; - let up = shared.schedule_any_immediate.clone(); - let memoized_updater = Rc::new(move || up(arena_idx)); + let schedule_any_update = shared.schedule_any_immediate.clone(); + + let memoized_updater = Rc::new(move || schedule_any_update(our_arena_idx)); let caller = caller as *const _; + + // wipe away the associated lifetime - we are going to manually manage the one-way lifetime graph let caller = unsafe { std::mem::transmute(caller) }; Self { @@ -82,16 +166,16 @@ impl Scope { shared, child_nodes, caller, - parent_idx: parent, - our_arena_idx: arena_idx, + parent_idx, + our_arena_idx, height, + frames: ActiveFrame::new(), hooks: Default::default(), suspended_nodes: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), borrowed_props: Default::default(), - descendents: Default::default(), } } @@ -107,35 +191,8 @@ impl Scope { self.child_nodes = child_nodes; } - /// Returns true if the scope completed successfully - /// - pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { - // Cycle to the next frame and then reset it - // This breaks any latent references, invalidating every pointer referencing into it. - // Remove all the outdated listeners - self.ensure_drop_safety(pool); - - // Safety: - // - We dropped the listeners, so no more &mut T can be used while these are held - // - All children nodes that rely on &mut T are replaced with a new reference - unsafe { self.hooks.reset() }; - - // Safety: - // - We've dropped all references to the wip bump frame - unsafe { self.frames.reset_wip_frame() }; - - // Cast the caller ptr from static to one with our own reference - let render: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b> = unsafe { &*self.caller }; - - match render(self) { - None => false, - Some(new_head) => { - // the user's component succeeded. We can safely cycle to the next frame - self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; - self.frames.cycle_frame(); - true - } - } + pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren { + unsafe { self.child_nodes.shorten_lifetime() } } /// This method cleans up any references to data held within our hook list. This prevents mutable aliasing from @@ -151,7 +208,6 @@ impl Scope { // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Referrence) // right now, we don't drop - // let vdom = &self.vdom; self.borrowed_props .get_mut() .drain(..) @@ -163,8 +219,8 @@ impl Scope { scope.ensure_drop_safety(pool); // Now, drop our own reference - let mut dropper = comp.drop_props.borrow_mut().take().unwrap(); - dropper(); + let mut drop_props = comp.drop_props.borrow_mut().take().unwrap(); + drop_props(); }); // Now that all the references are gone, we can safely drop our own references in our listeners. @@ -172,14 +228,10 @@ impl Scope { .get_mut() .drain(..) .map(|li| unsafe { &*li }) - .for_each(|listener| { - listener.callback.borrow_mut().take(); - }); + .for_each(|listener| drop(listener.callback.borrow_mut().take())); } - // A safe wrapper around calling listeners - // - // + /// A safe wrapper around calling listeners pub(crate) fn call_listener(&mut self, event: SyntheticEvent, element: ElementId) { let listners = self.listeners.borrow_mut(); @@ -205,31 +257,68 @@ impl Scope { } } - pub(crate) fn child_nodes<'a>(&'a self) -> ScopeChildren { - unsafe { self.child_nodes.shorten_lifetime() } - } + /* + General strategy here is to load up the appropriate suspended task and then run it. + Suspended nodes cannot be called repeatedly. + */ + pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) { + let mut nodes = self.suspended_nodes.borrow_mut(); - pub(crate) fn call_suspended_node<'a>(&'a self, task: u64) { - let g = self.suspended_nodes.borrow_mut(); - - if let Some(suspended) = g.get(&task) { - let sus: &'a VSuspended<'static> = unsafe { &**suspended }; + if let Some(suspended) = nodes.remove(&task_id) { + let sus: &'a VSuspended<'static> = unsafe { &*suspended }; let sus: &'a VSuspended<'a> = unsafe { std::mem::transmute(sus) }; - let bump = self.frames.wip_frame(); - let mut cb = sus.callback.borrow_mut(); - let mut _cb = cb.take().unwrap(); let cx: SuspendedContext<'a> = SuspendedContext { inner: Context { props: &(), - scope: &self, + scope: self, }, }; - let new_node: DomTree<'a> = (_cb)(cx); + + let mut cb = sus.callback.borrow_mut().take().unwrap(); + + let new_node: DomTree<'a> = (cb)(cx); } } - pub fn root(&self) -> &VNode { - self.frames.fin_head() + /// Render this component. + /// + /// Returns true if the scope completed successfully and false if running failed (IE a None error was propagated). + pub(crate) fn run_scope<'sel>(&'sel mut self, pool: &ResourcePool) -> bool { + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. + // Remove all the outdated listeners + self.ensure_drop_safety(pool); + + // Safety: + // - We dropped the listeners, so no more &mut T can be used while these are held + // - All children nodes that rely on &mut T are replaced with a new reference + unsafe { self.hooks.reset() }; + + // Safety: + // - We've dropped all references to the wip bump frame + unsafe { self.frames.reset_wip_frame() }; + + // just forget about our suspended nodes while we're at it + self.suspended_nodes.get_mut().clear(); + + // guarantee that we haven't screwed up - there should be no latent references anywhere + debug_assert!(self.listeners.borrow().is_empty()); + debug_assert!(self.suspended_nodes.borrow().is_empty()); + debug_assert!(self.borrowed_props.borrow().is_empty()); + + // Cast the caller ptr from static to one with our own reference + let render: &dyn for<'b> Fn(&'b Scope) -> DomTree<'b> = unsafe { &*self.caller }; + + // Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders. + if let Some(new_head) = render(self) { + // the user's component succeeded. We can safely cycle to the next frame + self.frames.wip_frame_mut().head_node = unsafe { std::mem::transmute(new_head) }; + self.frames.cycle_frame(); + + true + } else { + false + } } } diff --git a/packages/core/src/vdomdisplay.rs b/packages/core/src/vdomdisplay.rs index 1bb36ce3c..7b6b126a3 100644 --- a/packages/core/src/vdomdisplay.rs +++ b/packages/core/src/vdomdisplay.rs @@ -100,7 +100,7 @@ impl std::fmt::Display for VirtualDom { VNode::Component(vcomp) => { let idx = vcomp.associated_scope.get().unwrap(); if !self.cfg.skip_components { - let new_node = vdom.get_scope(idx).unwrap().root(); + let new_node = vdom.get_scope(idx).unwrap().root_node(); self.html_render(vdom, new_node, f, il)?; } } @@ -113,7 +113,7 @@ impl std::fmt::Display for VirtualDom { } let base = self.base_scope(); - let root = base.root(); + let root = base.root_node(); let renderer = ScopeRenderer { scope: base, cfg: Cfg { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8f1d63055..c4b349101 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -20,7 +20,7 @@ //! Additional functionality is defined in the respective files. use crate::innerlude::*; -use std::{any::Any, pin::Pin}; +use std::any::Any; /// An integrated virtual node system that progresses events and diffs UI trees. /// @@ -59,9 +59,10 @@ pub struct VirtualDom { root_fc: Box, - root_caller: Box Fn(&'b Scope) -> DomTree<'b> + 'static>, - root_props: Box, + + // we need to keep the allocation around, but we don't necessarily use it + _root_caller: Box Fn(&'b Scope) -> DomTree<'b> + 'static>, } impl VirtualDom { @@ -146,7 +147,7 @@ impl VirtualDom { }); Self { - root_caller, + _root_caller: root_caller, root_fc, base_scope, scheduler, @@ -187,11 +188,14 @@ impl VirtualDom { /// ``` pub fn update_root_props<'s, P: 'static>(&'s mut self, root_props: P) -> Option> { let root_scope = self.scheduler.pool.get_scope_mut(self.base_scope).unwrap(); + + // Pre-emptively drop any downstream references of the old props root_scope.ensure_drop_safety(&self.scheduler.pool); let mut root_props: Box = Box::new(root_props); if let Some(props_ptr) = root_props.downcast_ref::

().map(|p| p as *const P) { + // Swap the old props and new props std::mem::swap(&mut self.root_props, &mut root_props); let root = *self.root_fc.downcast_ref::>().unwrap(); @@ -204,6 +208,8 @@ impl VirtualDom { root_scope.update_scope_dependencies(&root_caller, ScopeChildren(&[])); + drop(root_props); + Some(self.rebuild()) } else { None @@ -228,33 +234,7 @@ impl VirtualDom { /// apply_edits(edits); /// ``` pub fn rebuild<'s>(&'s mut self) -> Mutations<'s> { - let mut shared = self.scheduler.pool.clone(); - let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared); - - let cur_component = self - .scheduler - .pool - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); - - // We run the component. If it succeeds, then we can diff it and add the changes to the dom. - if cur_component.run_scope(&self.scheduler.pool) { - diff_machine - .stack - .create_node(cur_component.frames.fin_head(), MountType::Append); - - diff_machine.stack.scope_stack.push(self.base_scope); - - diff_machine.work(|| false); - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); - } - - unsafe { std::mem::transmute(diff_machine.mutations) } + self.scheduler.rebuild(self.base_scope) } /// Compute a manual diff of the VirtualDOM between states. @@ -292,21 +272,7 @@ impl VirtualDom { /// let edits = dom.diff(); /// ``` pub fn diff<'s>(&'s mut self) -> Mutations<'s> { - let cur_component = self - .scheduler - .pool - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); - - if cur_component.run_scope(&self.scheduler.pool) { - let mut diff_machine: DiffMachine<'s> = - DiffMachine::new(Mutations::new(), &mut self.scheduler.pool); - diff_machine.cfg.force_diff = true; - diff_machine.diff_scope(self.base_scope); - diff_machine.mutations - } else { - Mutations::new() - } + self.scheduler.hard_diff(self.base_scope) } /// Runs the virtualdom immediately, not waiting for any suspended nodes to complete. diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index a03520b27..57d9d11df 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -28,7 +28,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option { "{:}", TextRenderer { cfg: SsrConfig::default(), - root: vdom.get_scope(scope).unwrap().root(), + root: vdom.get_scope(scope).unwrap().root_node(), vdom: Some(vdom) } )) @@ -68,7 +68,7 @@ impl<'a> TextRenderer<'a> { pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self { Self { cfg, - root: vdom.base_scope().root(), + root: vdom.base_scope().root_node(), vdom: Some(vdom), } } @@ -172,7 +172,7 @@ impl<'a> TextRenderer<'a> { let idx = vcomp.associated_scope.get().unwrap(); match (self.vdom, self.cfg.skip_components) { (Some(vdom), false) => { - let new_node = vdom.get_scope(idx).unwrap().root(); + let new_node = vdom.get_scope(idx).unwrap().root_node(); self.html_render(new_node, f, il + 1)?; } _ => {