diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 8e5858a4c..9909e2fb4 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -126,11 +126,9 @@ impl VirtualDom { } }); - for root in node.root_ids { - if let Some(id) = root.get() { - if id.0 != 0 { - self.try_reclaim(id); - } + for id in &node.root_ids { + if id.0 != 0 { + self.try_reclaim(id); } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 239bd85fa..b0bca26d5 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,13 +5,50 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext}; +use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template}; use std::cell::Cell; -use std::iter::{Enumerate, Peekable}; +use std::iter::Peekable; use std::rc::Rc; -use std::slice; use TemplateNode::*; +fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> { + let mut with_indecies = paths.iter().copied().enumerate().collect::>(); + with_indecies.sort_unstable_by(|(_, a), (_, b)| { + let mut a = a.iter(); + let mut b = b.iter(); + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) => { + if a != b { + return a.cmp(b); + } + } + // The shorter path goes first + (Some(_), None) => return std::cmp::Ordering::Less, + (None, Some(_)) => return std::cmp::Ordering::Greater, + (None, None) => return std::cmp::Ordering::Equal, + } + } + }); + with_indecies +} + +#[test] +fn sorting() { + let r: [(usize, &[u8]); 5] = [ + (0, &[0, 1]), + (1, &[0, 2]), + (2, &[1, 0]), + (4, &[1, 1]), + (3, &[1, 2]), + ]; + assert_eq!( + sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 2,], &[1, 1,],]), + r + ); + assert!(matches!(&[0], &[_, ..])) +} + impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// @@ -25,21 +62,79 @@ impl<'b> VirtualDom { /// Create this template and write its mutations pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize { + // check for a overriden template + #[cfg(debug_assertions)] + { + let (path, byte_index) = node.template.get().name.rsplit_once(':').unwrap(); + if let Some(template) = self + .templates + .get(path) + .and_then(|map| map.get(&byte_index.parse().unwrap())) + { + node.template.set(*template); + } + } + + // Intialize the root nodes slice + node.root_ids + .intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice()); + // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways - if !self.templates.contains_key(&node.template.name) { - self.register_template(node); + if !self.templates.contains_key(&node.template.get().name) { + self.register_template(node.template.get()); } // we know that this will generate at least one mutation per node - self.mutations.edits.reserve(node.template.roots.len()); + self.mutations + .edits + .reserve(node.template.get().roots.len()); // Walk the roots, creating nodes and assigning IDs + // nodes in an iterator of ((dynamic_node_index, sorted_index), path) // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS) - let mut attrs = node.template.attr_paths.iter().enumerate().peekable(); - let mut nodes = node.template.node_paths.iter().enumerate().peekable(); + #[cfg(not(debug_assertions))] + let (mut attrs, mut nodes) = ( + node.template + .get() + .attr_paths + .iter() + .copied() + .enumerate() + .peekable(), + node.template + .get() + .node_paths + .iter() + .copied() + .enumerate() + .map(|(i, path)| ((i, i), path)) + .peekable(), + ); + // If this is a debug build, we need to check that the paths are in the correct order because hot reloading can cause scrambled states + + #[cfg(debug_assertions)] + let (attrs_sorted, nodes_sorted) = { + ( + sort_bfs(node.template.get().attr_paths), + sort_bfs(node.template.get().node_paths), + ) + }; + #[cfg(debug_assertions)] + let (mut attrs, mut nodes) = { + ( + attrs_sorted.into_iter().peekable(), + nodes_sorted + .iter() + .copied() + .enumerate() + .map(|(i, (id, path))| ((id, i), path)) + .peekable(), + ) + }; node.template + .get() .roots .iter() .enumerate() @@ -48,7 +143,14 @@ impl<'b> VirtualDom { nodes.next().unwrap(); self.write_dynamic_root(node, *id) } - Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes), + Element { .. } => { + #[cfg(not(debug_assertions))] + let id = self.write_element_root(node, idx, &mut attrs, &mut nodes, &[]); + #[cfg(debug_assertions)] + let id = + self.write_element_root(node, idx, &mut attrs, &mut nodes, &nodes_sorted); + id + } Text { .. } => self.write_static_text_root(node, idx), }) .sum() @@ -65,8 +167,9 @@ impl<'b> VirtualDom { fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize { use DynamicNode::*; match &template.dynamic_nodes[idx] { - node @ Fragment(_) => self.create_dynamic_node(template, node, idx), - node @ Component { .. } => self.create_dynamic_node(template, node, idx), + node @ Component { .. } | node @ Fragment(_) => { + self.create_dynamic_node(template, node, idx) + } Placeholder(VPlaceholder { id }) => { let id = self.set_slot(template, id, idx); self.mutations.push(CreatePlaceholder { id }); @@ -98,17 +201,18 @@ impl<'b> VirtualDom { &mut self, template: &'b VNode<'b>, root_idx: usize, - dynamic_attrs: &mut Peekable>>, - dynamic_nodes: &mut Peekable>>, + dynamic_attrs: &mut Peekable>, + dynamic_nodes_iter: &mut Peekable>, + dynamic_nodes: &[(usize, &'static [u8])], ) -> usize { // Load the template root and get the ID for the node on the stack let root_on_stack = self.load_template_root(template, root_idx); // Write all the attributes below this root - self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template); + self.write_attrs_on_root(dynamic_attrs, root_idx as u8, root_on_stack, template); // Load in all of the placeholder or dynamic content under this root too - self.load_placeholders(dynamic_nodes, root_idx, template); + self.load_placeholders(dynamic_nodes_iter, dynamic_nodes, root_idx as u8, template); 1 } @@ -126,22 +230,32 @@ impl<'b> VirtualDom { /// } /// } /// ``` + #[allow(unused)] fn load_placeholders( &mut self, - dynamic_nodes: &mut Peekable>>, - root_idx: usize, + dynamic_nodes_iter: &mut Peekable>, + dynamic_nodes: &[(usize, &'static [u8])], + root_idx: u8, template: &'b VNode<'b>, ) { - let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) { + let (start, end) = match collect_dyn_node_range(dynamic_nodes_iter, root_idx) { Some((a, b)) => (a, b), None => return, }; - for idx in (start..=end).rev() { + // If hot reloading is enabled, we need to map the sorted index to the original index of the dynamic node. If it is disabled, we can just use the sorted index + #[cfg(not(debug_assertions))] + let reversed_iter = (start..=end).rev(); + #[cfg(debug_assertions)] + let reversed_iter = (start..=end) + .rev() + .map(|sorted_index| dynamic_nodes[sorted_index].0); + + for idx in reversed_iter { let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); if m > 0 { // The path is one shorter because the top node is the root - let path = &template.template.node_paths[idx][1..]; + let path = &template.template.get().node_paths[idx][1..]; self.mutations.push(ReplacePlaceholder { m, path }); } } @@ -149,12 +263,14 @@ impl<'b> VirtualDom { fn write_attrs_on_root( &mut self, - attrs: &mut Peekable>>, - root_idx: usize, + attrs: &mut Peekable>, + root_idx: u8, root: ElementId, node: &VNode, ) { - while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) { + while let Some((mut attr_id, path)) = + attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) + { let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); loop { @@ -210,10 +326,10 @@ impl<'b> VirtualDom { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root let this_id = self.next_root(template, root_idx); - template.root_ids[root_idx].set(Some(this_id)); + template.root_ids.set(root_idx, this_id); self.mutations.push(LoadTemplate { - name: template.template.name, + name: template.template.get().name, index: root_idx, id: this_id, }); @@ -241,7 +357,7 @@ impl<'b> VirtualDom { // if attribute is on a root node, then we've already created the element // Else, it's deep in the template and we should create a new id for it - let id = self.next_element(template, template.template.attr_paths[attr_id]); + let id = self.next_element(template, template.template.get().attr_paths[attr_id]); self.mutations.push(Mutation::AssignId { path: &path[1..], @@ -252,14 +368,59 @@ impl<'b> VirtualDom { } /// Insert a new template into the VirtualDom's template registry - fn register_template(&mut self, template: &'b VNode<'b>) { + pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template<'static>) { // First, make sure we mark the template as seen, regardless if we process it - self.templates - .insert(template.template.name, template.template); + let (path, _) = template.name.rsplit_once(':').unwrap(); + if let Some((_, old_template)) = self + .templates + .entry(path) + .or_default() + .iter_mut() + .min_by_key(|(byte_index, _)| **byte_index) + { + // the byte index of the hot reloaded template could be different + template.name = old_template.name; + *old_template = template; + } else { + // This is a template without any current instances + self.templates + .entry(path) + .or_default() + .insert(usize::MAX, template); + } // If it's all dynamic nodes, then we don't need to register it - if !template.template.is_completely_dynamic() { - self.mutations.templates.push(template.template); + if !template.is_completely_dynamic() { + self.mutations.templates.push(template); + } + } + + /// Insert a new template into the VirtualDom's template registry + pub(crate) fn register_template(&mut self, mut template: Template<'static>) { + // First, make sure we mark the template as seen, regardless if we process it + let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + let byte_index = byte_index.parse::().unwrap(); + + // if hot reloading is enabled, then we need to check for a template that has overriten this one + #[cfg(debug_assertions)] + if let Some(mut new_template) = self + .templates + .get_mut(path) + .and_then(|map| map.remove(&usize::MAX)) + { + // the byte index of the hot reloaded template could be different + new_template.name = template.name; + template = new_template; + } + + self.templates + .entry(path) + .or_default() + .insert(byte_index, template); + + // If it's all dynamic nodes, then we don't need to register it + if !template.is_completely_dynamic() { + self.mutations.templates.push(template); } } @@ -285,7 +446,7 @@ impl<'b> VirtualDom { idx: usize, ) -> usize { // Allocate a dynamic element reference for this text node - let new_id = self.next_element(template, template.template.node_paths[idx]); + let new_id = self.next_element(template, template.template.get().node_paths[idx]); // Make sure the text node is assigned to the correct element text.id.set(Some(new_id)); @@ -296,7 +457,7 @@ impl<'b> VirtualDom { // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], value, }); @@ -311,14 +472,14 @@ impl<'b> VirtualDom { idx: usize, ) -> usize { // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.node_paths[idx]); + let id = self.next_element(template, template.template.get().node_paths[idx]); // Make sure the text node is assigned to the correct element placeholder.id.set(Some(id)); // Assign the ID to the existing node in the template self.mutations.push(AssignId { - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], id, }); @@ -386,7 +547,7 @@ impl<'b> VirtualDom { }; // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree - let new_id = self.next_element(new, parent.template.node_paths[idx]); + let new_id = self.next_element(new, parent.template.get().node_paths[idx]); // Now connect everything to the boundary self.scopes[scope.0].placeholder.set(Some(new_id)); @@ -408,7 +569,7 @@ impl<'b> VirtualDom { // Now assign the placeholder in the DOM self.mutations.push(AssignId { id: new_id, - path: &parent.template.node_paths[idx][1..], + path: &parent.template.get().node_paths[idx][1..], }); 0 @@ -423,7 +584,7 @@ impl<'b> VirtualDom { idx: usize, scope: ScopeId, ) -> usize { - let new_id = self.next_element(template, template.template.node_paths[idx]); + let new_id = self.next_element(template, template.template.get().node_paths[idx]); // Set the placeholder of the scope self.scopes[scope.0].placeholder.set(Some(new_id)); @@ -431,7 +592,7 @@ impl<'b> VirtualDom { // Since the placeholder is already in the DOM, we don't create any new nodes self.mutations.push(AssignId { id: new_id, - path: &template.template.node_paths[idx][1..], + path: &template.template.get().node_paths[idx][1..], }); 0 @@ -443,24 +604,26 @@ impl<'b> VirtualDom { slot: &'b Cell>, id: usize, ) -> ElementId { - let id = self.next_element(template, template.template.node_paths[id]); + let id = self.next_element(template, template.template.get().node_paths[id]); slot.set(Some(id)); id } } fn collect_dyn_node_range( - dynamic_nodes: &mut Peekable>>, - root_idx: usize, + dynamic_nodes: &mut Peekable>, + root_idx: u8, ) -> Option<(usize, usize)> { let start = match dynamic_nodes.peek() { - Some((idx, p)) if p[0] == root_idx as u8 => *idx, + Some(((_, idx), [first, ..])) if *first == root_idx => *idx, _ => return None, }; let mut end = start; - while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) { + while let Some(((_, idx), p)) = + dynamic_nodes.next_if(|(_, p)| matches!(p, [idx, ..] if *idx == root_idx)) + { if p.len() == 1 { continue; } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index abf938929..fa11a6e2f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -55,6 +55,21 @@ impl<'b> VirtualDom { fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + // If hot reloading is enabled, we need to make sure we're using the latest template + #[cfg(debug_assertions)] + { + let (path, byte_index) = right_template.template.get().name.rsplit_once(':').unwrap(); + if let Some(map) = self.templates.get(path) { + let byte_index = byte_index.parse::().unwrap(); + if let Some(&template) = map.get(&byte_index) { + right_template.template.set(template); + if template != left_template.template.get() { + return self.replace(left_template, [right_template]); + } + } + } + } + // If the templates are the same, we don't need to do anything, nor do we want to if templates_are_the_same(left_template, right_template) { return; @@ -99,11 +114,7 @@ impl<'b> VirtualDom { }); // Make sure the roots get transferred over while we're here - left_template - .root_ids - .iter() - .zip(right_template.root_ids.iter()) - .for_each(|(left, right)| right.set(left.get())); + right_template.root_ids.transfer(&left_template.root_ids); } fn diff_dynamic_node( @@ -644,6 +655,7 @@ impl<'b> VirtualDom { /// Push all the real nodes on the stack fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { node.template + .get() .roots .iter() .enumerate() @@ -652,7 +664,7 @@ impl<'b> VirtualDom { Some(node) => node, None => { self.mutations.push(Mutation::PushRoot { - id: node.root_ids[idx].get().unwrap(), + id: node.root_ids.get(idx).unwrap(), }); return 1; } @@ -779,11 +791,11 @@ impl<'b> VirtualDom { } fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { - for (idx, _) in node.template.roots.iter().enumerate() { + for (idx, _) in node.template.get().roots.iter().enumerate() { if let Some(dy) = node.dynamic_root(idx) { self.remove_dynamic_node(dy, gen_muts); } else { - let id = node.root_ids[idx].get().unwrap(); + let id = node.root_ids.get(idx).unwrap(); if gen_muts { self.mutations.push(Mutation::Remove { id }); } @@ -796,7 +808,15 @@ impl<'b> VirtualDom { let mut id = None; for (idx, attr) in node.dynamic_attrs.iter().enumerate() { // We'll clean up the root nodes either way, so don't worry - if node.template.attr_paths[idx].len() == 1 { + let path_len = node + .template + .get() + .attr_paths + .get(idx) + .map(|path| path.len()); + // if the path is 1 the attribute is in the root, so we don't need to clean it up + // if the path is 0, the attribute is a not attached at all, so we don't need to clean it up + if let Some(..=1) = path_len { continue; } @@ -814,12 +834,16 @@ impl<'b> VirtualDom { fn remove_nested_dyn_nodes(&mut self, node: &VNode) { for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { - // Roots are cleaned up automatically above - if node.template.node_paths[idx].len() == 1 { - continue; + let path_len = node + .template + .get() + .node_paths + .get(idx) + .map(|path| path.len()); + // Roots are cleaned up automatically above and nodes with a empty path are placeholders + if let Some(2..) = path_len { + self.remove_dynamic_node(dyn_node, false) } - - self.remove_dynamic_node(dyn_node, false); } } @@ -879,7 +903,7 @@ impl<'b> VirtualDom { fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { - None => node.root_ids[0].get().unwrap(), + None => node.root_ids.get(0).unwrap(), Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_first_element(&t[0]), Some(Placeholder(t)) => t.id.get().unwrap(), @@ -894,8 +918,8 @@ impl<'b> VirtualDom { } fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { - match node.dynamic_root(node.template.roots.len() - 1) { - None => node.root_ids.last().unwrap().get().unwrap(), + match node.dynamic_root(node.template.get().roots.len() - 1) { + None => node.root_ids.last().unwrap(), Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), Some(Placeholder(t)) => t.id.get().unwrap(), @@ -917,27 +941,30 @@ impl<'b> VirtualDom { /// We use the pointer of the dynamic_node list in this case fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool { std::ptr::eq(left_template, right_template) - || std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes) } fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool { - !std::ptr::eq(left_template.template.name, right_template.template.name) - && left_template.template.name != right_template.template.name + let left_template_name = left_template.template.get().name; + let right_template_name = right_template.template.get().name; + // we want to re-create the node if the template name is different by pointer even if the value is the same so that we can detect when hot reloading changes the template + !std::ptr::eq(left_template_name, right_template_name) } fn matching_components<'a>( left: &'a VNode<'a>, right: &'a VNode<'a>, ) -> Option, &'a VComponent<'a>)>> { - if left.template.roots.len() != right.template.roots.len() { + let left_template = left.template.get(); + let right_template = right.template.get(); + if left_template.roots.len() != right_template.roots.len() { return None; } // run through the components, ensuring they're the same - left.template + left_template .roots .iter() - .zip(right.template.roots.iter()) + .zip(right_template.roots.iter()) .map(|(l, r)| { let (l, r) = match (l, r) { (TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r), @@ -962,11 +989,12 @@ fn matching_components<'a>( /// - for appending children we can use AppendChildren #[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { - let path = node.template.node_paths[idx]; + let template = node.template.get(); + let path = template.node_paths[idx]; // use a loop to index every static node's children until the path has run out // only break if the last path index is a dynamic node - let mut static_node = &node.template.roots[path[0] as usize]; + let mut static_node = &template.roots[path[0] as usize]; for i in 1..path.len() - 1 { match static_node { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index d36031556..a9e919bfa 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -31,8 +31,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { Some(VNode { key: children.key, parent: children.parent, - template: children.template, - root_ids: children.root_ids, + template: children.template.clone(), + root_ids: children.root_ids.clone(), dynamic_nodes: children.dynamic_nodes, dynamic_attrs: children.dynamic_attrs, }) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 99b7499c9..24f2e083d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -5,7 +5,7 @@ use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use std::{ any::{Any, TypeId}, - cell::{Cell, RefCell}, + cell::{Cell, RefCell, UnsafeCell}, fmt::Arguments, future::Future, }; @@ -42,11 +42,11 @@ pub struct VNode<'a> { pub parent: Option, /// The static nodes and static descriptor of the template - pub template: Template<'static>, + pub template: Cell>, /// The IDs for the roots of this template - to be used when moving the template around and removing it from /// the actual Dom - pub root_ids: &'a [Cell>], + pub root_ids: BoxedCellSlice, /// The dynamic parts of the template pub dynamic_nodes: &'a [DynamicNode<'a>], @@ -55,21 +55,120 @@ pub struct VNode<'a> { pub dynamic_attrs: &'a [Attribute<'a>], } +// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell) +#[derive(Debug, Default)] +pub struct BoxedCellSlice(UnsafeCell>>); + +impl Clone for BoxedCellSlice { + fn clone(&self) -> Self { + Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() })) + } +} + +impl BoxedCellSlice { + pub fn last(&self) -> Option { + unsafe { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().last().copied()) + } + } + + pub fn get(&self, idx: usize) -> Option { + unsafe { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().get(idx).copied()) + } + } + + pub unsafe fn get_unchecked(&self, idx: usize) -> Option { + (*self.0.get()) + .as_ref() + .and_then(|inner| inner.as_ref().get(idx).copied()) + } + + pub fn set(&self, idx: usize, new: ElementId) { + unsafe { + if let Some(inner) = &mut *self.0.get() { + inner[idx] = new; + } + } + } + + pub fn intialize(&self, contents: Box<[ElementId]>) { + unsafe { + *self.0.get() = Some(contents); + } + } + + pub fn transfer(&self, other: &Self) { + unsafe { + *self.0.get() = (*other.0.get()).clone(); + } + } + + pub fn take_from(&self, other: &Self) { + unsafe { + *self.0.get() = (*other.0.get()).take(); + } + } + + pub fn len(&self) -> usize { + unsafe { + (*self.0.get()) + .as_ref() + .map(|inner| inner.len()) + .unwrap_or(0) + } + } +} + +impl<'a> IntoIterator for &'a BoxedCellSlice { + type Item = ElementId; + + type IntoIter = BoxedCellSliceIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + BoxedCellSliceIter { + index: 0, + borrow: self, + } + } +} + +pub struct BoxedCellSliceIter<'a> { + index: usize, + borrow: &'a BoxedCellSlice, +} + +impl Iterator for BoxedCellSliceIter<'_> { + type Item = ElementId; + + fn next(&mut self) -> Option { + let result = self.borrow.get(self.index); + if result.is_some() { + self.index += 1; + } + result + } +} + impl<'a> VNode<'a> { /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element<'a> { Some(VNode { key: None, parent: None, - root_ids: &[], + root_ids: BoxedCellSlice::default(), dynamic_nodes: &[], dynamic_attrs: &[], - template: Template { + template: Cell::new(Template { name: "dioxus-empty", roots: &[], node_paths: &[], attr_paths: &[], - }, + }), }) } @@ -77,7 +176,7 @@ impl<'a> VNode<'a> { /// /// Returns [`None`] if the root is actually a static node (Element/Text) pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> { - match &self.template.roots[idx] { + match &self.template.get().roots[idx] { TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None, TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { Some(&self.dynamic_nodes[*id]) @@ -102,32 +201,85 @@ impl<'a> VNode<'a> { /// /// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of /// ways, with the suggested approach being the unique code location (file, line, col, etc). -#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] pub struct Template<'a> { /// The name of the template. This must be unique across your entire program for template diffing to work properly /// /// If two templates have the same name, it's likely that Dioxus will panic when diffing. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_string_leaky") + )] pub name: &'a str, /// The list of template nodes that make up the template /// /// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm. + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] pub roots: &'a [TemplateNode<'a>], /// The paths of each node relative to the root of the template. /// /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the /// topmost element, not the `roots` field. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_bytes_leaky") + )] pub node_paths: &'a [&'a [u8]], /// The paths of each dynamic attribute relative to the root of the template /// /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the /// topmost element, not the `roots` field. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "deserialize_bytes_leaky") + )] pub attr_paths: &'a [&'a [u8]], } +#[cfg(feature = "serialize")] +fn deserialize_string_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a str, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = String::deserialize(deserializer)?; + Ok(&*Box::leak(deserialized.into_boxed_str())) +} + +#[cfg(feature = "serialize")] +fn deserialize_bytes_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a [&'a [u8]], D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = Vec::>::deserialize(deserializer)?; + let deserialized = deserialized + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>(); + Ok(&*Box::leak(deserialized.into_boxed_slice())) +} + +#[cfg(feature = "serialize")] +fn deserialize_leaky<'a, 'de, T: serde::Deserialize<'de>, D>( + deserializer: D, +) -> Result<&'a [T], D::Error> +where + T: serde::Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let deserialized = Box::<[T]>::deserialize(deserializer)?; + Ok(&*Box::leak(deserialized)) +} + impl<'a> Template<'a> { /// Is this template worth caching at all, since it's completely runtime? /// @@ -144,7 +296,11 @@ impl<'a> Template<'a> { /// /// This can be created at compile time, saving the VirtualDom time when diffing the tree #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] pub enum TemplateNode<'a> { /// An statically known element in the dom. /// @@ -164,9 +320,11 @@ pub enum TemplateNode<'a> { /// A list of possibly dynamic attribues for this element /// /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`. + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] attrs: &'a [TemplateAttribute<'a>], /// A list of template nodes that define another set of template nodes + #[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))] children: &'a [TemplateNode<'a>], }, @@ -512,8 +670,8 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([VNode { parent: self.parent, - template: self.template, - root_ids: self.root_ids, + template: self.template.clone(), + root_ids: self.root_ids.clone(), key: self.key, dynamic_nodes: self.dynamic_nodes, dynamic_attrs: self.dynamic_attrs, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 217768c50..c772b8317 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -142,7 +142,8 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future /// } /// ``` pub struct VirtualDom { - pub(crate) templates: FxHashMap>, + // Maps a template path to a map of byteindexes to templates + pub(crate) templates: FxHashMap>>, pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -354,10 +355,11 @@ impl VirtualDom { while let Some(el_ref) = parent_path { // safety: we maintain references of all vnodes in the element slab let template = unsafe { &*el_ref.template }; + let node_template = template.template.get(); let target_path = el_ref.path; for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = template.template.attr_paths[idx]; + let this_path = node_template.attr_paths[idx]; // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing // we should fix this so that we look for "onclick" instead of "click" @@ -454,6 +456,30 @@ impl VirtualDom { } } + /// Replace a template at runtime. This will re-render all components that use this template. + /// This is the primitive that enables hot-reloading. + /// + /// The caller must ensure that the template refrences the same dynamic attributes and nodes as the original template. + /// + /// This will only replace the the parent template, not any nested templates. + pub fn replace_template(&mut self, template: Template<'static>) { + self.register_template_first_byte_index(template); + // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine + for (_, scope) in &self.scopes { + if let Some(RenderReturn::Sync(Some(sync))) = scope.try_root_node() { + if sync.template.get().name.rsplit_once(':').unwrap().0 + == template.name.rsplit_once(':').unwrap().0 + { + let height = scope.height; + self.dirty_scopes.insert(DirtyScope { + height, + id: scope.id, + }); + } + } + } + } + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// /// The mutations item expects the RealDom's stack to be the root of the application. diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index c732f5a8b..2d08ad79a 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,7 +1,7 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; use dioxus_core::*; use dioxus_html::HtmlEvent; -use futures_channel::mpsc::{unbounded, UnboundedSender}; +use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use futures_util::StreamExt; #[cfg(target_os = "ios")] use objc::runtime::Object; @@ -25,7 +25,10 @@ pub(super) struct DesktopController { pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, pub(super) proxy: EventLoopProxy, + pub(super) event_tx: UnboundedSender, + #[cfg(debug_assertions)] + pub(super) templates_tx: UnboundedSender>, #[cfg(target_os = "ios")] pub(super) views: Vec<*mut Object>, @@ -41,6 +44,7 @@ impl DesktopController { ) -> Self { let edit_queue = Arc::new(Mutex::new(Vec::new())); let (event_tx, mut event_rx) = unbounded(); + let (templates_tx, mut templates_rx) = unbounded(); let proxy2 = proxy.clone(); let pending_edits = edit_queue.clone(); @@ -68,6 +72,18 @@ impl DesktopController { loop { tokio::select! { + template = { + #[allow(unused)] + fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { + #[cfg(debug_assertions)] + return templates_rx.select_next_some(); + #[cfg(not(debug_assertions))] + return std::future::pending(); + } + maybe_future(&mut templates_rx) + } => { + dom.replace_template(template); + } _ = dom.wait_for_work() => {} Some(json_value) = event_rx.next() => { if let Ok(value) = serde_json::from_value::(json_value) { @@ -103,6 +119,8 @@ impl DesktopController { quit_app_on_close: true, proxy: proxy2, event_tx, + #[cfg(debug_assertions)] + templates_tx, #[cfg(target_os = "ios")] views: vec![], } @@ -133,8 +151,4 @@ impl DesktopController { } } } - - pub(crate) fn set_template(&self, _serialized_template: String) { - todo!("hot reloading currently WIP") - } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 7b41d33e0..943fbf91e 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -179,11 +179,6 @@ pub enum UserWindowEvent { DragWindow, FocusWindow, - /// Set a new Dioxus template for hot-reloading - /// - /// Is a no-op in release builds. Must fit the right format for templates - SetTemplate(String), - Visible(bool), Minimize(bool), Maximize(bool), @@ -232,7 +227,6 @@ impl DesktopController { match user_event { Initialize | EditsReady => self.try_load_ready_webviews(), - SetTemplate(template) => self.set_template(template), CloseWindow => *control_flow = ControlFlow::Exit, DragWindow => { // if the drag_window has any errors, we don't do anything diff --git a/packages/desktop/src/hot_reload.rs b/packages/desktop/src/hot_reload.rs index e2628cdd4..e1383390d 100644 --- a/packages/desktop/src/hot_reload.rs +++ b/packages/desktop/src/hot_reload.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use dioxus_core::VirtualDom; +use dioxus_core::Template; use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; use std::io::{BufRead, BufReader}; @@ -13,7 +13,7 @@ fn handle_error(connection: std::io::Result) -> Option>) { let latest_in_connection: Arc>>> = Arc::new(Mutex::new(None)); @@ -36,11 +36,9 @@ pub(crate) fn init(_dom: &VirtualDom) { let mut buf = String::new(); match conn.read_line(&mut buf) { Ok(_) => { - todo!() - // let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap(); - // channel - // .start_send(SchedulerMsg::SetTemplate(Box::new(msg))) - // .unwrap(); + let msg: Template<'static> = + serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap(); + proxy.unbounded_send(msg).unwrap(); } Err(err) => { if err.kind() != std::io::ErrorKind::WouldBlock { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0080e671b..72ebf2f8a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -106,6 +106,9 @@ pub fn launch_with_props(root: Component

, props: P, mut cf let event_loop = EventLoop::with_user_event(); let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy()); + #[cfg(debug_assertions)] + hot_reload::init(desktop.templates_tx.clone()); + event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; diff --git a/packages/dioxus/benches/jsframework.rs b/packages/dioxus/benches/jsframework.rs index 24ca2fe69..b3405b2bd 100644 --- a/packages/dioxus/benches/jsframework.rs +++ b/packages/dioxus/benches/jsframework.rs @@ -42,7 +42,7 @@ fn create_rows(c: &mut Criterion) { c.bench_function("create rows", |b| { let mut dom = VirtualDom::new(app); - dom.rebuild(); + let _ = dom.rebuild(); b.iter(|| { let g = dom.rebuild(); diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 3436dcc93..de5db6645 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.2.1" } +dioxus-rsx = { path = "../rsx", optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { version = "0.2.79", optional = true } @@ -47,3 +48,4 @@ serde_json = "*" default = ["serialize"] serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"] wasm-bind = ["web-sys", "wasm-bindgen"] +hot-reload-context = ["dioxus-rsx"] \ No newline at end of file diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index efbb4d797..931bc3d60 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,61 +1,258 @@ #![allow(non_upper_case_globals)] - +#[cfg(feature = "hot-reload-context")] +use crate::{map_global_attributes, map_svg_attributes}; use crate::{GlobalAttributes, SvgAttributes}; +#[cfg(feature = "hot-reload-context")] +use dioxus_rsx::HotReloadingContext; pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); +macro_rules! impl_attribute { + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (DEFAULT), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident ($name:literal), + ) => { + pub const $fil: AttributeDiscription = ($name, None, false); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (volatile), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), None, true); + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (in $ns:ident), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), false) + }; + + ( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident (in $ns:ident : volatile), + ) => { + pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), true) + }; +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_attribute_match { + ( + $attr:ident $fil:ident: $vil:ident (DEFAULT), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!($fil), None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident (volatile), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!($fil), None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident ($name:literal), + ) => { + if $attr == stringify!($fil) { + return Some(($name, None)); + } + }; + + ( + $attr:ident $fil:ident: $vil:ident (in $ns:ident), + ) => { + if $attr == stringify!($fil) { + return Some((stringify!(fil), Some(stringify!(ns)))); + } + }; +} + +macro_rules! impl_element { + ( + $(#[$attr:meta])* + $name:ident None { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = None; + + $( + impl_attribute!( + $(#[$attr_method])* + $fil: $vil ($extra), + ); + )* + } + + impl GlobalAttributes for $name {} + }; + + ( + $(#[$attr:meta])* + $name:ident $namespace:tt { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $name; + + impl SvgAttributes for $name {} + + impl $name { + pub const TAG_NAME: &'static str = stringify!($name); + pub const NAME_SPACE: Option<&'static str> = Some($namespace); + + $( + impl_attribute!( + $(#[$attr_method])* + $fil: $vil in $namespace $extra + ); + )* + } + } +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_element_match { + ( + $el:ident $name:ident None { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return Some((stringify!($name), None)); + } + }; + + ( + $el:ident $name:ident $namespace:tt { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return Some((stringify!($name), Some(stringify!($namespace)))); + } + }; +} + +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_element_match_attributes { + ( + $el:ident $attr:ident $name:ident None { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + $( + impl_attribute_match!( + $attr $fil: $vil ($extra), + ); + )* + } + }; + + ( + $el:ident $attr:ident $name:ident $namespace:tt { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + $( + impl_attribute_match!( + $attr $fil: $vil in $namespace $extra + ); + )* + } + } +} + macro_rules! builder_constructors { ( $( $(#[$attr:meta])* - $name:ident { + $name:ident $namespace:tt { $( $(#[$attr_method:meta])* - $fil:ident: $vil:ident, + $fil:ident: $vil:ident $extra:tt, )* }; )* - ) => { - $( - #[allow(non_camel_case_types)] - $(#[$attr])* - pub struct $name; - - - impl $name { - pub const TAG_NAME: &'static str = stringify!($name); - pub const NAME_SPACE: Option<&'static str> = None; + ) => { + #[cfg(feature = "hot-reload-context")] + pub struct HtmlCtx; + #[cfg(feature = "hot-reload-context")] + impl HotReloadingContext for HtmlCtx { + fn map_attribute(element: &str, attribute: &str) -> Option<(&'static str, Option<&'static str>)> { $( - pub const $fil: AttributeDiscription = (stringify!($fil), None, false); + impl_element_match_attributes!( + element attribute $name $namespace { + $( + $fil: $vil $extra, + )* + } + ); )* + map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute)) } - impl GlobalAttributes for $name {} - )* - }; - - ( $( - $(#[$attr:meta])* - $name:ident <> $namespace:tt { - $($fil:ident: $vil:ident,)* - }; - )* ) => { - $( - #[allow(non_camel_case_types)] - $(#[$attr])* - pub struct $name; - - impl SvgAttributes for $name {} - - impl $name { - pub const TAG_NAME: &'static str = stringify!($name); - pub const NAME_SPACE: Option<&'static str> = Some($namespace); - + fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> { $( - pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($namespace)), false) + impl_element_match!( + element $name $namespace { + $( + $fil: $vil $extra, + )* + } + ); )* + None } + } + + $( + impl_element!( + $(#[$attr])* + $name $namespace { + $( + $(#[$attr_method])* + $fil: $vil $extra, + )* + } + ); )* }; } @@ -75,63 +272,63 @@ builder_constructors! { /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) /// element. /// - base { - href: Uri, - target: Target, + base None { + href: Uri DEFAULT, + target: Target DEFAULT, }; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) /// element. - head {}; + head None {}; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) /// element. - link { + link None { // as: Mime, - crossorigin: CrossOrigin, - href: Uri, - hreflang: LanguageTag, - media: String, // FIXME media query - rel: LinkType, - sizes: String, // FIXME - title: String, // FIXME - r#type: Mime, - integrity: String, + crossorigin: CrossOrigin DEFAULT, + href: Uri DEFAULT, + hreflang: LanguageTag DEFAULT, + media: String DEFAULT, // FIXME media query + rel: LinkType DEFAULT, + sizes: String DEFAULT, // FIXME + title: String DEFAULT, // FIXME + r#type: Mime DEFAULT, + integrity: String DEFAULT, }; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) /// element. - meta { - charset: String, // FIXME IANA standard names - content: String, - http_equiv: HTTPEquiv, - name: Metadata, + meta None { + charset: String DEFAULT, // FIXME IANA standard names + content: String DEFAULT, + http_equiv: HTTPEquiv DEFAULT, + name: Metadata DEFAULT, }; /// Build a /// [`